コンストラクタの初期化リストで初期化を完了しよう、とは言うけど・・・

に公開

昨日 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はメンバー関数でなくてもいいです。このクラスだけが使うような初期化や設定だったら、メンバー関数の方がいいです。

もちろんこれら以外の方法もたくさんあると思います。

とにかく意識したいのは「できればクラスのコンストラクタの初期化リストで初期化を完了させよう」という事になると思います。


|cpp記事一覧へのリンク|

Discussion