C ++初期化リスト:良い、悪い、悪い


この記事では、C ++でのブレース初期化子リストの動作 、それらが解決するために呼び出された問題、順番に発生した問題、およびトラブルを回避する方法について説明します。


まず、コンパイラー(または言語弁護士)のように感じ、次の例をコンパイルするかどうか、理由、およびその動作を理解することをお勧めします。


クラシック:


std::vector<int> v1{5}; std::vector<int> v2(5); std::vector<int> v3({5}); std::vector<int> v4{5}; std::vector<int> v5 = 5; 

現代のC ++は安全な言語です。私は決して自分の足を撃ちません。


 std::vector<std::string> x( {"a", "b"} ); std::vector<std::string> y{ {"a", "b"} }; 

ブラケットの神にもっとブラケット!


 //    ,     ? std::vector<std::vector<int>> v1{{{{{}}}}}; 

1つのコンストラクターが適合しない場合、2番目のコンストラクターを使用しますか?


 struct T{}; struct S { S(std::initializer_list<int>); S(double, double); S(T, T); }; int main() { S{T{}, T{}}; //    ? S{1., 2.}; //  ? } 

ほとんど常に自動 、彼らは言った。 これにより、読みやすさが向上します。


 auto x = {0}; //     x? auto y{0}; //   y? //  ?      

古代からの挨拶:


 struct S { std::vector<int> a, b; }; struct T { std::array<int, 2> a, b; }; int main() { T t1{{1, 2}, {3, 4}}; T t2{1, 2, 3, 4}; T t3{1, 2}; S s1{{1, 2}, {3, 4}}; S s2{1, 2, 3, 4}; S s3{1, 2}; } 

すべてが明確ですか? それとも、何もはっきりしていませんか? 猫へようこそ。


免責事項


  1. この記事はガイダンスのみであり、完全であると主張するものではなく、明確にするために正確さを犠牲にすることがよくあります。 一方、読者はC ++の基本的な知識があることを前提としています。
  2. 私は英語の用語でロシア語への合理的な翻訳を考え出そうとしましたが、一部では完全な大失敗に見舞われました。 {...} braced-init-listsという形式の構文構成を呼び出します{...}標準ライブラリの型はstd::initializer_list 、初期化型は次のように記述します: int x{5} is list-initユニフォーム初期化構文 、またはユニバーサル初期化構文とも呼ばれます。

注意!


最初に注意することは重要な観察です。 記事全体から彼だけを取り上げて、怠lazがさらに読みやすくなったとしても、ここでの私の使命は果たされます。


そのため、中括弧付きリスト (中括弧付きのピース、{1、2、3}、 均一な初期化構文 )とstd::initializer_listは2つの異なるものです! それらは強く結びついており、あらゆる種類の微妙な相互作用がそれらの間で発生しますが、それらのいずれかが他なしで存在する可能性があります。


しかし、最初に、少しの背景。


Unicorn初期化構文



C ++ 98(およびそのバグ修正アップデート、C ++ 03)では、初期化に関連する十分な問題と矛盾がありました。 それらのいくつかを次に示します。



したがって、C ++ 11の開発中に、次のアイデアが生まれました。中括弧で何かを初期化する機会を与えましょう。



落とし穴


C ++ 11では可変数のパラメーターを持つテンプレートも表示されるため、コンテナーの初期化は自動的に行われるはずです。そのため、可変引数コンストラクターを記述すると、実際には機能しません。



これらの問題を解決するために、彼らはstd::initializer_listを思い付きました。これは既知のサイズの要素の配列の非常に軽量なラッパーであり、 braced-init-listから構築することもできる「マジッククラス」です。


なぜ「魔法」なのですか? 上記の理由だけで、ユーザーコードで効果的に構築できないため、コンパイラは特別な方法で作成します。


なぜ必要なのですか? ほとんどの場合、カスタムクラスは「この型のbraced-init-list要素から構築したい」と言うことができ、このためにテンプレートコンストラクターは必要ありません。


(ところで、現時点では、 std::initializer_listbraced-init-listは異なる概念であることが明らかになるはずです)


すべて順調ですか? vector(std::initializer_list<T>)コンストラクターvector(std::initializer_list<T>)をコンテナーに追加するだけで機能しますか? ほぼ。


次のエントリを検討してください。


 std::vector<int> v{5}; 

v(5)またはv({5})意味は何ですか? 言い換えると、5つの要素のベクトル、または5値を持つ1つの要素のベクトルを構築する必要がありますか?


この競合、 オーバーロードの解決を解決するために、リストの初期化の場合、渡された引数に応じた目的の関数の選択は2段階で行われます。


  1. 最初に、タイプがstd::initializer_list単一のパラメーターを持つコンストラクターのみstd::initializer_list (これは、コンパイラーが中括弧の内容に基づいてstd::initializer_listを生成するときの主なポイントの1つです)。 それらの間で過負荷の解決が行われます。
  2. 適切なコンストラクタがない場合、すべてが通常通りです-braced-init-listを引数リストに展開し、利用可能なすべてのコンストラクタ間のオーバーロードを解決します。

最初の段階で負けた設計者は、2番目の段階でうまく適合することに注意してください。 これは、記事の最初からベクトルを初期化するための過剰な括弧を使用した例を説明しています。 明確にするために、ネストされたテンプレートの1つを削除し、 std::vectorをクラスに置き換えます。


 template<typename T> struct vec { vec(std::initializer_list<T>); }; int main() { vec<int> v1{{{}}}; } 

コンストラクターはポイント1に適合しません- {{{}}} std::initializer_list<int>intます。intは{{}}初期化できないためです。 ただし、 {}は非常にゼロで初期化されるため、コンストラクターは2番目のステップで受け入れられます。


しかし、面白い変換は、コンストラクターをスローする適切な理由ではないことです-次の例では、最初のコンストラクターがオーバーロードを解決する最初のステップで取得され、その後コンパイラエラーが発生します。 良くも悪くも-私は知りません、私にとってはそれは単に驚くべきことです。


 struct S { S(std::initializer_list<int>); S(double, double); }; int main() { S{1., 2.}; } 

記事の冒頭から行ベクトルを使用した例では、かなり怖い結果を伴う同様の問題が得られます。 残念ながら、 std::stringは、渡された2つのポインターを文字列の先頭と末尾として扱うコンストラクターがあります。 文字列リテラルに対するこの動作の結果は明らかに嘆かわしいですが、構文は正しいオプションに非常に似ており、たとえば一般化されたコードによく現れます。


集合クラス


さて、これですべてですか? そうでもない。 Cから継承された古い構造初期化構文はなくなりません。これを行うことができます。


 struct A { int i, j; }; struct B { A a1, a2; }; int main() { B b1 = {{1, 2}, {3, 4}}; B b2 = {1, 2, 3, 4}; // brace elision B b3 = {{1, 2}}; // clause omission } 

ご覧のように、 集約を初期化するとき(大まかに言うと、 PODと混同しないCのような構造、PODは別のものです)、ネストされたブラケットをスキップして初期化子の一部を捨てることもできます。 この動作はすべて、C ++にきちんと移植されています。


なんてナンセンスなのか、なぜこれが現代の言語で書かれているのか? GCCとclangの開発者は、これについて少なくともコンパイラーに警告しましょうstd::array内部に配列を含む集合クラスでなければ正しいでしょう。 したがって、明らかな理由で、括弧を捨てることに関する警告は、このような無邪気なコードで機能します。


 int main() { std::array<int, 3> a = {1,2,3}; } 

GCCは、 -Wallモードで対応する警告をオフにすることでこの問題を「解決」しましたが、clangでは、過去3年間、すべて同じままです。


ところで、 std::arrayが集合体であるという事実は、標準の狂った作者や標準ライブラリの怠librariesな開発者の気まぐれではありません:効率を損なうことなく、このクラスの必要なセマンティクスを達成することは単に不可能です。 Cとその奇妙な配列からの別の挨拶。


おそらく、集約クラスの最大の問題は、標準ライブラリの一般化された関数(を含む)との相互作用が最も成功しないことです。 現時点では、渡されたパラメーターからオブジェクトを構築する関数(たとえば、 vector::emplace_backまたはmake_unique )は、「ユニバーサル」ではなく、通常の初期化を呼び出します。 これは、 リスト初期化を使用すると、 std::initializer_listを受信する代わりに、通常の方法で「通常の」コンストラクターを呼び出すことができないためです(非テンプレートコードでの初期化と同じ問題です。ここでのみ、別の呼び出しで回避できません)コンストラクター)。 この方向での作業は進行中ですが、これまでのところ私たちは持っているものを持っています。


ほとんど常に自動


型推論と組み合わせた場合、 braced-init-listはどのように動作しますか? auto x = {0}; auto y = {1, 2};と書くとどうなりますかauto x = {0}; auto y = {1, 2}; auto x = {0}; auto y = {1, 2}; ? いくつかの合理的な戦略を考え出すことができます。


  1. 一般的にそのような初期化を許可しません(実際、プログラマはこれで何を言いたいですか?)
  2. 最初の変数の型をintとして出力し、2番目のオプションを無効にします
  3. xとyの両方をstd::initializer_lits<int>タイプにしstd::initializer_lits<int>

私が一番好きな最後のオプション(実際にはstd::initializer_listようなローカル変数を持っている人はほとんどいません)が、C ++ 11標準に陥ったのは彼でした。 これがプログラマー(考えている人)に問題を引き起こすことが徐々に明らかになったため、標準のhttp://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.htmlにパッチが追加されました動作#2を実装します... 直接リストの初期化auto x{5} )の場合にのみ、 コピーリストの初期化auto x = {5} )の場合にすべてを以前のままにします。


コメントできません。 私の意見では、これは常識が一時的に言語の作者を去った非常にまれなケースの一つです。 これについて何かコメントがあれば、コメントで教えてください。


小計


普遍的な初期化構文とstd::initializer_listは良い動機から追加された言語機能ですが、後方互換性が永遠に必要であり、初期段階では常に先見の明のあるソリューションではないため、それらを取り巻く状況は現在あまりにも複雑であるようです拷問を受け、関係者全員にとって最も快適ではない-標準の作成者、コンパイラ、ライブラリ、およびアプリケーション開発者。 最高のものが欲しかったのですが、有名な漫画のように判明しました。



例として、たとえば、最初に標準に追加された[over.best.ics] /4.5のストーリーを取り上げます。その後、考えずに冗長として削除され、修正された形式で再び追加されました。 !)条件。


それにもかかわらず、この機会は有用であり、生活が楽になるので、ここでは、自分自身を足で撃たないための、小さくて客観的でないリストを示します。


  1. 実際に何が起こっているかを理解するために少し時間をかけてください(標準の段落を読むことをお勧めます-驚くほど明確で、残りにあまり依存していません)
  2. コンストラクターパラメーターを除き、 std::initializer_listを使用しないでください
  3. はい、何が起こっているかを理解している場合にのみコンストラクターパラメーターを使用します(不明な場合は、ベクター、イテレーターのペア、または範囲からより適切に構築します)
  4. どうしても必要な場合を除き、集約クラスを使用しないでください;すべてのフィールドを初期化するコンストラクターを作成する方が適切です
  5. autoと組み合わせてbraced-init-listを使用しないでください
  6. 空の初期化リストをどう処理するかについてのこの記事を読んでください(翻訳して投稿するために私の手がかゆくなります。すぐにやるでしょう)
  7. そして、冒頭で書いたように、 braced-init-liststd::initializer_listは、互いに非常に巧妙に相互作用する異なる概念であることに留意してください

夢を見てみましょう


ここで、現状の紹介を終了します。 投入する 私たちが完璧な世界に住んでいたなら、すべてがどのようになるかを夢見てください。


std::initializer_list中に中括弧を再利用してstd::initializer_listを作成することは、言語設計エラーだと思われます。 代わりに、より明示的で個別の構文(たとえば、 <$...$>などの奇妙なかっこや、 std::of(...) )。 つまり、次のようなベクトルを初期化しますstd::vector<std::vector<int>> x = std::of(std::of(1, 2), std::of(3, 4));


それは何を与えますか? 新しい初期化メソッド(ほとんどの厄介な解析および縮小変換に対する保護付き)はstd::initializer_listから切り離され、オーバーロードを解決するために別のステップを導入する必要はありません。 vector<int>またはvector<string>コンストラクターの問題はなくなり、新しい構文初期化は、一般化されたコードで問題なく使用できます。


もちろん、このアプローチの欠点は非常に深刻です:最も単純な場合のugい構文と、Cスタイルの初期化で構文をより統一するという目標からの逸脱(このような統一についてはかなり懐疑的ですが、これは別の議論のトピックです)。


集約クラスも好きではありません。 std::array問題をstd::arrayと、言語のこのような大きく特別な機能の存在を正当化する理由がわかりません。 プログラマーが単純なクラス用の簡単なコンストラクターを作成したくないという事実の問題は、例えば、すべてのフィールドを順番に初期化するコンストラクターを生成する機会を与えることにより、より侵襲性の低い方法で解決できます。


 struct S { int a, b; S(...) = aggregate; }; 

おわりに


最後に、100%正しい、または究極の真実を装っていないことをもう一度繰り返します。 不明な点がある場合や、この特定のトピックについて何かコメントがある場合は、コメントへようこそ。



Source: https://habr.com/ru/post/J330402/


All Articles