C++における構造体の初期化

「[迷信] とりあえず memset で初期化」へのアクセスは相変わらず多いのですが、解説があっさりしているために十分意図が伝わっていないことも少なくないようです。これまでも何度か補足的な解説を行ってきたのですが、今回もその一環として、補足解説を行うことにします。

一般的な話をすると、どうしても解説が抽象的になってしまい、その結果またしても十分に意図が伝わらないということが起きそうです。そこで、今回は解説の対象を絞り込み、できるだけ具体的な話をするつもりです。今回対象とするのは「C++の構造体の初期化」です。Cの話ではありません。また、「初期化」と書いていますが、いわゆる初期化子による初期化だけでなく、最初に値を設定する意味上の初期化も対象とします。

利用者定義のコンストラクタを持つ構造体

意外に思われる方も少なくないと思いますので、最初におさらいをしておきます。構造体というのはデータメンバを羅列しただけのもので、コンストラクタを持つはずがないというのは間違いです。
「[迷信] 構造体はクラスではない」

構造体はクラスの一種ですので、当然コンストラクタを定義することができます。この記事のタイトルで、「クラス」ではなく「構造体」としたのは、クラスと書いてしまうと、「そんなのはコンストラクタで行うに決まっている」と脊椎反射的に考えてしまう方が多いと考えたからです。

前置きはこれぐらいにして、利用者定義のコンストラクタを持つ構造体(クラスといってもよいでしょう)の場合、オブジェクトの初期化はコンストラクタで行います。より正確には、特別な事情がないかぎり、コンストラクタで各メンバに代入するのではなく、コンストラクタ初期化子を用いて初期化を行います。

集成体

集成体というのは、次の条件を満たす配列とクラスのことです。

  • 利用者宣言のコンストラクタを持たない
  • 非公開または限定公開の非静的データメンバを持たない
  • 基底クラスを持たない
  • 仮想関数を持たない

上の条件を満たしさえすれば配列や共用体も集成体になりますが、ここでは考えないことにします。集成体の場合は、明示的なコンストラクタがありませんから、型の利用者が個々のデータメンバの値を何らかの方法で設定してやらなければなりません。

集成体の場合、Cの構造体と同じように、{ } で囲んだ初期化子を使って初期化することができます。new で割りつける場合などをのぞけば、{ } で囲んだ初期化子で、個々のメンバを初期化するのが原則です。ちなみに、

struct A
{
  int a;
  int b;
};
A a = { };

のようにすれば、すべてのデータメンバをゼロ初期化または省略時初期化(データメンバが利用者定義のコンストラクタを持つクラスの場合)することができます。Cの場合は、{ } の中に少なくともひとつの初期値を記述しなければなりませんが、C++ではまったく書かなくてもOKです。

new で割り付けた場合や一時オブジェクトの場合には話がかなり煩雑になります。最大限に初期化を頑張るには、( ) を付けることになります。例えば、

A* a = new A();
B(); // Bの一時オブジェクト

のようにです。( ) を付けた場合ですが、実はISO/IEC 14882:1998とISO/IEC 14882:2003では動作が変わります。1998年版では省略時初期化になり、2003年版では値初期化になります(1998年版には値初期化の概念がありません)。JIS X3014では翻訳に誤りがあり、意味不明の状況になっています。
「JIS X3014の値初期化の記述に悩まされる」

実在する処理系への移植性を考えると、次に解説するC互換構造体をのぞけば、( ) を使った初期化は1998年版を想定するしかなく、まともに初期化されるとは考えない方がよいでしょう。特に、データメンバはすべてC互換型だけれども、そのうちの一部がpublicではないとか、利用者定義のデストラクタを持つといった場合には要注意です。

まともに値初期化が働かないのであれば、そして { } で囲んだ初期化子が使えない状況であれば、個々のメンバに代入等の方法を用いて値を設定する以外にはありません。

C互換構造体の初期化

C互換構造体というのは、次の条件を満たす集成体クラスのことです。

  • 非C互換構造体またはその型の配列を非静的データメンバに持たない
  • 非C互換共用体またはその型の配列を非静的データメンバに持たない
  • 参照型を非静的データメンバに持たない
  • 利用者定義のコピー代入演算子を持たない
  • 利用者定義のデストラクタを持たない

C互換構造体も集成体の一種ですので、{ } で囲んだ初期化子を使うのが原則です。しかし、それが使えない状況では、集成体とは異なり () を使った初期化が効果があります。C互換型(C互換構造体もC互換型の一種です)を ( ) で初期化した場合、すべての非静的データメンバがゼロ初期化されます。ポインタ型や浮動小数点型のデータメンバを含んでいても正しく初期化されます。

これは、newで割り付けたオブジェクトや一時オブジェクトを初期化する際に便利です。つまり、

C* p = new D();
D(); // Dの一時オブジェクト

のようにすれば、C互換構造体であるCやDの全データメンバをゼロ初期化することができるのです。こうした仕様がただしく動作しない非標準処理系をのぞき、memset(ZeroMemoryやbzeroも同じ)でゼロクリアする理由はまったくありません。

{ } による初期化にせよ、( ) による初期化にせよ、処理系によっては内部的にmemsetを呼び出していることも少なからずあります。しかし、だからといって、C++のソースコードレベルでmemsetを使うのと同じだということにはなりません。処理系の実装の内情を知っているコンパイラが内部的にmemsetを使うことと、処理系に依存する必要がないコードでmemsetを使うのではまったく異なります。これは、コンパイラがレジスタのゼロクリアにxor命令を出力するからといって、変数のゼロクリアに x ^= x; としないのと同じことです。

この記事のトラックバックURL:

http://www.kijineko.co.jp/trackback/681