ホーム < ゲームつくろー! < Programming TIPs編

その13 引数付きコンストラクを持つクラスの配列を初期化する方法


 クラスのコンストラクタには引数を持たせる事ができます。いわゆる「引数付きコンストラクタ」と呼ばれるものです。引数付きコンストラクタを持つクラスは、宣言時に適切な値を渡さないとコンパイルエラーになってしまいます。例えばこんな感じです:

引数付きコンストラクタを持つクラスの宣言(エラー)
class MyClass {
public:
   // 引数付きコンストラクタ
   MyClass( int value ) : m_Value( value )
   {
   }

private:
   int m_Value;
};

int main() {
   MyClass object;   // コンパイルエラーになります!!
}


 このクラスを正しく宣言するには引数を付ける必要があります:

引数付きコンストラクタを持つクラスの宣言(OK)
MyClass object( 100 );

 では、MyClassを持つクラス内でこのクラスを宣言するとどうなるでしょうか?

クラスメンバとして宣言(エラー)
class World {
private:
   MyClass object;
};

これはコンパイルエラーになってしまいます。MyClassを初期化するための引数が無いからです。かと言って、

クラスメンバとして宣言(エラー)
class World {
private:
   MyClass object( 100 );
};

という書き方は出来ません。こういう場合、コンストラクタで次のようにすると正しく初期化できます:

クラスメンバとして宣言(OK)
class World {
public:
   World() : object( 100 )
  {
   }

private:
   MyClass object;
};

ここまではC++の教科書や参考書にも載っているお決まりの初期化方法です。しかし、次のような場合はどうでしょう?

クラスの配列として宣言(エラー)
class World {
private:
   MyClass object[5];
};

引数付きコンストラクタの配列がメンバにあります。これらは実体ですから、Worldクラスのコンストラクタで引数を渡さなければならないのですが…実は、これを渡す方法がC++には用意されていません。ですから、

配列の初期化は普通の方法では無理
// 駄目その1
World::World() : object[5]( 100 ) {
}

// 駄目その2
World::World() : object( 100 ) {
}

// 駄目その3
World::World() {
   object = new MyClass( 100 )[ 5 ];
}

などとあがいてもすべて文法エラーとなります。

 では、いったいどうやって初期化したら良いのか?そこが、この章のポイントです。



@ デフォルトコンストラクタを作る

 MyClassの配列が宣言できない理由は、引数付きコンストラクタに値を渡す方法が文法的に用意されていないためです。そこで、大胆にもまずMyClassにデフォルトコンストラクタを作ります:

デフォルトコンストラクタを追加する
class MyClass {
public:
   // デフォルトコンストラクタ
   MyClass() : m_Value( 0 ) // ダミーの初期化
   {
   }

   // 引数付きコンストラクタ
   MyClass( int value ) : m_Value( value )
   {
   }

private:
   int m_Value;
};

こうすると少なくともWorldクラスが持つ配列は何もせずに正常に初期化されます。「いや、でも引数でしか値を渡せない事があるじゃない(怒)」と思われるかもしれません。その通りなんです。そういうので良くあるのがコンストラクタの引数でIDirect3DDevice9のような描画デバイスを渡すクラスです。ここを逃すとこのデバイスを渡す方法が無くなってしまいます。デフォルトコンストラクタでは渡しようが無い。じゃぁ、この方法は駄目じゃないかとなるわけです。もちろん、秘策があります。



A placement newの利用

 秘策は「placement new」を使う事にあります。placement newというのはnewの特別バージョンです。通常のnewの使い方は、

通常のnew
MyClass *pmyclass = new MyClass( 100 );

と左辺にポインタを取り、右辺で動的にオブジェクトを作ってそのポインタを左辺に代入します。この時右辺のオブジェクトは「ヒープ領域」というメモリ領域に作られます。一方placement newを使った場合、上と同様にするには次のようにします:

placement new
#include <new>  // これが必要

MyClass myclass;   // デフォルトコンストラクタで初期化
new( &myclass ) MyClass( 100 );   // 引数付きコンストラクタで置き換え

newに括弧が付いています。その中にはアドレスを指定します。その後にMyClass( 100 )と引数付きコンストラクタで初期化したオブジェクトが来ます。

 placement newは通常のnewと違いヒープ領域からメモリを確保しません。プログラマが予め確保したメモリに対して生成したオブジェクトを流し込んでくれます。例えば上の例だとローカル変数として確保したmyclassにMyClass(100)と初期化したオブジェクトが格納されます。

 これを踏まえてWorldクラスでMyClassの配列を初期化するには次のようにします:

placement newによるクラスの初期化
#include <new>  // これが必要

class World {
public:
   World() {
      // 配列をplacement newを使って初期化する
      for ( int i = 0; i < 5; i++ ) {
         new( object + i ) MyClass( 100 );
      }
   }

private:
   MyClass object[ 5 ];
};

 文法的に一気にどばっと初期化する方法が無いのでループで1つずつ初期化していきます。注目は「object + i」とポインタを1つずつずらしながら初期化する点です。これによりobject[5]の中にめでたくオブジェクトが格納されます。

 newと聞くと対応するdeleteを考えてしまうのですが、実はplacement newはdeleteする必要がありません。と言うのもplacement newは単に指定のメモリにオブジェクトを置くだけで、それ自身がメモリを確保するわけではないからです。ですから、上のクラスのデストラクタで何か処理する必要はありません。


 これまで配列の初期化に悩まれていた方は是非お試し下さい。