C++ 標準(具体的には [over.ics.list])によると、リスト初期化が行われると、コンパイラはブレース初期化リストを std::initializer_list<T> を受け入れるコンストラクタに一致させようとします。このバインディングは同一変換(正確な一致)を構成し、非初期化リストコンストラクタに個別の要素を一致させるために必要なユーザー定義の変換よりも高い優先順位を持ちます。したがって、Container(size_t count, T value) のようなコンストラクタは、Container(std::initializer_list<T>) に {10, 20} で呼び出された場合に負けます。後者はブレース初期化リスト引数自体に変換を必要としないためであり、要素ごとの狭義化に関係なくなります。
私たちはグラフィックスエンジン用の Matrix クラスを設計しており、フィルコンストラクタ Matrix(size_t rows, size_t cols, double val) とリテラルテーブル初期化のためのアグリゲートスタイルのコンストラクタ Matrix(std::initializer_list<std::initializer_list<double>>) の両方を提供していました。あるジュニア開発者は Matrix m{1080, 1920, 0.0} と書き、1080x1920 のゼロ初期化された行列を期待していましたが、実際には3つのスカラー値を含む1x3行列が作成され、デバッグセッション中に追跡が難しい微妙なランタイムレンダリングクラッシュを引き起こしました。
当初、私たちはフィルコンストラクタのために Matrix(1080, 1920, 0.0) という括弧構文を義務付けて std::initializer_list オーバーロードを回避することを検討しました。しかし、これは私たちのコーディングスタンダードの C++11 の均一初期化の好みを侵害し、いくつかのコンストラクタに括弧が必要である一方、他のコンストラクタはブレースを使用するという不一致な API を作成しました。
次に、フィルコンストラクタに fill_tag_t パラメータを追加してタグディスパッチを探求し、効果的にユーザーが Matrix{fill_tag, 1080, 1920, 0.0} と書かなければならないようにしました。この呼び出しのあいまいさを解消しましたが、パブリックインターフェースを煩雑にし、人工的なタグタイプなしで直感的なコンストラクタシグネチャを期待していた開発者を混乱させました。
第三に、SFINAE を使用してテンプレートパラメータに対して std::initializer_list コンストラクタをネストされたブレースのみに反応するように制限しようとしました。このアプローチは、Matrix{{1.0, 2.0}, {3.0, 4.0}} のような正当なユースケースを破壊し、コンパイル時間とエラーメッセージの複雑さを増加させる脆弱なテンプレートメタプログラミングを導入しました。
最終的に、私たちは静的ファクトリ関数 Matrix::filled(rows, cols, val) を導入し、3つのパラメータを持つフィルコンストラクタを非公開にし、ユーザーが次元ベースの構築のために明示的な構文を使用するように指示し、std::initializer_list コンストラクタはアグリゲート構文のために公開したままにしました。これにより、リテラルテーブルのための直感的なブレース初期化が保たれ、次元引数の誤解釈のリスクが軽減されました。
リファクタリングされた API により、Matrix{1080, 1920, 0.0} が一致する公開コンストラクタがないため、コンパイル時エラーになりました。開発者は、フィル操作のために Matrix::filled(1080, 1920, 0.0) を使用するか、初期化リストのために Matrix{{...}} を使用することを余儀なくされ、コードの明快さと安全性が大幅に向上しました。
コンパイラは、ブレース初期化リストから非初期化リストコンストラクタへの変換シーケンスをどのようにランク付けしますか?
C++ 標準のリスト初期化におけるオーバーロード解決ルールによれば、ブレース初期化リストを std::initializer_list<T> パラメータにバインドすることは、最も高い優先順位を持つ同一変換(正確な一致)を構成します。それに対して、同じブレース初期化リストを別のコンストラクタに一致させるには、コンパイラがリストを括弧付きの式リストとして扱い、各要素ごとにユーザー定義または標準の変換を実行する必要があります。同一変換がすべての他の変換シーケンスよりも高い優先順位を持つため、初期化リストコンストラクタは、代替コンストラクタによって要求される要素タイプが論理的に悪化しても勝ちます。
なぜ auto x = {1, 2, 3}; は C++11 と C++14 で std::initializer_list<int> を推論し、auto x{1, 2, 3} は C++17 以降で不正なものになるのですか?
C++17 以前は、auto との = トークンを使用したコピーリスト初期化は、ブレース初期化リストに対して常に std::initializer_list を推論しました。しかし、C++17 では、auto を使用した直接リスト初期化の新しいルールが導入され(= なし)、標準テンプレート引数推論を実行します:ブレース初期化リストが複数の要素を含む場合、推論は失敗します。なぜなら、auto はこのコンテキストで std::initializer_list を表現できないため、プログラムは不正になります。この変更により、直接初期化のための "秘密の std::initializer_list" トラップが排除されましたが、候補者はしばしばコピー構文(auto x = {...})が現代の C++ でもなお std::initializer_list を推論することを見落とし、初期化スタイル間に微妙な不一致を生じさせます。
初期化リストコンストラクタと可変テンプレートコンストラクタの両方を持つクラスがあいまいになるシナリオはどのようなもので、どのように std::in_place_t でそれを解消できますか?
クラスが Container(std::initializer_list<T>) と template<typename... Args> Container(Args&&... args) の両方を提供している場合、可変パックはテンプレート引数推論を介して初期化リストコンストラクタと同じ引数に一致することができます。Container c{1, 2, 3} に対して、両方のコンストラクタが利用可能です:最初はブレース初期化リストの同一変換を介して、次に Args を int, int, int と推論することによって。通常、非テンプレート初期化リストコンストラクタがタイブレーカーに勝ちますが、可変コンストラクタに std::in_place_t のようなタグタイプを追加することで(例:Container(std::in_place_t, Args&&... args))、ユーザーは Container{std::in_place, 1, 2, 3} と書く必要があり、可変バージョンが明示的に呼び出され、初期化リストコンストラクタが同質のブレースリストをデフォルトで処理することを保証します。