クラスのインスタンスをスタックに生成できないようにする

概要

 C++0x では関数の delete 指定によりnew演算子で割り当てられないようにすることも出来るらしいですが、その逆のお話。
 コンパイル時ではなくランタイム時になりますが、コンストラクタでスタックの範囲を調べてその中に納まっていたら例外を発生させれば良いわけです。

スタックの範囲を取得する

 スタックの範囲は TIB とよばれるスレッド毎にもつ領域に納められています。TIB の中身は Windows のバージョンによって異なりますが最初の数バイトはどのバージョンでも共通で、その中にスレッドの範囲も収められています。
 TIB の共通部分は WinNT.h で NT_TIB という名前の構造体で以下のように定義されています。

WinNT.h
typedef struct _NT_TIB {
    struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
    PVOID StackBase;
    PVOID StackLimit;
    PVOID SubSystemTib;
    union {
        PVOID FiberData;
        DWORD Version;
    };
    PVOID ArbitraryUserPointer;
    struct _NT_TIB *Self;
} NT_TIB;
typedef NT_TIB *PNT_TIB;
 StackBase がスタックの開始位置、StackLimit がスタックの終了位置になります。この NT_TIB へのポインタを取得する関数は以下のようになります。
//! TIB へのポインタを取得する。
__declspec(naked) NT_TIB* GetTIB()
{ __asm {
  mov eax, dword ptr FS:[18H]
  ret
} }
 インラインアセンブラを使いたくない場合や任意のスレッドの TIB のポインタを取得したいなら以下のようになります。
//! 任意のスレッドの TIB へのポインタを取得する。
NT_TIB* GetTIB(HANDLE i_thread_handle)
{
  LDT_ENTRY ldt_entry = { 0 };
  CONTEXT   context   = { CONTEXT_SEGMENTS, 0 };

  GetThreadContext(i_thread_handle, &context);
  GetThreadSelectorEntry(i_thread_handle, context.SegFs, &ldt_entry);
  return reinterpret_cast<NT_TIB*>(
           ldt_entry.BaseLow
         | (ldt_entry.HighWord.Bytes.BaseMid << 16)
         | (ldt_entry.HighWord.Bytes.BaseHi  << 24)
         );
}
 引数に GetCurrentThread() の戻り値を渡せば自スレッドの TIB へのポインタが取れます。

スタックの範囲の確認

 スタックの範囲がとれれば冒頭で述べたようにコンストラクタで this ポインタが範囲に収まっているか調べればよいわけです。

//! new でしか生成できないクラス
class HeapOnly
{
public:
  //! コンストラクタ
  HeapOnly()
  {
    // this がスタック内にあった場合は例外を投げる。
    if( GetTIB()->StackLimit <= this && this < GetTIB()->StackBase )
      throw "new で生成してください!";
  }
};


int main()
{
  try
  {
    HeapOnly* heap = new HeapOnly;
    printf("ok\n");
    HeapOnly stack;
    printf("ok\n");
  }
  catch(const char* err)
  {
    fprintf(stderr, "error : %s\n", err);
  }

  return 0;
}
実行結果
ok
error : new で生成してください!