コンストラクタの初期化リストで初期化を完了しよう、とは言うけど・・・
昨日 stackoverflow で面白い C++ の質問を見たので、少し紹介してみます。
一昔前の C++ のクラスでよくあるのは、クラスのインスタンスを作ってから、以下のように別で初期化の関数や、オプションなどを設定するような関数を呼ばないといけなかったりします。
OldClass myClass;
myClass.init()
myClass.setOption(true);
新しい C++ だったら、初期化はコンストラクタの中で行い、オプションなどはコンストラクタに引数で渡すことで、クラスのインスタンスが作成されたらすでに完成している状態にすると思います。
NewClass myClass(true);
でも上記のOldClass
のようなクラスを新しいNewClass
の中でメンバー変数として扱いたい場合はどうしますか?
OldClass
自体を変えられたら一番いいのですが、できないケースもあります。
一番簡単な解決方法は以下になると思います。
class NewClass {
public:
NewClass(bool option)
: m_oldClass()
{
m_oldClass.init();
m_oldClass.setOption(true);
}
private:
OldClass m_oldClass;
};
でもm_oldClass
を違うメンバー変数のコンストラクタに渡さないといけない場合はどうしますか?
class NewClass {
public:
NewClass(bool option)
: m_oldClass()
, m_otherClass(m_oldClass)
{
m_oldClass.init();
m_oldClass.setOption(option);
}
private:
OldClass m_oldClass;
OtherClass m_otherClass;
};
これだと、m_oldClass
の初期化やオプションの設定が間に合わないため、m_otherClass
のコンストラクタにまだ初期化されていないm_oldClass
を渡すことになります。
解決方法をいくつか紹介します。
コンマ演算子
class NewClass {
public:
NewClass(bool option)
: m_oldClass()
, m_otherClass((m_oldClass.init(), m_oldClass.setOption(option), m_oldClass))
{}
private:
OldClass m_oldClass;
OtherClass m_otherClass;
};
コンマ演算子を使えば、複数の式を連続で実行できて、一番最後の方が返されます。
注意: 括弧を使わないと、m_otherClass
に3つの引数を渡しているようになり、コンパイルエラーになります。
// 引数全体を()で囲んでいます
// V V
, m_otherClass((m_oldClass.init(), m_oldClass.setOption(option), m_oldClass))
でもこれは流石に読みづらくてゴリ押し感が凄いため、あんまりお勧めできないです。そもそもコンマ演算子のことが分からない人からすると、非常に分かりづらいコードになってしまうと思います。
まあ、一応こういうことできるっていうことを覚えておくといいです。
ラムダ式関数の即時実行
class NewClass {
public:
NewClass(bool option)
: m_oldClass()
, m_otherClass( [&] {
m_oldClass.init();
m_oldClass.setOption(option);
return m_oldClass;
}())
{}
private:
OldClass m_oldClass;
OtherClass m_otherClass;
};
無名のラムダ関数を作って、その場で即時実行するというやり方ですが、もう少し自然な書き方になっただけで、解決方法としてはコンマ演算子とあんまり変わらないですね。でもこちらの方がまだ読みやすいかもしれないです。
注意: 最後に()
を付けることで実行されるため、付けないとm_otherClass
にラムダ式関数を渡しているようになってしまいます。
やはりその場で何とかしようとするのが良くないかもしれないですね。コンストラクタの初期化子リストでこんなことするアプローチ自体が間違っていると思います。
初期化のクラスを作成
class OldClassInitialization {
public:
OldClassInitialization(bool option)
: m_oldClass()
{
m_oldClass.init();
m_oldClass.setOption(option);
}
const OldClass& GetOldClass() const { return m_oldClass; }
private:
OldClass m_oldClass;
};
class NewClass {
public:
NewClass(bool option)
: m_initializedOldClass(option)
, m_otherClass(m_initializedOldClass.getOldClass())
{}
private:
InitializedOldClass m_initializedOldClass;
OtherClass m_otherClass;
};
こちらの方が一番 C++ らしい方法の気がしますが、わざわざクラスを作るのも面倒ですし、使う度に毎回getOldClass()
を呼ばないといけないのがあんまり好きではないですね。
初期化の関数の使用
class NewClass {
public:
NewClass(bool option)
: m_oldClass(createInitializedOldClass(option))
, m_otherClass(m_oldClass)
{}
private:
static OldClass createInitializedOldClass(bool option) {
OldClass oldClass;
oldClass.init();
oldClass.setOption(option);
return oldClass;
}
OldClass m_oldClass;
OtherClass m_otherClass;
};
こちらは個人的に一番気に入っていて、自分でも使ったことがあります。
注意: createInitializedOldClass
はメンバー関数でなくてもいいです。このクラスだけが使うような初期化や設定だったら、メンバー関数の方がいいです。
もちろんこれら以外の方法もたくさんあると思います。
とにかく意識したいのは「できればクラスのコンストラクタの初期化リストで初期化を完了させよう」という事になると思います。
Discussion