|
本サイトは移転しました。旧アドレスからのリダイレクトは2025年03月31日(月)まで有効です。
|
🛈 | ✖ |
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++黎明から当たり前に利用するプリミティブな言語要素が、この複雑さ、不安定さはどうしたもんだろうか。
変数はオブジェクトか参照のどちらかを指す(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)でなければそれをコールする。配列型は全ての要素を値初期化する。それ以外はゼロ初期化する。
初期化子を持つ初期化は、コピー初期化と(値初期化を含む)直接初期化に分類される。変数初期化では初期化子が=aであるか、{...}あるいは(a)であるかで容易に区別できる。初期化子を持たなければデフォルト初期化になる。以下に変数初期化以外の場合をまとめるが、どちらであるかは文脈から判断する。コピー初期化は変数初期化以外の文脈で形式=aとならないが、=aで代表して記述する。コピー初期化はムーブ(15.8)かもしれない。
集約(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がクラス型でなければコピー初期化と直接初期化に差異は無い。クラス型の場合、コピー初期化は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)した。なお直接初期化でも同じで、初期化式が同様の時にオーバーロード選択するはずのコピーコンストラクタは無くても良い。
古い教科書は直接初期化を、値コピーのセマンティクスを持つ(コピーコンストラクタを必要とする)コピー初期化に優先せよとした(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型の式で以下に初期化される。一時実体化変換は後に詳述する。
(※1)(※2)は直接バインド(bind directlyあるいはdirect binding)、(※3)は間接バインド(indirect binding)と言う。直接バインドがユーザー定義変換を伴う場合は変換関数に限定されるが、ここでは明示せずオーバーロード解決(16.3.1.6)が明示する。非constな左辺値参照へのユーザー定義変換は変換関数のみであるが(なぜなら非constな左辺値参照は右辺値式にバインドできず、変換コンストラクタは純右辺値式である)、そうでない参照へのユーザー定義変換は変換コンストラクタも可能で、そういった場合は間接バインドとする。つまり(※3)第1項のT1あるいはT2がクラス型の場合のユーザー定義変換は変換コンストラクタであり、結果は純右辺値式でこれで参照を直接初期化すれば再帰ルール(※2)第1項が一時実体化変換する。(※4)は間接バインドがT1がT2に参照関連する場合を除くための遠回しの文言と思われる。
これらを不正確ながら理解しやすく言い換えれば以下となる。
直接バインド(※2)は間接バインド(※3)に優先する。これからサイト作成者は、クラス型参照の初期化がユーザー定義変換を伴う場合は変換関数が変換コンストラクタに優先すると解釈するが、mingw-w64はそのように振る舞わない。解釈とmingw-w64のどちらが間違っているのだろうか。
リスト初期化(list-initialization)はC++11で追加され、全ての型の初期化を{...}で統一する。最古のC言語から配列と構造体は初期化子={...}で初期化できたが、これを一般化した上で初期化子{...}を追加した。C++標準ライブラリのコンテナ型も配列のように要素のリストで初期化できて、その目的でstd::initializer_list(21.9)という特殊なプロクシクラステンプレートが追加された。
初期化子{...}による初期化は(値初期化を含む)直接初期化の文脈に現れて直接リスト初期化(direct-list-initialization)と呼び、={...}はコピー初期化の文脈に現れてコピーリスト初期化(copy-list-initialiation)と呼ぶ。それぞれ直接初期化とコピー初期化の部分集合であるものの、それぞれの補集合(直接リスト初期化でない直接初期化、あるいはコピーリスト初期化でないコピー初期化)とは別に定義される。
リスト初期化はオブジェクトまたは参照において以下とする(11.6.4/p3)。コピー初期化/直接初期化は、コピーリスト初期化/直接リスト初期化に従うものとする。
std::initializer_list<E>型あるいはその参照型を最初の仮引数として、他の仮引数が無いか全てデフォルト値を持つコンストラクタを初期化リストコンストラクタ(initializer-list constructor)と呼ぶ(11.6.4/p2)。初期化子{a}あるいは={a}の時に最優先でオーバーロード選択されて(16.3.1.7/p1)初期化リストを仮引数に受ける。C++標準ライブラリのコンテナ型は初期化リストコンストラクタを持ち、配列のように要素のリストで初期化できる。
Tが集約でないクラス型の場合も{...}と={...}は同定義で、つまりその場合に={...}は値コピーのセマンティクスを持たない。
初期化に現れる{...}と(...)を比較する。{...}は"波括弧による初期化リスト"(braced-init-list)で、(...)は関数コールの一般形式に後置する"丸括弧に囲まれた式リスト"((expression-list))で、式リスト(expression-list)と初期化リスト(initializer-list)は等しい。{...}と(...)は(C++最厄構文を除いて)共に直接初期化の初期化子として使えるが、{...}だけが初期化節としてコピーリスト初期化できる。初期化節に形式()は許されず、形式(a)はコンマ演算(8.19)を囲む括弧式(8.1.3)でコピーリスト初期化ではない。さらに{a}は(a)より以下2点で厳密である。
一時オブジェクト(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)。
一時オブジェクトはそれを生成した完全式(full-expression)(4.6/p12)の完了で解体される(15.2/p4)が、三つの例外となる文脈がある。第一の文脈は初期化子を持たない配列の要素初期化がデフォルトコンストラクタをコールする場合で、第二の文脈は配列全体をコピーするときの要素コピーがコピーコンストラクタをコールする場合で、これらのコンストラクタが一つ以上の実引数デフォルト値を持てばデフォルト実引数として構築される一時オブジェクトは次の要素が構築される前に解体される(p5)。第三の文脈は参照へのバインドで、参照にバインドされた一時オブジェクトまたは参照にバインドされたサブオブジェクトを持つ一時オブジェクトは参照の寿命まで延命するが、以下を例外とする(p6)。
参照にバインドされない一時オブジェクトは、同一完全式で先に構築された一時オブジェクトより早く解体される(p7)。二つ以上の一時オブジェクトがそれぞれ寿命の等しい参照にバインドされる場合、構築の逆順で解体される。参照にバインドされた一時オブジェクトの解体は、参照の記憶域領域(storage duration)(6.7/p1)に依存する。参照メンバを一時オブジェクトで初期化することは不適格とする(15.6.2/p8)。
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>でクラステンプレートを実装する。
明示のコンストラクタはデフォルトのみであるが、暗黙のコピーコンストラクタ/代入演算子は=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)。