パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.7.0.5(5)
初期化とオーバーロード(1)

C++規格で密に相互依存する変数初期化と関数オーバーロードのうち、変数初期化の詳細を確認する。

C++プログラミングで変数初期化はC言語から受け継ぎ、通常は存在さえ意識しない。関数オーバーロードもまた現代的なプログラミング言語に当然で、C++でも黎明より存在する(Bjarne Stroustrup, The Design and Evolution of C++, Reading, Addison-Wesley, 1994; Reading, Addison-Wesley, 1998, pp.78-85)。両者は異なる目的を持つ別個の言語要素であるが、規格(JTC1/SC22/WG21 N4659 11.6、16)において暗黙的変換シーケンス(16.3.3.1)を挟む表裏の関係にある。規格は複雑を極め言語拡張でさらに難解化し、そのスナップショットとしてC++17(N4659)の規格を確認する。本記事は前編として両者の関係を概観してから変数初期化を議論し、初期化とオーバーロード(2)は後編として暗黙的変換シーケンスと関数オーバーロードを議論する。

本記事においてオブジェクトはC++規格定義(4.5/p1)とする。他記事のほとんどで用いる"クラスとインスタンスの総称"ではないことに留意いただきたい。

初期化とオーバーロードの関係

C++規格において、初期化(N4659 11.6)はオーバーロード解決(16.3)を16箇所で参照し、オーバーロード解決は初期化を25箇所で参照する。両者は異なる目的を持つ言語要素でありながら規格は密接に関連し、同一事象をそれぞれの側面から重複定義することも少なくないが、それらは厳密に同じであるはずだ。例えばクラス型の初期化(11.6/p7.1)はコンストラクタのオーバーロード解決(16.3.1.3/p1)を必要とし、オーバーロード解決には実引数から仮引数型への暗黙的変換シーケンスが重要で(16.3.3/p1)、暗黙的変換シーケンスはつまりコピー初期化である(16.3.3.1/p1)。初期化とオーバーロードが暗黙的変換シーケンスを介在する循環定義であるかに見えるが、もちろん文脈が異なりそのような事はありえない。とは言え正確に規格を理解しようとすれば、初期化とオーバーロード解決を行ったり来たりで混乱は免れない。

本記事と後編(初期化とオーバーロード(2))はサイト作成者がこれらを理解しようとした試みを記す。初期化とオーバーロードを暗黙的変換シーケンスを挟む表裏と見なして再整理を試みたが、結果は御覧のとおりで、規格の下手くそな日本語訳以上の何物でもない。本記事と続編はC++17のスナップショットで議論するが、C++20、C++23でさらに変更追加が施されている。メタプログラミングのようなエキゾチックな言語要素を議論しているわけではない。C言語/C++黎明から当たり前に利用するプリミティブな言語要素が、この複雑さ、不安定さはどうしたもんだろうか。

// コンパイラオプション-fno-elide-constructorsでRVO抑止しておく
struct A
{
A(int,int=0) {} // コンストラクタオーバーロード(関数A)
A(double,double=0) {} // コンストラクタオーバーロード(関数B)
A(const A&) {} // コピーコンストラクタ(関数C)
operator int() {return 0;} // intへの変換関数(関数D)
};
int main()
{
// (1)オブジェクト初期化でコンストラクタあるいは変換関数をオーバーロード解決
// (2)そのオーバーロード解決には暗黙的変換シーケンスを考慮
// (3)暗黙的変換シーケンスは実引数で仮引数型をコピー初期化
// (4)ユーザー定義変換は1レベルのみ許されるので(1)に再帰しない
//
// C++14以前はRVOで関数Cをコールしないかもしれないが、関数C=deleteできない
// C++17以降はコピー省略保証で関数Cはコールされず、関数C=deleteでも良い
// コンストラクタを実引数2個でネストする場合
// C++14以前のコール順(RVO抑止時) C++17以降のコール順
A a1=A(0,0); // 関数A,関数C 関数A
A a2=A(A(0,0),0); // 関数A,関数D,関数A,関数C 関数A,関数D,関数A
A a3=A(A(A(0,0),0),0); // 関数A,関数D,関数A,関数D,関数A,関数C 関数A,関数D,関数A,関数D,関数A
// コンストラクタを実引数1個でネストする場合
// C++14以前のコール順(RVO抑止時) C++17以降のコール順
A a4=A(0); // 関数A,関数C 関数A
A a5=A(A(0)); // 関数A,関数C,関数C 関数A
A a6=A(A(A(0))); // 関数A,関数C,関数C,関数C 関数A
return 0;
}

初期化

変数はオブジェクトか参照のどちらかを指す(N4659 6/p6)。値変数はオブジェクトを指す変数、参照変数は参照を指す変数として、初期化以降は値変数とそれを参照する参照変数はC++標準のis_lvalue_reference/is_rvalue_referenceメタ関数(23.15.4.1)を例外として区別できない。

初期化する変数xの型をTとする。aは初期化リスト(initializer-list)で、すなわち1個以上のコンマで区切られた初期化節(initializer-clause)によるリストとする(11.6/p1)。初期化節とは代入式であるか、あるいは{}または{a}であり、後者は式ではない。初期化子(initializer)は初期化リストを与える構文、すなわち()、{}、=a、(a)、{a}を総称する。=aのみaは初期化節1個に制限される。{}または{a}は初期化節である場合と初期化子である場合がある。T x();は関数xの宣言となるため(C++最厄構文)(Scott Meyers, Effective STL, Boston, Addison-Wesley, 2001, pp.33-35)、初期化子()は変数初期化に用いない。

名称 N4659 11.6 用例 値変数 参照変数
ゼロ初期化 zero-initialization (p6) - ✔(NOP)
デフォルト初期化 default-initialization (p7) T x;
値初期化 value-initialization (p8) T x{};
コピー初期化 copy-initialization (p15) T x=a;
直接初期化 direct-initialization (p16) T x(a); T x{a};

初期化は変数初期化の他でも行われ後にまとめる。例えばオブジェクトであれば、関数形式の明示的変換式(8.2.3)はT()、T{}、T(a)、T{a}でTを構築して初期化するT型の純右辺値式であり、new式(8.3.4)はnew T、new T()、new T{}、new T(a)、new T{a}でTを構築して初期化するT*型の純右辺値式である。

(...)は()と(a)を総称し、{...}は{}と{a}を総称するものとする。初期化子を{...}とする値初期化/直接初期化を直接リスト初期化と呼び、aを{...}とするコピー初期化(すなわち初期化子が={...})をコピーリスト初期化と呼び、合わせてリスト初期化と呼ぶ(11.6.4)。コピー初期化はオブジェクトのコピーを伴うとは限らない。

ゼロ初期化

ゼロ初期化は静的ストレージのオブジェクト構築が暗黙に行い(11.6/p10)、値初期化のある条件においても暗黙に行う。ゼロ初期化に続き他の初期化が行われる場合がある。

Tがスカラー型(数値型、列挙型、ポインタ型など)(6.9/p9)であれば0に初期化する。クラス型であれば非スタティックなメンバ変数と基底クラスをゼロ初期化してゼロパディング(データアライメント余白を0で初期化)する。配列型は全ての要素をゼロ初期化する。参照型は何もしない(NOP)。

デフォルト初期化と値初期化

参照型はデフォルト初期化も値初期化もできない(11.6/p9)。

デフォルト初期化は、Tがクラス型であればT()でコールできるコンストラクタの中からオーバーロード解決(16.3)で最優関数をコールする。オーバーロード解決の最優関数は後編(初期化とオーバーロード(2))で詳説する。配列型は全ての要素をデフォルト初期化する。それ以外は何もせずデータは不定値(indeterminate value)となる。

値初期化は、Tがクラス型でデフォルトコンストラクタをユーザー定義あるいは=deleteすればデフォルト初期化する。クラス型でそれ以外の場合はゼロ初期化し、コンパイラ生成のデフォルトコンストラクタが自明(trivial)(ユーザー定義でなく、仮想メンバ関数と仮想基底クラスを持たず、サブオブジェクト全てが自明なデフォルトコンストラクタを持つ、など)(15.1/p6)でなければそれをコールする。配列型は全ての要素を値初期化する。それ以外はゼロ初期化する。

// ユーザー定義デフォルトコンストラクタがメンバ変数をデフォルト初期化するクラス
struct A
{
int i_;
A() {}
};
int main()
{
static int i1; // 静的ストレージデフォルト初期化 i1 = 0
static int i2{}; // 静的ストレージ値初期化 i2 = 0
int i3; // 自動ストレージデフォルト初期化 i3 = 不定値
int i4{}; // 自動ストレージ値初期化 i4 = 0
int* pi5=new int; // 動的ストレージデフォルト初期化 *pi5 = 不定値
int* pi6=new int{}; // 動的ストレージ値初期化 *pi6 = 0
// 値初期化がゼロ初期化にならない場合
A a1{}; // 自動ストレージ値初期化 a1.i_= 不定値
return 0;
}

コピー初期化と直接初期化

初期化子を持つ初期化は、コピー初期化と(値初期化を含む)直接初期化に分類される。変数初期化では初期化子が=aであるか、{...}あるいは(a)であるかで容易に区別できる。初期化子を持たなければデフォルト初期化になる。以下に変数初期化以外の場合をまとめるが、どちらであるかは文脈から判断する。コピー初期化は変数初期化以外の文脈で形式=aとならないが、=aで代表して記述する。コピー初期化はムーブ(15.8)かもしれない。

  • 関数の実引数渡し、関数の戻り値、例外送出(18.1)、例外処理(18.3)、および集約のメンバ初期化(11.6.1)は、コピー初期化である。
  • new式、static_cast式(8.2.9)、明示的変換式(8.2.3)、およびサブオブジェクト(基底クラスやメンバ)初期化(15.6.2)は、直接初期化である。このうちnew式(new T)(8.3.4/p18)とサブオブジェクト(メンバ初期化リスト(mem-initializer-list)から省略する)(15.6.2/p9)だけがデフォルト初期化できる。

集約(aggregate)は配列または集約クラス型で、集約クラス型はユーザー定義、explicit、あるいは継承したコンストラクタを持たず、非スタティックなprivate/protectedメンバを持たず、仮想メンバ関数を持たず、仮想あるいはprivate/protectedな基底クラスを持たない(11.6.1/p1)。配列または集約クラス型は後述のリスト初期化で初期化され(p3)、集約クラス型はaが単独式の場合(初期化式)に限り他の形式でも良い(p4)。文字型配列は文字列リテラルでも初期化できる(11.6.2/p1)。

初期化子の解釈を示す(11.6/p17)。Sはaが単独式の場合(初期化式)の型として、それ以外の場合は定義しない。初期化節が2個以上なら、Tは必ずクラス型で直接初期化する。なおユーザー定義変換シーケンス、ユーザー定義変換、変換コンストラクタ、変換関数は、後述の暗黙的変換シーケンスで定義する。

  • 初期化子が{...}、={...}の場合はリスト初期化として後述する。
  • Tが参照型の場合は後述する。
  • Tが文字型配列で初期化子(初期化リスト単一要素の誤りではないか?)が文字列リテラルの場合は文字列リテラルで初期化する。
  • 初期化子が()の場合は値初期化する。
  • さもなくば、もしTが配列であれば不適格(ill-formed)とする。
  • Tが(cv修飾されているかもしれない)クラス型の場合、
    • 初期化式が純右辺値式でSからcv修飾を除いた型がTと同じ場合、初期化子は対象オブジェクトを初期化する。(※1)
    • さもなくば、直接初期化の場合、あるいはSからcv修飾を除いた型がTと同じか派生型であるコピー初期化の場合、適用可能なコンストラクタを列挙してオーバーロード解決(16.3)で最優関数を選択する。コンストラクタが適用できないか曖昧の場合は不適格とする。(※2)
    • さもなくば(つまりその他のコピー初期化の場合)、SからTあるいは派生型へ変換できるユーザー定義変換シーケンスを列挙して(16.3.1.4)、オーバーロード解決で最優関数を選択する。変換できないか曖昧の場合は不適格とする。初期化式を実引数として選択した関数をコールする。選択した関数がコンストラクタであればコールはTからcv修飾を除いた純右辺値式で、結果オブジェクトは変換コンストラクタで初期化される。コールは値カテゴリに従い(※1)あるいは(※2)でコピー初期化の対象オブジェクトを直接初期化する。(※3)
  • さもなくば(つまりTが非クラス型の場合)、Sが(cv修飾されているかもしれない)クラス型の場合、適用可能な変換関数を列挙して(16.3.1.5)、オーバーロード解決で最優関数を選択してコールする。変換できないか曖昧である場合は不適格とする。
  • さもなくば(つまりTが非クラス型でSが非クラス型あるいは非クラス型に変換された場合)、対象オブジェクトの初期値は(変換されたかもしれない)初期化式の値である。標準変換シーケンス(7)は必要ならcv修飾されていないTへ変換する。ユーザー定義変換は用いられない。変換できなければ不適格とする。

Tがクラス型でなければコピー初期化と直接初期化に差異は無い。クラス型の場合、コピー初期化はxに値をコピーするというセマンティクスを(例外を除き)持つ。コンストラクタに右辺値式を与えてムーブコンストラクタが得られればそれを選択し、そうでなければコピーコンストラクタを選択する。以降、コピーコンストラクタを選択するとして説明する。SがTあるいは派生型であれば、(※2)はコピーコンストラクタにaを与える。SがTあるいは派生型へユーザー定義変換できるならば、(※3)はaからTへの変換式を選択して、(※2)の直接初期化がコピーコンストラクタへ変換式を与える。コンパイラの戻り値最適化(return value optimization, RVO)はコピーコンストラクタのコールを省略する(copy elision、コピー省略)かもしれないが(15.8.3/p1)、(※2)によりセマンティクスは維持する。つまりコピーコンストラクタが得られないコピー初期化は不適格である。

C++17は(※1)を追加してこのセマンティクスに例外を設けた。(※1)は(※2)に優先し、初期化式が純右辺値式でSがTと同じコピー初期化はこのセマンティクスを持たず、コピーコンストラクタを不要としてコピー省略を保証(guaranteed copy elision)した。なお直接初期化でも同じで、初期化式が同様の時にオーバーロード選択するはずのコピーコンストラクタは無くても良い。

// コピーコンストラクタを持たないクラス、intからの変換コンストラクタを持つ
struct A
{
A() {}
A(const A&)=delete;
A(int) {}
};
// const A&への変換関数を持つクラス
struct B
{
A a_;
operator const A&() {return a_;}
};
int main()
{
// デフォルト初期化
A a1;
// コピー初期化
//A a2=a1; // A型左辺値式による初期化、C++17以降、C++14以前共に(※2)でエラー
A a3=A{}; // A型純右辺値式による初期化、C++17以降は(※1)でOK、C++14以前は(※2)でエラー
A a4=0; // (※3)でint型純右辺値式をA型純右辺値式に変換して初期化、C++17以降は(※1)でOK、C++14以前は(※2)でエラー
//A a5=B{}; // (※3)でB型純右辺値式をA型左辺値式に変換して初期化、C++17以降、C++14以前共に(※2)でエラー
// 直接初期化
//A a6(a1); // A型左辺値式による初期化、C++17以降、C++14以前共に(※2)でエラー
A a7(A{}); // A型純右辺値式による初期化、C++17以降は(※1)でOK、C++14以前は(※2)でエラー
A a8(0); // int型純右辺値式による初期化、C++17以降、C++14以前共にOK
//A a9(B{});// B型純右辺値式をA型左辺値式に変換して初期化、C++17以降、C++14以前共に(※2)でエラー
return 0;
}

古い教科書は直接初期化を、値コピーのセマンティクスを持つ(コピーコンストラクタを必要とする)コピー初期化に優先せよとした(Herb Sutter, Exceptional C++, Boston, Addison-Wesley, 2000; Boston, Addison-Wesley, 2002, pp.224-225)。しかしC++11が追加したautoによる変数定義はコピー初期化のみが可能(10.1.7.4/p4)でRVOに期待せざるを得ない。C++17が追加したコピー省略の保証は、その方向性を強化するものと見なせる。

参照型の初期化

参照はオブジェクトまたは関数にバインドされる(11.3.2/p5)。参照は初期化しなければならず(11.6.3/p1)、その後に参照先を変更できない(p2)。参照宣言において、初期化子は関数仮引数宣言、関数戻り値、クラス定義内のメンバ変数宣言、あるいはextern修飾されている場合のみ省略できる(11.3.2/p5、11.6.3/p3)。このリストが例外ハンドラの宣言(exception-declaration)(18.3/p1)を含まないのはなぜだろうか。

cv1 T1とcv2 T2でT1とT2が同じ型、あるいはT1がT2の基底クラスである場合、前者は後者の"参照関連"(reference-related)と言い、さらにcv1≧cv2の時は"参照互換"(reference-compatible)と言う(11.6.3/p4)。cv1とcv2はそれぞれcv修飾子(無し、const、volatile、const volatileのいずれか)で、(無し)<(constあるいはvolatile)<(const volatile)とする(6.9.3/p1,p4)。T1とT2が例外指定以外は同じ関数型の場合、後者がnoexcept(18.4/p1)の場合も"参照互換"である。関数型はcv修飾子を無視する(11.3.5/p7)。

参照の初期化リストaは単独式(初期化式)で、必ず初期化式が初期化する。初期化する変数の型Tをcv1 T1&(左辺値参照)あるいはcv1 T1&&(右辺値参照)として、初期化式の型Sをcv2 T2とする。cv1 T1への参照はcv2 T2型の式で以下に初期化される。一時実体化変換は後に詳述する。

  • 左辺値参照で初期化式が、
    • 左辺値式でcv1 T1がcv2 T2に参照互換である場合、あるいは
    • クラス型でT1がT2に参照関連せず、cv3 T3型の左辺値式に変換関数で変換(16.3.1.6)できてcv1 T1がcv3 T3に参照互換である場合、
    参照はそれぞれの左辺値式あるいは適切な基底クラスサブオブジェクトにバインドする(※1)。
  • さもなくば、参照はvolatileでないconst型への左辺値参照であるか、右辺値参照である。
    • もし初期化式が、
      • 右辺値式あるいは関数型左辺値式でcv1 T1がcv2 T2に参照互換である場合、あるいは
      • クラス型でT1がT2に参照関連せず、cv3 T3型の右辺値式あるいは関数型左辺値式に変換関数で変換(16.3.1.6)できてcv1 T1がcv3 T3に参照互換である場合、
      それぞれを変換された初期化式と呼ぶ。変換された初期化式が純右辺値式の場合、その型であるT4(T2またはT3)はcv1 T4に修正されて一時実体化変換される。全ての場合で、参照は結果の汎左辺値式あるいは適切な基底クラスサブオブジェクトにバインドする(※2)。
    • さもなくば、
      • もしT1あるいはT2がクラス型でT1がT2に参照関連しない場合、ユーザー定義変換によるcv1 T1型オブジェクトのコピー初期化のルール(11.6、16.3.1.4、16.3.1.5)を用いるユーザー定義変換を試み、対応する参照でないコピー初期化が不適格であれば不適格とする。変換結果は参照を直接初期化する。この直接初期化にユーザー定義変換は考慮しない。
      • さもなくば、初期化式はcv1 T1型の純右辺値式に暗黙変換される。これを一時実体化変換して、参照はその結果にバインドする(※3)。
      T1がT2に参照関連する場合、
      • cv1≧cv2であらねばならず、かつ
      • 右辺値参照であれば初期化式は左辺値式であってはならない(※4)。

(※1)(※2)は直接バインド(bind directlyあるいはdirect binding)、(※3)は間接バインド(indirect binding)と言う。直接バインドがユーザー定義変換を伴う場合は変換関数に限定されるが、ここでは明示せずオーバーロード解決(16.3.1.6)が明示する。非constな左辺値参照へのユーザー定義変換は変換関数のみであるが(なぜなら非constな左辺値参照は右辺値式にバインドできず、変換コンストラクタは純右辺値式である)、そうでない参照へのユーザー定義変換は変換コンストラクタも可能で、そういった場合は間接バインドとする。つまり(※3)第1項のT1あるいはT2がクラス型の場合のユーザー定義変換は変換コンストラクタであり、結果は純右辺値式でこれで参照を直接初期化すれば再帰ルール(※2)第1項が一時実体化変換する。(※4)は間接バインドがT1がT2に参照関連する場合を除くための遠回しの文言と思われる。

覚え書き
"変換結果は参照を直接初期化する"の規格原文(11.6.3/p5.2.2.1)は"The result of the call to the conversion function, ..., is then used to direct-initialize the reference."であり、変換関数のみの変換であるかのように書かれているが、これは誤りで変換コンストラクタを含むユーザー定義変換を指す。この誤りはC++23ドラフト(N4950 9.4.4/p5.4.1)でも解消されていない。

これらを不正確ながら理解しやすく言い換えれば以下となる。

  • 左辺値参照は、
    • 初期化式が、cv1 T1が参照互換するcv2 T2型の左辺値式であるか、そういった左辺値式に変換関数で変換できれば、それに直接バインドする(※1)。
  • const型の左辺値参照、あるいは右辺値参照は、
    • 初期化式が、cv1 T1が参照互換するcv2 T2型の右辺値式であるか、そういった右辺値式に変換関数で変換できれば、それに直接バインドする(※2)。
    • さもなくば、T1がT2に参照関連する場合を除き、初期化式がcv1 T1の純右辺値式に変換できれば、それに間接バインドする(※3)。
#include <utility>
struct B{};
struct C{};
struct D{};
using F=void(int); // Fはvoid(int)関数型、operator T()のTにvoid(int)は不可
void f(int) {} // F関数型の関数定義
struct A:B // BはAの基底
{
C c_;
operator C&() {return c_;} // C&への変換関数、左辺値式
operator D() const {return D{};}// Dへの変換関数、右辺値式
operator int() const {return 0;}// intへの変換関数、右辺値式
operator F&() const {return f;} // F&への変換関数、関数型左辺値式
};
struct E
{
E(const A&) {} // Aからの変換コンストラクタ
E(int) {} // intからの変換コンストラクタ
};
int main()
{
A a01;
// 全てコピー初期化(初期化子=a)だが、直接初期化((a))、リスト初期化(={a}あるいは{a})も変わらない
// 参照互換型(同じクラス型)がバインド(直接バインド)
A& a02=a01; // 非const左辺値参照を左辺値式へバインド(※1)
//A& a03=A{}; // 非const左辺値参照を右辺値式(純右辺値式)へバインド、エラー
//A& a04=std::move(a01); // 非const左辺値参照を右辺値式(期限切れ値式)へバインド、エラー
const A& a05=a01; // const左辺値参照を左辺値式へバインド(※1)
const A& a06=A{}; // const左辺値参照を右辺値式(純右辺値式)へバインド(※2)
const A& a07=std::move(a01); // const左辺値参照を右辺値式(期限切れ値式)へバインド(※2)
//A&& a08=a01; // 右辺値参照を左辺値式へバインド、エラー
A&& a09=A{}; // 右辺値参照を右辺値式(純右辺値式)へバインド(※2)
A&& a10=std::move(a01); // 右辺値参照を右辺値式(期限切れ値式)へバインド(※2)
// 参照互換型(基底クラス型)がバインド(直接バインド)
B& b1=a01; // 基底B型の非const左辺値参照を左辺値式へバインド(※1)
B&& b2=A{}; // 基底型の右辺値参照を派生型の右辺値式(純右辺値式)へバインド(※2)
// 変換関数で変換して、その参照互換型がバインド(直接バインド)
C& c1=a01; // 変換関数がC型の左辺値式に変換して、左辺値参照がそれにバインド(※1)
D&& d2=A{}; // 変換関数がD型の純右辺値式に変換して、右辺値参照がそれにバインド(※2)
int&& i1=A{}; // 変換関数がint型の純右辺値式に変換して、右辺値参照がそれにバインド(※2)
F& f1=A{}; // 変換関数がF型の関数型左辺値式に変換して、関数型左辺値参照がそれにバインド(※2)
// 変換コンストラクタまたは標準変換で変換してバインド(間接バインド)
int&& i2=double{0}; // 標準変換がdouble型からint型に変換して、右辺値参照がそれにバインド(※3)
E&& e1=A{}; // 変換コンストラクタでA型をE型の純右辺値式に変換して、右辺値参照がそれにバインド(※3)
E&& e2=0; // 変換コンストラクタでint型をE型の純右辺値式に変換して、右辺値参照がそれにバインド(※3)
return 0;
}

直接バインド(※2)は間接バインド(※3)に優先する。これからサイト作成者は、クラス型参照の初期化がユーザー定義変換を伴う場合は変換関数が変換コンストラクタに優先すると解釈するが、mingw-w64はそのように振る舞わない。解釈とmingw-w64のどちらが間違っているのだろうか。

// ユーザー定義変換を伴うクラス型初期化(N4659 11.6/p17.6.3)とクラス型参照初期化(11.6.3/p5)を比較する。
// 前者はクラス型ユーザー定義変換(16.3.1.4)で変換関数と変換コンストラクタを合わせてオーバーロード解決する。
// 後者は直接バインドユーザー定義変換(16.3.1.6)による直接バインドを試み、それが得られない場合に
// クラス型ユーザー定義変換で間接バインドを試みる。直接バインドユーザー定義変換は変換関数による
// オーバーロード解決なので、間接バインドは変換コンストラクタによるオーバーロード解決のみとなり、
// つまりクラス型参照初期化は常に変換関数が変換コンストラクタに優先すると解釈できる。
// サンプルコードは両者の違いを例示する試みであったがmingw-w64で差は現れず、両者ともに変換関数と
// 変換コンストラクタを同時にオーバーロード解決する。解釈とmingw-w64のどちらが間違っているのだろうか?
struct Base
{
Base() {}
Base(struct B);
Base(struct C);
};
struct Derived:Base
{
Derived() {}
Derived(struct A);
};
// Baseクラス型の初期化において、各クラスからの変換関数と変換コンストラクタのオーバーロード選択の優劣を
// "派生から基底変換"(derived-to-base conversion)(16.3.3.1/p6)の有無で設定
struct A // 変換関数が優れる
{
operator Base() {return Base{};}
};
Derived::Derived(A) {}
struct B // 変換コンストラクタが優れる
{
operator Derived() {return Derived{};}
};
Base::Base(B) {}
struct C // 変換関数と変換コンストラクタの優劣無し(曖昧)
{
operator Base() {return Base{};}
};
Base::Base(C) {}
int main()
{
// クラス型初期化
Base val_a=A{}; // A::operator Base()(変換関数)をコール
Base val_b=B{}; // Base::Base(B)(変換コンストラクタ)をコール
//Base val_c=C{}; // エラー、C::operator Base()とBase::Base(C)が曖昧
// クラス型参照初期化
Base&& ref_a=A{}; // A::operator Base()をコール
Base&& ref_b=B{}; // Base::Base(B)をコール、しかしB::operator Derived()をコールするべき?
//Base&& ref_c=C{}; // エラー、しかしC::operator Base()をコールするべき?
return 0;
}

リスト初期化

リスト初期化(list-initialization)はC++11で追加され、全ての型の初期化を{...}で統一する。最古のC言語から配列と構造体は初期化子={...}で初期化できたが、これを一般化した上で初期化子{...}を追加した。C++標準ライブラリのコンテナ型も配列のように要素のリストで初期化できて、その目的でstd::initializer_list(21.9)という特殊なプロクシクラステンプレートが追加された。

初期化子{...}による初期化は(値初期化を含む)直接初期化の文脈に現れて直接リスト初期化(direct-list-initialization)と呼び、={...}はコピー初期化の文脈に現れてコピーリスト初期化(copy-list-initialiation)と呼ぶ。それぞれ直接初期化とコピー初期化の部分集合であるものの、それぞれの補集合(直接リスト初期化でない直接初期化、あるいはコピーリスト初期化でないコピー初期化)とは別に定義される。

リスト初期化はオブジェクトまたは参照において以下とする(11.6.4/p3)。コピー初期化/直接初期化は、コピーリスト初期化/直接リスト初期化に従うものとする。

  • Tが集約クラス型で、aが単一要素のcv U型でUがTと同じか派生型の場合、その要素でコピー初期化/直接初期化される。
  • さもなくば、Tが文字型配列でaが単一要素で文字リテラルの場合、それで初期化される。
  • さもなくば、Tが集約の場合、集約のルールで初期化される。
  • さもなくば、初期化子が{}あるいは={}でTがデフォルトコンストラクタを持つクラス型の場合、値初期化される。
  • さもなくば、Tがstd::initializer_list<E>クラステンプレート特殊化である場合、
    • 以下に示すE型の配列の純右辺値式を参照するstd::initializer_list<E>型のオブジェクトを生成する。初期化子{a}あるいは={a}のaと同じ要素数で、各要素はaの各要素でコピー初期化される。要素の一つでも縮小変換(narrowing conversion)(11.6.4/p7)されれば不適格とする(p5)。初期化子{}あるいは={}は既述のルールが値初期化して、デフォルトコンストラクタ(21.9.2)が空のオブジェクトを生成する。
    • 配列は一時変数の寿命を持つが(15.2)、オブジェクトは一時変数への参照としてその寿命を延長する(11.6.4/p6)。
  • さもなくば、Tがクラス型の場合、適用可能なコンストラクタを列挙してオーバーロード解決で最優関数を選択する。実引数が一つでも縮小変換されれば不適格とする。
  • さもなくば、Tが定まった基底型を持つ列挙型で(10.2)、aが単一要素vで直接リスト初期化の場合、T(v)で初期化される。vが縮小変換されれば不適格とする。
  • さもなくば、aが単一要素のE型で、Tが参照型でない、あるいはTが参照型でEの参照関連である場合、オブジェクトあるいは参照はその要素でコピー初期化/直接初期化される。
  • さもなくば、Tが参照型の場合、Tで参照できる型の純右辺値式を生成する。純右辺値式はコピー初期化/直接初期化で結果オブジェクトを初期化しで、参照型を直接初期化する。Tが非constな左辺値参照型の場合は初期化できない。
  • さもなくば、初期化子が{}あるいは={}の場合、値初期化する。
  • さもなくば、不適格とする。

std::initializer_list<E>型あるいはその参照型を最初の仮引数として、他の仮引数が無いか全てデフォルト値を持つコンストラクタを初期化リストコンストラクタ(initializer-list constructor)と呼ぶ(11.6.4/p2)。初期化子{a}あるいは={a}の時に最優先でオーバーロード選択されて(16.3.1.7/p1)初期化リストを仮引数に受ける。C++標準ライブラリのコンテナ型は初期化リストコンストラクタを持ち、配列のように要素のリストで初期化できる。

#include <vector>
struct A // 集約クラス型
{
int i1_;
struct {int i2_;int i3_;} s_;
};
struct B // 集約でないクラス型
{
B(int i1,int i2,int i3) {}
};
struct C // 変換関数を持つクラス型
{
operator B() const {return {4,5,6};} // B型への変換関数
operator int() const {return 0;} // int型への変換関数
};
int main()
{
C c1{};
// コピーリスト初期化
A x1={1,{2,3}}; // {1,{2,3}}
A x2={x1}; // {1,{2,3}}
char x3[]={"string literal"}; // "string literal"
int x4[]={1,2,3,4}; // {1,2,3,4}
B x5={1,2,3}; // B(1,2,3)
B x6={c1}; // B(4,5,6)
int x7={c1}; // 0
int x8={1}; // 1
std::vector<int> x9={1,2,3,4}; // {1,2,3,4}
// 直接リスト初期化
A y1{1,{2,3}}; // {1,{2,3}}
A y2{y1}; // {1,{2,3}}
char y3[]{"string literal"}; // "string literal"
int y4[]{1,2,3,4}; // {1,2,3,4}
B y5{1,2,3}; // B(1,2,3)
B y6{c1}; // B(4,5,6)
int y7{c1}; // 0
int y8{1}; // 1
std::vector<int> y9{1,2,3,4}; // {1,2,3,4}
// {...}が(initializer_list<E>)と(size_t,E)にマッチすれば前者をオーバーロード選択する
std::vector<int> z1{5,6}; // {5,6}, vector(initializer_list<E>)を選択
std::vector<int> z2(5,6); // {6,6,6,6,6}, vector(size_t,E)を選択
std::vector<const char*> z3{5,"six"}; // {"six","six","six","six","six"}, vector(size_t,E)を選択
std::vector<const char*> z4(5,"six"); // {"six","six","six","six","six"}, vector(size_t,E)を選択
return 0;
}

Tが集約でないクラス型の場合も{...}と={...}は同定義で、つまりその場合に={...}は値コピーのセマンティクスを持たない。

// コピーコンストラクタを持たないクラス、intからの変換コンストラクタを持つ
struct A
{
A(int) {}
A(const A&)=delete;
};
// A型仮引数aを持つ関数
void f(A a) {}
int main()
{
// コピーリスト初期化
A a1={0}; // int型純右辺値式による初期化、C++17以降、C++14以前共にOK
// 直接リスト初期化
A a2{0}; // int型純右辺値式による初期化、C++17以降、C++14以前共にOK
// 仮引数aのコピー初期化
f(0); // A a=0;と同等、コピーリスト初期化でない、C++17以降はOK、C++14以前はエラー
f({0}); // A a={0};と同等、コピーリスト初期化、C++17以降、C++14以前共にOK
return 0;
}

初期化に現れる{...}と(...)を比較する。{...}は"波括弧による初期化リスト"(braced-init-list)で、(...)は関数コールの一般形式に後置する"丸括弧に囲まれた式リスト"((expression-list))で、式リスト(expression-list)と初期化リスト(initializer-list)は等しい。{...}と(...)は(C++最厄構文を除いて)共に直接初期化の初期化子として使えるが、{...}だけが初期化節としてコピーリスト初期化できる。初期化節に形式()は許されず、形式(a)はコンマ演算(8.19)を囲む括弧式(8.1.3)でコピーリスト初期化ではない。さらに{a}は(a)より以下2点で厳密である。

  • {a}は式の縮小変換を許さない。(a)は標準変換(7.8、7.9、7.10)として許す。
  • {a}は仮引数の初期化順が初期化リスト先頭からと定義されている(11.6.4)。(a)のそれは未決定(8.2.2/p5)。
#include <iostream>
struct A
{
A(int i1=0,int i2=0,int i3=0)
{
std::cout<<"i1="<<i1<<",i2="<<i2<<",i3="<<i3<<std::endl;
}
};
struct B
{
int val_;
operator int() {return ++val_;}
};
int main()
{
A a01={}; // i1=0,i2=0,i3=0 コピーリスト初期化(デフォルト)
// A a02=(); // エラー 構文が不正
A a03={1,2,3}; // i1=1,i2=2,i3=3 コピーリスト初期化
A a04=(1,2,3); // i1=3,i2=0,i3=0 括弧式の結果で初期化(=3)、コピーリスト初期化でない
A a05{1,2,3}; // i1=1,i2=2,i3=3 直接リスト初期化
A a06(1,2,3); // i1=1,i2=2,i3=3 直接リスト初期化ではない直接初期化
B b1{},b2{};
A a07{b1,b1,b1}; // i1=1,i2=2,i3=3 {...}の仮引数初期化順は規格定義
A a08(b2,b2,b2); // i1=3,i2=2,i3=1 (...)の仮引数初期化順は実装依存
// A a09{1,2,3.0}; // エラー {...}は縮小変換を許さない
A a10(1,2,3.0); // i1=2,i2=2,i3=3 (...)は縮小変換を許す
return 0;
}

一時実体化変換

一時オブジェクト(temporary object)は一時的に生成される名前を持たないオブジェクトとされる。C++14以前は、参照を純右辺値式にバインドする場合、純右辺値式の返値、純右辺値式への変換、例外送出、例外処理、およびいくつかの初期化に現れるとした(N3797 12.2/p1)。C++17以降はコピー省略保証の導入で、純右辺値式から汎左辺値式への変換、自明コピー可能(tribially-copyable)なオブジェクトの関数との受け渡し、例外送出に整理された(N4659 15.2/p1)。自明コピー可能なオブジェクトはレジスタ処理を想定する単純なクラスオブジェクトでコピー省略の例外とされる(15.2/p3)。純右辺値式から汎左辺値式へ変換する事を一時実体化変換(temporary materialization conversion)と呼び、純右辺値が初期化した結果オブジェクトを期限切れ値式として得る(7.4/p1)。一時実体化変換は標準変換シーケンスを構成せず暗黙的変換シーケンスの優劣に関係しない。

一時実体化変換は不要な一時コピーを避けるため可能な限り遅延され、以下に現れる(15.2/p2)。

  • 参照を純右辺値式にバインドする
  • クラス型の純右辺値式でメンバアクセスする
  • 配列型の純右辺値式で配列からポインタへ変換されるか、要素にアクセスする
  • {...}でstd::initializer_list<T>型のオブジェクトを初期化する
  • sizeofやtypeidでの評価されない被演算子
  • 純右辺値式が破棄値式(discarded-value-expression)(8/p12)である

一時オブジェクトはそれを生成した完全式(full-expression)(4.6/p12)の完了で解体される(15.2/p4)が、三つの例外となる文脈がある。第一の文脈は初期化子を持たない配列の要素初期化がデフォルトコンストラクタをコールする場合で、第二の文脈は配列全体をコピーするときの要素コピーがコピーコンストラクタをコールする場合で、これらのコンストラクタが一つ以上の実引数デフォルト値を持てばデフォルト実引数として構築される一時オブジェクトは次の要素が構築される前に解体される(p5)。第三の文脈は参照へのバインドで、参照にバインドされた一時オブジェクトまたは参照にバインドされたサブオブジェクトを持つ一時オブジェクトは参照の寿命まで延命するが、以下を例外とする(p6)。

  • 関数コールの文脈で一時オブジェクトが参照仮引数にバインドされる場合、一時オブジェクトは関数コールを含む完全式の完了で解体される。
  • 関数返値にバインドされる一時オブジェクトは関数リターンで解体されて返値の寿命まで延命されない。
  • new初期化子の参照にバインドされる一時オブジェクトはnew初期化子を含む完全式の完了で解体される。
    struct S { int mi; const std::pair<int,int>& mp; };
    S a { 1, {2,3} };
    S* p = new S{ 1, {2,3} }; // Creates dangling reference

参照にバインドされない一時オブジェクトは、同一完全式で先に構築された一時オブジェクトより早く解体される(p7)。二つ以上の一時オブジェクトがそれぞれ寿命の等しい参照にバインドされる場合、構築の逆順で解体される。参照にバインドされた一時オブジェクトの解体は、参照の記憶域領域(storage duration)(6.7/p1)に依存する。参照メンバを一時オブジェクトで初期化することは不適格とする(15.6.2/p8)。

struct A {
A() : v(42) { } // error
const int& v;
};

std::initializer_listクラステンプレート

C++11の導入したstd::initializer_list<E>はconst E型配列へのアクセスを提供する。

クラステンプレート特殊化をinitializer_listクラス、それから構築されたインスタンスをinitializer_listインスタンスと参照する。{...}によるinitializer_listインスタンス(std::initializer_list<E>型のオブジェクト)初期化は一時実体化変換の一つである(15.2/p2.4)。mingw-w64は<initializer_list>でクラステンプレートを実装する。

#include <bits/c++config.h>
namespace std _GLIBCXX_VISIBILITY(default)
{
template<class _E>
class initializer_list
{
public:
typedef _E value_type;
typedef const _E& reference;
typedef const _E& const_reference;
typedef size_t size_type;
typedef const _E* iterator;
typedef const _E* const_iterator;
private:
iterator _M_array;
size_type _M_len;
// The compiler can call a private constructor.
constexpr initializer_list(const_iterator __a, size_type __l)
: _M_array(__a), _M_len(__l) { }
public:
constexpr initializer_list() noexcept
: _M_array(0), _M_len(0) { }
// Number of elements.
constexpr size_type
size() const noexcept { return _M_len; }
// First element.
constexpr const_iterator
begin() const noexcept { return _M_array; }
// One past the last element.
constexpr const_iterator
end() const noexcept { return begin() + size(); }
};
template<class _Tp>
constexpr const _Tp*
begin(initializer_list<_Tp> __ils) noexcept
{ return __ils.begin(); }
template<class _Tp>
constexpr const _Tp*
end(initializer_list<_Tp> __ils) noexcept
{ return __ils.end(); }
}

明示のコンストラクタはデフォルトのみであるが、暗黙のコピーコンストラクタ/代入演算子は=deleteされていない。

つまりinitializer_listクラスの公開(public)コンストラクタは明示デフォルトと暗黙コピーだけであるが、リスト初期化ルールでinitializer_listインスタンスを{...}で初期化できる(11.6.4/p5)。コンパイラは{...}からconst T型配列の純右辺値式を一時実体化変換したかのように振る舞い、最初に一時オブジェクトの配列を生成して、次にinitializer_listインスタンスを初期化してポインタによる参照(C++構文の参照ではない)を保持させる。mingw-w64は後者をinitializer_listクラスの非公開(private)コンストラクタとして実装するが、そのコンストラクタは配列ポインタ(配列先頭アドレス)と配列サイズを仮引数に受ける。配列の純右辺値式は通常の文脈で存在せず仮定のものとして捉えるべきだが、その一時オブジェクトは期限切れ値式として現実に存在できて、initializer_listインスタンスはそれにバインドするC++構文の参照のようなものとして振る舞う。

コンパイラが生成する配列の一時オブジェクトは、それで初期化したinitializer_listインスタンスの寿命まで延命し(11.6.4/p6)、その意味でもinitializer_listインスタンスはC++構文の参照としての特質を持つ。しかしながらオブジェクトとして暗黙の代入演算子を持ち、メンバ変数の配列ポインタと配列サイズを他インスタンスからコピーできる。つまり真にC++構文の参照であればバインドを変更できないが、initializer_listインスタンスは変更できて場合によってはエラーをもたらす。

auto型指定子を={...}から型推定する場合は適用可能なinitializer_listクラスを選択する(10.1.7.4.1/p4)。

#include <iostream>
// メモリ回収(リクレイム)に依存してエラーが必ずしも不正に見える出力をするとは限らない
template<typename T> void print_array(const T& val)
{
for (const auto& r:val) {std::cout<<r<<" ";}
std::cout<<std::endl;
}
std::initializer_list<int> func() {return {0,1,2};}
struct S
{
std::initializer_list<int> a_;
S():a_{3,4,5} {}
};
int main()
{
// std::initializer_listは配列一時オブジェクトへの参照として振る舞う
print_array(std::initializer_list<int>{6,7,8}); // OK、一時オブジェクトは完全式評価完了まで有効
auto a1=std::initializer_list<int>{9,10,11}; // auto a1={9,10,11};でも良い
print_array(a1); // OK、一時オブジェクトはa1寿命まで延命
print_array(func()); // 実行時エラー、一時オブジェクトはリターン時に破棄済み
print_array(S{}.a_); // 実行時エラー、一時オブジェクトの参照メンバへのバインドは不適格
// std::initializer_listは暗黙の代入演算子で参照先(バインド)を変更できてしまう
a1=std::initializer_list<int>{12,13,14};
print_array(a1); // 実行時エラー、一時オブジェクトは完全式評価完了で破棄済み
{
auto a2=std::initializer_list<int>{15,16,17};
a1=a2;
print_array(a1); // OK、一時オブジェクトはa2寿命まで延命
}
print_array(a1); // 実行時エラー、一時オブジェクトはa2寿命で破棄済み
return 0;
}