gl5_progのメモ

自分のためのメモとかまとめとか

C++で自由な順番でメンバの初期化(相当)を行うアイデア

はじめに

C++で正しくメンバ変数の初期化を行おうとすると、コンストラクタでメンバイニシャライザを使って行わなければなりません。このメンバイニシャライザは、「メンバ変数の宣言順に記述する必要がある」「コンストラクタの最初に記述する必要がある」などの制限があり、このせいでメンバの初期化コードは少し書きにくくなっています。もっと自由に初期化コードを書く方法はないのか考えてみました。

そもそもメンバイニシャライザを使わなければいけない理由

メンバイニシャライザを使わなくても初期化に相当することを記述することは可能です。しかし、なぜメンバイニシャライザを使わなければならないのか。それは処理コストのためです。メンバイニシャライザを使わないで初期化(相当)を行う場合、メンバ変数のデフォルトコンストラクタによる初期化処理が無駄になります。ちょっと制限があっても速いのであればメンバイニシャライザを使わない手はありません。ちなみに、intなどの組み込み型の場合、デフォルトコンストラクタは呼ばれないのでメンバイニシャライザを使用しなくても処理コスト的には問題ないです。

アイデア

メンバイニシャライザ以外で初期化(相当)のコードを書きつつ、無駄な初期化処理を発生させないようにするにはどうすればいいのか。そういうコンストラクタを作ればいいのです。例えばこんな風に:

enum EmptyConstructor { _EmptyConstructor };
class A
{
    int m_Value;
public:
    A() : m_Value( 0 ) {} // デフォルトコンストラクタ ( 0で初期化 )
    A( EmptyConstructor e ){ (void)e; } // 空コンストラクタ( 初期化すらしない )
    void Initialize( int value ){ printf( "freedom!!\n" ); m_Value = value; } // (自由な)初期化関数を別途用意
};

空のコンストラクタ+Initialize関数で無駄な初期化処理が行われず、しかも自由な初期化コードが実現できています。デフォルトコンストラクタも用意しているので、未初期化の心配もありません。

注意点

「空のコンストラクタ+Initialize関数」による自由な初期化は、すべてのメンバ変数、基底クラスにも「空のコンストラクタ+Initialize関数」が備わっている必要があります。備わっていない場合、そこで無駄な処理が発生します。また、Initialize関数内では未初期化なメンバ変数にアクセスしないように気をつける必要があります。

コード例

それぞれのInitialize関数をみてください。自由に初期化コードが書けています。空のコンストラクタは本当に何もしないので2重初期化などの無駄な処理は発生してないはずです。オブジェクトとしては構築済みなのでvirtual関数の呼び出しも問題ないはず。

http://ideone.com/KxgEvP

#include <stdio.h>

enum EmptyConstructor
{
    _EmptyConstructor
};

class Foo
{
    int m_ValueFoo;
public:
    Foo() : m_ValueFoo( 0 ){ printf("Foo()\n"); }
    Foo(EmptyConstructor e){(void)e;}
    void Initialize( int value )
    {
        m_ValueFoo = value;
    }
    int GetValue( void ) const { return m_ValueFoo; }
};

class A
{
    Foo m_ValueA;
public:
    A(){ printf("A()\n"); }
    A(EmptyConstructor e) : m_ValueA(e) {}
    void Initialize( int value )
    {
        m_ValueA.Initialize( value );
    }
    int GetValueA( void ) const { return m_ValueA.GetValue(); }
};

class B : public A
{
    Foo m_ValueB;
public:
    B(){ printf("B()\n"); }
    B(EmptyConstructor e) : A(e), m_ValueB(e) {}
    void Initialize( int value )
    {
        m_ValueB.Initialize( value );
        A::Initialize( value ); // 基底クラスを後から初期化
    }
    int GetValueB( void ) const { return m_ValueB.GetValue(); }
};

class C : public B
{
    Foo m_ValueC_1;
    Foo m_ValueC_2;
public:
    C(){ printf("C()\n"); }
    C(EmptyConstructor e) : B(e), m_ValueC_1(e), m_ValueC_2(e) {}
    void Initialize( int value )
    {
        m_ValueC_2.Initialize( value ); // 2番目のメンバ変数の初期化
        B::Initialize( value );         // 基底クラスの初期化
        m_ValueC_1.Initialize( value ); // 1番目のメンバ変数の初期化
    }
    int GetValueC1( void ) const { return m_ValueC_1.GetValue(); }
    int GetValueC2( void ) const { return m_ValueC_2.GetValue(); }
};

int main( void )
{
    // デフォルトコンストラクタによる初期化
    {
        C c;
        printf( "%d,%d,%d,%d\n", c.GetValueA(), c.GetValueB(), c.GetValueC1(), c.GetValueC2() );
    }
    // 自由な初期化(Initialize関数)
    {
        C c( _EmptyConstructor );
        c.Initialize( 9 );
        printf( "%d,%d,%d,%d\n", c.GetValueA(), c.GetValueB(), c.GetValueC1(), c.GetValueC2() );
    }
    // べつの引数で。
    {
        C c( _EmptyConstructor );
        c.Initialize( 1111 );
        printf( "%d,%d,%d,%d\n", c.GetValueA(), c.GetValueB(), c.GetValueC1(), c.GetValueC2() );
    }
    return 0;
}
Foo()
A()
Foo()
B()
Foo()
Foo()
C()
0,0,0,0
9,9,9,9
1111,1111,1111,1111

最後に

もともと、自作言語のC++コード吐き出し用に考えたことなので、手動でC++コード書く場合には、「すべてのクラスに空のコンストラクタ+Initialize関数が必要」というのは現実的じゃないかもしれません。