オブジェクト指向の倒し方04/11 | 「オブジェクト指向の倒し方、知らないでしょ? オレはもう知ってますよ」

「オブジェクト指向の倒し方、知らないでしょ? オレはもう知ってますよ」

オブジェクト指向と公的教義の倒し方を知っているブログ
荀子を知っているブログ 織田信長を知っているブログ

<仕様調査-番地変数による手続きの呼び出しの仕様 1/2>
 
C++の仕様には「手続き番地変数」と「型の手続き番地変数」が存在し、番地変数を使って型の手続きを呼べる( thiscall できる )ようになっている。
 
ここではCの手続きの呼び出し( cdecl )の方も順次解説していく。
 
なお、これも仮想型の手続きについては別途、後述する。
 
設計側
ClassExe
{
public:
    int   iData01;
    int   iData02;
    ClassExe();
    void ProcE01();
    int  ProcE02( int iRef01, int iRef02 );
};
ClassExe::ClassExe()
{
   iData01 = 100;
   iData02 = 200;
}
void Class::Proc01()
{
    // 特に何もしない
}
int  Class::Proc02( int iRef01, int iRef02 )
{
    int iRet = iData01 + iData02 + iRef01 + iRef02;
    return iRet;
}
 
利用側1
void ( ClassExe::*pThisPr01 )();            // 型の手続きの番地変数
int  ( ClassExe::*pThisPr02 )( int, int );  // 型の手続きの番地変数(戻り値と引数あり)
 
pThisPr01 = ClassExe::ProcE01;
pThisPr02 = ClassExe::ProcE02;
 
int iRet;
ClassExe clsE;
// 呼び出し①
       ( ( &clsE )->*pThisPr01 )();
iRet = ( ( &clsE )->*pThisPr02 )( 500, 1000 ); // iRet に 1800 が入る
// 呼び出し②
ClassExe *pcls = &clsE
       ( ( pcls )->*pThisPr01 )();
iRet = ( ( pcls )->*pThisPr02 )( 500, 1000 ); // iRet に 1800 が入る

「手続き番地変数」と「型の手続き番地変数」は、書式が独特で、pThisPr01、pThisPr02 の部分が変数名になる。
 
手続きに戻り値や引数がある場合は、その形に合わせておかないと、呼び出す際に強制エラーが起きる。
 
もちろん手続き番地が入っていない番地変数で手続き呼び出そうとする時も強制エラーになる。
 
呼び出し①、呼び出し②の部分が呼び出しだが、型の番地変数を用いていない時と用いている時の簡単な例であり、やっていることは同じである。
 
手続きの呼び出し方に番地変数を使っている他は、後の仕組みは先述した <仕様調査-型の手続きの呼び出しと this の関係> と原理的に大体同じである。
 
想定さえされていれば this への送り番地は何でも良い点も同じである。

利用側2
void ( ClassExe::*pThisPr01 )();             // 型の手続きの番地変数
int  ( ClassExe::*pThisPr02 )( int, int );   // 型の手続きの番地変数(戻り値と引数あり)
 
pThisPr01 = &ClassExe::ProcE01;
pThisPr02 = &ClassExe::ProcE02;
 
int iList = [] = { 1000, 2000, 3000, 4000 };
       ( ( ( ClassExe * )0x0000 )->*pThisPr01 )();
iRet = ( ( ( ClassExe * )&iList )->*pThisPr02 )( 500, 1000 ); // iRet に 4500 が入る

次はCの手続きの呼び出し( cdecl )の番地変数について説明する。

設計側
void CProc01();                          // Cの手続き
int  CProc02( int iRef01, int iRef02 );  // Cの手続き(戻り値と引数あり)
void CProc01()
{
    // 特に何もしない
}
int  CProc02( int iRef01, int iRef02 )
{
    int iRet = iRef01 + iRef02;
    return iRet;
}

例:利用側
void ( *pCPr01 )();              // Cの手続きの番地変数
int  ( *pCPr02 )( int, int );    // Cの手続きの番地変数(戻り値と引数あり)
 
pCPr01 = &CProc01;
pCPr02 = &CProc02;
 
       ( *pCPr01 )();
iRet = ( *pCPr02 )( 500, 1000 ); // iRet に 1500 が入る

Cの手続き番地変数での呼び出し( cdecl )の方は、this の存在がない分だけいくらかすっきりしている。
 
型の手続き番地変数の方は、誤解や錯覚をしやすい例を示す。
 
設計側
class CLASS{}; // 変数も手続きもない、空の型
class ClassA
{
public:
    int iDataA01;
    int iDataA02;
    ClassA()
    {
        iDataA01 = 100;
        iDataA02 = 200;
    }
    int ProcA()
    {
        int iRet = iDataA01 + iDataA02;
        return iRet;
    }
    int ProcB()
    {
        // 特に何もしない
        return 0;
    }
};
class ClassB
{
public:
    int iDataB01;
    int iDataB02;
    int iDataB03;
    int iDataB04;
    ClassB()
    {
        iDataB01 =  50;
        iDataB02 = 150;
        iDataB03 = 250;
        iDataB04 = 350;
    }
    int ProcA()
    {
        // 特に何もしない
        return 0;
    }
    int ProcB()
    {
        int iRet = iDataB01 + iDataAB02 + iDataAB03 + iDataAB04;
        return iRet;
    }
};
 
利用側
int ( CLASS::*pThisProc )();  // 型の手続きの番地変数(戻り値あり)
int iRet;
ClassA clsA;
ClassB clsB;
 
pThisProc = ( ( CLASS:* )() )&ClassA::ProcA;
 
// 番地の指定の方が CLASS でも、ちゃんと ClassA の ProcA が呼び出される
iRet = ( ( CLASS::* )&clsB )->pThisProc )();  // iRet に 200 が入る(情報領域は clsB のため)
 
pThisProc = ( ( CLASS:* )() )&ClassB::ProcB;
 
// こちらも同じく ClassB の ProcB が呼び出される
iRet = ( ( CLASS::* )&clsB )->pThisProc )();  // iRet に 800 が入る

「型の手続き番地変数」での手続きの呼び出しは、this に送る番地の型は何の関連性もないはずであるが、書式上は「なんでもいいから型(クラス)を指定しておかなければならない」というおかしな仕様になっている。
 
「型の手続き番地変数」で型の手続きを呼び出す場合の番地側の型(クラス)の指定は、型(クラス)でさえあればなんでも良いのである。
 
なぜこんなおかしな仕様になっているのかは、型の手続きの呼び出しの書式仕様を、番地変数を使う場合と使わない場合で書式仕様を分け、安直に使いまわしていることが原因と思われる。
 
まず、番地変数を使わない型の手続きの呼び出しの時は、ここでいう ProcA() を呼び出そうとした場合、ClassA の ProcA() なのか ClassB の ProcA() なのか、どこの型に所属している手続きなのかを判別するために、this 番地の型が必要だという理由がある。
 
しかし型の手続きの番地とは「どの型の」まで一緒になっている番地であるため、番地変数を使って型の手続きを呼ぶ場合は this 側の型を指定する理由が全くない。
 
この例で明確だが、何の手続きも存在しない CLASS という型で this に番地を指定しても、実際には「その番地が示している型の手続き」が呼び出される仕様になっている。
 
そもそも this 番地の仕様にしても「受け取る側(呼び出される側)は、その this 番地は自身の型情報として受け取る大前提」となっているため、この例のように CLASS と指定して送ったからといって、呼び出された側が型情報を CLASS と見なして動作しようとはしない。
 
型の手続きの呼び出し( thiscall )は、番地変数を使う時と使わない時とで意味がだいぶ違うため、本来は書式仕様を別にするべきだっただろう。
 
「プロシージャ判別子」という概念がC++には存在せず、内部動作(機械語)を見ていると、型の手続き呼び出し( thiscall )とはそもそもCの手続き呼び出し( cdecl )の概念の使いまわしであり、つじつまが合うように機械語を埋め込んでいるに過ぎない。
 
ここから少々強引な話に入っていくが、あくまでC++の仕様の認識を第一目的とした話として、進めていく。
 
まず型の手続きの呼び出し( thiscall )の場合は、Cの手続きの呼び出し( cdecl )との違いを整理すると、前者には主に2点の「追加的な違い」がある。(仮想仕様については後述)
 
1つは、this 番地を送るようになっている点、もう1つは条件による this 番地の自動変更が行われる仕様である点で、その他にはCの手続きの呼び出し( cdecl )との違いはない。
 
そのため「Cの手続き番地変数」を使って、型の手続きを呼ぶこともできるし、逆に「型の手続き番地変数」を使ってCの手続きを呼ぶこともできる。
 
その場合はもちろん、this が不要なのに指定しなければならない呼び出しになってしまうのと、逆に this が必要なのに指定せずに呼び出すことになってしまうという、ちぐはぐな状態になるが、それ自体が原因で強制エラーが起きることはなく、後者においては設計者がその不特定性を解って想定していれぱ別に問題はない。
 
ただしC++のコンパイルの仕様としては、左辺と右辺(代入と取得)が
 
①「型の手続き番地変数」=「型の手続き番地」
②「型の手続き番地変数」=「型の手続き番地変数」
 
であることを守らせる禁則( C2440 )が備わっていて、これはキャストを用いても①か②でないとこの禁則にかかる。
 
一方でCの手続き番地の方ではそんな監視は全く行われておらず、取得と代入はもっと自由になっているため、ただの番地になぜこんな禁則を入れているのか理由はよく解らない。
 
実際はいくらでも実現可能なため順述していくが、これについては「Cから新たに実装される番地の概念以降は監視を厳しくしていく」という格好の良い理由より、単に「 thiscall の中途半端な使いまわしの仕様を追求されるのが面倒だから」という格好の悪い理由だけのように見える。
 
話は戻り、型の手続き番地も所詮はただの番地であり、先述の主な2点の追加的な違い以外は、Cの手続きとほとんど同じ仕様である。(仮想仕様については後述)
多少わずらわしいが C2440 をかいくぐるため memcpy を使えばあっけなく取得できるため、このもくろみは可能である。
 
そもそも機械語的には C2440 という概念などない上に、設計における理論性も何も内在しておらず、機械語だと極めて基本的な命令で取得可能である。
 
設計側
class CLASS{}; // 空の型
class ClassExe
{
public:
    int iEx01;
    int iEx02;
    int ProcExe01()
    {
        int iRet = 3333;
        return iRet;
    }
    int ProcExe02()
    {
        int iList[] = { 123, 456 };
        int iRet = ( ( ClassExe * )&iList )->ProcExe03();
        return iRet
    }
    int ProcExe03()
    {
        int iRet = iEx01 + iEx02;
        return iRet;
    }
};
int CProcExe(); // Cの手続き(戻り値あり)
int CProcExe()
{
    int iRet = 5000;
    return iRet;
}

利用側
int iRet;      // 戻り値用
void *pAdr;    // 代入と取得の実験用の番地変数
int ( *pCdeclProc )();             // Cの手続きの番地変数(戻り値あり)
int ( CLASS::*pThisProc )();       // 型の手続きの番地変数(戻り値あり)
 
//型の手続き番地を、型の手続き番地変数でないものに入れようとすると C2440 になる例
//pAdr   = ( void * )&ClassExe::ProcExe;       // 標準の番地変数に、型の手続き番地を入れようとする
//pCProc = ( void( * )() )&ClassExe::ProcExe;  // Cの手続き番地変数に、型の手続き番地を入れようとする
 
//型の手続き番地変数に、型の手続き番地でないものに入れようとすると C2440 になる例
//pThisProc = ( void( CLASS::* )() )pAdr;      // 型の手続き番地変数に、標準の番地変数から番地を入れようとする
//pThisProc = ( void( CLASS::* )() )pCProcExe; // 型の手続き番地変数に、Cの手続き番地変数から番地を入れようとする
 
// Cの手続き番地だと自由に出し入れできる
pAdr = ( void * )&CProcExe;
pCdeclProc = ( int( * )() )pAdr;
pAdr = ( void * )&pCdeclProc;
 
// memcpy のサイズを用意しておく
size_t stSizeAdd = sizeof( void * ); // stSizeAdd に 32 Bit なら 4 が、64 Bit なら 8 が入る
 
// いったんCの手続き番地変数の方に、Cの手続き番地を入れておく
pCdeclProc = &CProcExe;
 
// 型の手続き番地変数に、Cの手続き番地を代入する
memcpy( &pThisProc, &pCdeclProc, stSizeAdd );
 
iRet = ( ( CLASS * )0x00 )->pThisProc(); // this の指定の意味が全くなくなるが CProcExe() が呼ばれ 5000 が戻る
 
// いったん型の手続き番地変数の方に、型の手続き番地を入れておく
pThisProc = &ClassExe::ProcExe02;
 
// Cの手続き番地変数に、型の手続き番地を代入する
memcpy( &pCdeclProc, &pThisProc, stSizeAdd );
 
iRet = ( pCdeclProc )(); // this を指定せずに型の手続き ClassExe::ProcExe02() が呼ばれ、579 が戻る
 
// 以下は 32 Bit 限定だが、構造を説明するために例を示す
ClassExe clsExe;
clsExe.iEx01 = 1000;
clsExe.iEx02 = 2000;
 
// いったん型の手続き番地変数の方に、型の手続き番地を入れておく
pThisProc = &ClassExe::ProcExe03;
 
// Cの手続き番地変数に、型の手続き番地を代入する
memcpy( &pCdeclProc, &pThisProc, stSizeAdd );
// 32 bit 限定の例
_asm
{
    mov ecx, dword ptr [clsExe]
}
iRet = ( pCdeclProc )(); // ここでは this を指定していないが、直前に clsExe の番地を ecx レジスタに入れている
                         // そのため ClassExe::ProcExe03() が呼ばれると、claExe の領域番地が参照され iRet に 3000 が戻る
 
// 32 Bit 限定だが、機械語だと何のわずらわしさもなく1つの取得につき1処理で代入できる
_asm
{
    mov dword ptr [pThisProc] , offset CProcExe
 mov dword ptr [pCdeclProc], offset ClassExe::ProcExe03
}
 
Cの手続きでも、型の手続きでも、戻り値と引数の形さえあっていれば、書式はちぐはぐになるが手続きは呼び出せる。
 
この場合、例えば ClassExe::ProcExe02() のように、this が不特定であることが想定されている作りになっていれば問題はない。
 
型の手続きを呼び出す時の、this の領域番地の指定の仕組みは、機械語の観点だと、とにかく手続きが呼ばれる直前に ?cx レジスタ(32ビットだと ecx、64ビットだと rcx )に領域番地が入っていればよいという動作になっている。
 
内部的には型の手続き先は ecx ( rcx ) に this の領域番地が入っている想定から受け取るという動作をしているだけであり、要するに送り側の this はただの ecx ( rcx ) である。
 
条件による自動番地変更にしても「型の手続きが呼ばれる」動作を契機に自動的に行っているに過ぎず、とにかく「型の手続きが呼ばれる」という直前に ecx ( rcx ) に領域番地が入っていれば、手続き先の this に通達することができる。
 
そのため「this が不特定」という言い方は厳密には、それは何かの演算や判定後に ecx ( rcx ) に残ったままのいわば残骸値が this の値と見なされる状態となる。
 
この仕組みから、機械語を使わずに、演算や判定を逆用して ecx に狙い通りに領域番地が入るようにしておいて、Cの手続き番地変数で型の手続き番地を呼び出して狙い通りに this に設置するという、小ざかしい荒わざも可能である。
 
上記の「例:利用側」の続き(32ビットの場合)
 
// 型の手続き番地変数の方に、型の手続き番地を入れておく
pThisProc = &ClassExe::ProcExe03;
 
// Cの手続き番地変数に、型の手続き番地を代入する
memcpy( &pCdeclProc, &pThisProc, stSizeAdd );
__int64 setthis = ( __int64 )clsExe; << 32;
if( setthis == setthis ){ setthis = 0; }
( *pCdeclProc )();

この例は32ビットの場合だが、これで、ecx に clsExe の領域番地が入った状態で、Cの手続き番地変数から型の手続きを呼び出すことになり、呼び出し先は claExe の領域番地を this に受け取れるようになる。
 
例えばあらゆる番地を一元管理する設計をしたい場合は、型の手続き番地だけは C2440 の禁則によって直接やりとりができず、それをしたい場合は memcpy の必要に迫られることを認知しておく必要がある。
 
次に、筆者にとっての「こういう方法もある」という紹介を1つしておきたい。
 
それは、Visual C++ の「新たなプロジェクト」の Windows の標準ウインドウのアプリケーション作成から始める際の、基本的なウインドウ処理である WndProc を、Cの手続きではなく型の手続きで受け取りたい場合である。
 
設計側
ClassWindow
{
public:
    LRESULT WindowProc( HWND, UINT, WPARAM, LPARAM );
}
LRESULT ClassWindow::WindowProc( HWND, UINT, WPARAM, LPARAM )
{
    /*
        ここでは
            WM_CREATE
            WM_COMMAND
            WM_PAINT
        といった処理になるが省略
    */
    return 0;
}

例:利用側
ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style   = CS_HREDRAW | CS_VREDRAW;
    /* ここを変更する
        wcex.lpfnWndProc = (WNDPROC)WndProc;
    */
    wcex.cbClsExtra  = 0;
    wcex.cbWndExtra  = 0;
    wcex.hInstance  = hInstance;
    wcex.hIcon   = LoadIcon(hInstance, (LPCTSTR)IDI_CLASSTEST03);
    wcex.hCursor  = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName = (LPCTSTR)IDC_CLASSTEST03;
    wcex.lpszClassName = szWindowClass;
    wcex.hIconSm  = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);
 
    LRESULT ( ClassWindow::*pThisProc )( HWND, UINT, WPARAM, LPARAM ); // 型の手続き番地変数
    pThisProc = &ClassWindow::WindowProc;
    size_t stSize = sizeof( pThisProc ); // 番地変数のバイト数取得のため pThisProc の所は ( void * ) 等でも可
    memcpy( &wcex.lpfnWndProc, &pThisProc, stSize );
 
    // 32ビット限定だが、機械語だと上記4行はこの1命令だけで達成
    _asm
    {
        mov dword ptr wcex.lpfnWndProc, offset ClassWindow::WindowProc
    }
    return RegisterClassEx(&wcex);
}
このように設定しておけば、ウインドウのイベントの基本処理は ClassWindow の WindowProc が呼ばれるようになる。
 
Cの手続き呼び出し方法で型の手続き呼び出することになるため、WindowProc は this が不特定状態である前提の設計をする必要がある。
 
この場合に ClassWindow の this の問題を解消する方法は、ClassWindow を共通変数として変数化(または番地変数を共通変数で用意)しておき、this を失っている箇所はそこから参照する方法もある。
 
設計側
ClassWindow
{
public:
    int iWin01:
    int iWin02:
    int iWin03:
    ClassWindpow()
    {
        iWin01 =  0:
        iWin02 = 50:
        iWin03 = 10;
    }
    LRESULT WindowProc( HWND, UINT, WPARAM, LPARAM );
    ATOM MyRegisterClass( HINSTANCE hInstance );
};
 
/* 共通変数の部分 */
ClassWindow clsWin; // または exturn ClassWindow clsWin;

LRESULT ClassWindow::WindowProc( HWND, UINT, WPARAM, LPARAM )
{
    /*
        ここでは
            WM_CREATE
            WM_COMMAND
            WM_PAINT
        といった処理になるが省略
    */
    // this ではなく共通変数から参照する
    if( clsWin.iWin01 == 0 )
    {
    }
    // 32ビット限定の方法だが、this に入れて参照したい場合
    _asm
    {
        _asm{ mov dword ptr [this], offset [clsWin]
    }
    // 以後 this に領域番地が入っているため参照できる
    if( iWin01 == 0 )
    {
    }
    return 0;
}
 
ここでは先述の MyRegisterClass は省略している。
 
これは32ビット限定の方法になるが、この例で this もただの変数領域に過ぎないことが解ると思う。(機械語には const の概念はない)
 
この方法を用いた場合の64ビットにおける this への代入は、いったん this を送る手続きを用意するという方法になる。

設計側
ClassWindow
{
public:
    int iWin01:
    int iWin02:
    int iWin03:
    ClassWindpow()
    {
        iWin01 =  0:
        iWin02 = 50:
        iWin03 = 10;
    }
    LRESULT WindowProcPre( HWND, UINT, WPARAM, LPARAM );
    LRESULT WindowProc( HWND, UINT, WPARAM, LPARAM );
};
 
/* 共通変数の部分 */
ClassWindow clsWin; // または exturn ClassWindow clsWin;
 
// この手続きが呼び出される時点では this は不特定
LRESULT ClassWindow::WindowProcPre( HWND, UINT, WPARAM, LPARAM )
{
    LRESULT lres = clsWin.WindowProc( HWND, UINT, WPARAM, LPARAM );
    return lres;
}
 
// WindowProcPre から呼ばれる前提
LRESULT ClassWindow::WindowProc( HWND, UINT, WPARAM, LPARAM )
{
    /*
        ここでは
            WM_CREATE
            WM_COMMAND
            WM_PAINT
        といった処理になるが省略
    */
    // WindowProcPre から呼ばれる前提で、this に領域番地が入っているため参照できる
    if( iWin01 == 0 )
    {
    }
    if( iWin02 == 0 )
    {
    }
    return 0;
}
 
省略しているが、先述の MyRegisterClass の所の wcex.lpfnWndProc に  WindowProcPre を設定しておき、WindowProcPre から必要な this で WindowProc を呼べば WindowProc の方で「this からは参照できない」ことのわずらわしさの対策になる。
 
こうまでしてこの箇所( WindowProc )を型手続き化させる利点があるのか? という疑問もあるかも知れないが「 Windows 標準ウインドウアプリケーションで設計したい場合に、Cの手続きは _tWinMain だけにして、後は自分の手で全て型(クラス)にしまいこむ設計をしたい」時は有効である。
 
このような呼び出しで this が不特定となる場合に、必要な this 番地を取得したい場合は、基本的には共通変数を利用する前提となってしまうが、この例のように WindowProcPre という手前の手続きで都合に合わせた this を呼び出すという方法もあり、設計によってはかなり大事な手段になる。
 
今のマイクロソフトの仕様だと、ウインドウの基本処理を型化(クラス化)したいだけのために、MFC仕様を強引に使わせようとする仕様になっている。
 
MFC仕様は標準ウインドウ仕様と比べると、include と resorce が強制付加され、書式から使い勝手がだいぶ違ってきてしまうため、MFCに大した用がないのにMFCを使わされることを避けたい人向けの方法である。
 
※次頁に続く