🎰

ただしいインスタンス初期化のお作法

に公開

今日は C++ の初期化という非常に初歩的で大事な動作について話そうと思います。

さっそく問題です。

int x;
std::cout << x << std::endl;

xはこの場合初期化されているでしょうか?
答えは No です。初期化されないため、xのためにアロケートされたメモリに既にあるデータが値として解釈されて、予想できない値になります。大体変な大きな数字になると思います。
そのため、以下のように初期化しないといけないです。

int x{}; // 波括弧による一様初期化

それか、以下のように好きな値で初期化しないといけないです。

int x(0);
int x{0};

ここまではみんなご存じだと思います。では、構造体とクラスの場合は、どうでしょうか?

クラスと構造体は以下の点以外は完全に同じなので、これから構造体だけの話にします。

  • クラスの場合はメンバーがデフォルトでprivateになる
  • 構造体の場合はメンバーがデフォルトでpublicになる
struct Foo
{
    int x;
};

Foo foo;
std::cout << foo.x << std::endl;

構造体とクラスの場合は、コンストラクタを定義しなくても、自動的にデフォルトコンストラクタが定義されてコールされるため、そのデフォルトコンストラクタがxを初期化してくれますよね?

残念!C++ は何もしてくれない不便なプログラミング言語です!

デフォルトコンストラクタは自動的に定義されて呼ばれますが、以下のように何もしない定義になるため、xは初期化されないです。

struct Foo
{
    Foo() {};

    int x;
};

C++ のプログラマーはほぼみんな一度はこれにやられたことがあると思います。
C++ は基本余計なことを何もしないというスタンスなので、このようなインスタンスの作り方を使いたい場合は、明示的にメンバー変数を初期化するデフォルトコンストラクタを定義しないといけないです。

struct Foo
{
    Foo()
    : x(0)
    {};

    int x;
};

上記のようなメンバー変数を初期化するデフォルトコンストラクタがある場合は、以下のようにインスタンスを作っても、そのデフォルトコンストラクタが自動的に呼ばれて、メンバー変数が初期化されます。

Foo foo;

C++ の標準ライブラリの構造体とクラスは全部そういうコンストラクタが定義されてあるため、以下のようにインスタンスを作っても、メンバー変数が初期化されます。

std::optional<int> maybe_data; // 初期化の std::nullopt になります。
std::vector<int> data; // 空の std::vector<int> になります。

纏めると、構造体とクラスの場合にメンバー変数が初期化されるかは、デフォルトコンストラクタがちゃんと全メンバー変数を初期化しているかによります。
少しややこしいため、以下のようにどの構造体とクラスでも守備的に初期化するプログラマーもいます。

Foo foo;
memset(&foo, 0, sizeof(foo)); // memset による初期化

でもこれだと、実際にメンバー変数を初期化するデフォルトコンストラクタが定義されている場合は、2回も初期化されるため、パフォーマンス的にあんまり良くないです。
そもそもmemsetでの初期化はメンバー変数の型によって、正しく初期化してくれない危険がありますし、メンバーごとに違う値を指定できないため、お勧めできないです。

個人的には C++11 で追加された波括弧による一様初期化がお勧めです。

struct Foo
{
    int x;
};

Foo foo{}; // 波括弧による一様初期化

これだと、全メンバー変数が初期化されます。
でもこれはコンストラクタを定義していない限りの話です。

以下のように何もしないコンストラクタを定義している場合は、そのコンストラクタが呼ばれるだけです。

struct Foo
{
    Foo() {};

    int x;
};

Foo foo{}; // x は初期化されない

最後にもう1つ初期化の方を紹介します。デフォルトメンバ初期化子と言って、各メンバー変数を定義する時にデフォルト値を定義する方法です。

struct Foo
{
    int x{};
    int y{10};
};

Foo foo; // x はデフォルト値で、y は 10 で初期化される

この方法だと、メンバー変数を明示的に初期化しない限り、そのデフォルト値で初期化されます。
こうすれば、コンストラクタなしでもデフォルト値を設定できます。
後、メンバー変数の定義のところに書くため、分かりやすい一覧ができて、わざわざコンストラクタの実装を見に行かなくてもいいという意味でもお勧めです。
もう1つの利点は、複数のコンストラクタを定義する場合、どのコンストラクタでも同じ値で初期化したいメンバー変数があったら、一ヵ所で一回だけ設定すればいいので、お勧めです。

しかし、これはあくまでも明示的に初期化していない場合のみ適用されるものなので、注意が必要です。

以下のようにコンストラクタで明示的に違う値で初期化する場合はデフォルトメンバ初期化子の内容は適用されないです。

struct Foo
{
    Foo()
    : x(1)
    , y(2)
    {}

    int x{};
    int y{10};
};

Foo foo; // x は 1 で、y は 2 で初期化される

以下のようにC++20から使えるようになった指示付き初期化で明示的に違う値で初期化する場合も、デフォルトメンバ初期化子は適用されないです。

struct Foo
{
    int x{};
    int y{10};
};

Foo foo{ .x = 1, .y = 2 }; // x は 1 で、y は 2 で初期化される

なので、以下の使い分けになると思います。

  • メンバー変数を絶対に特定の値で初期化したい場合は、コンストラクタでの初期化を使う
  • 強制しないが、明示的に初期化されない場合に特定の値で初期化したい場合は、デフォルトメンバ初期化子を使う
  • カスタムのデフォルト値を設定する必要がない場合は、コンストラクタを一切定義しない
    • コンストラクタが定義されていない方が、指示付き初期化や波括弧による一様初期化などの好きな初期化の方法が使える

正しい初期化を心がけましょう。


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

Discussion