sを使用した次の3つの初期化の違いは何std::initializer_list
ですか?
std::vector<int> a{ 2, 3, 5, 7};
std::vector<int> b( { 2, 3, 5, 7} );
std::vector<int> c = { 2, 3, 5, 7};
上記の例では、std::vector
は単なるプレースホルダーですが、一般的な回答に興味があります。
sを使用した次の3つの初期化の違いは何std::initializer_list
ですか?
std::vector<int> a{ 2, 3, 5, 7};
std::vector<int> b( { 2, 3, 5, 7} );
std::vector<int> c = { 2, 3, 5, 7};
上記の例では、std::vector
は単なるプレースホルダーですが、一般的な回答に興味があります。
従来(C ++ 98/03)、T x(T());
呼び出された直接初期化のような初期化、およびT x = T();
呼び出されたコピー初期化のような初期化。コピーの初期化を使用した場合、コピーコンストラクターは、使用されていない(つまり、通常は使用されていない)場合でも、使用可能である必要がありました。
イニシャライザーは、そのような変更をリストします。§8.5/14と§8.5/15を見ると、直接初期化とコピー初期化という用語が引き続き適用されることがわかりますが、§8.5/ 16を見ると、ブレース付き初期化リストの場合、これは違いのない区別であることがわかります。 、少なくとも最初と3番目の例では:
—初期化子が(括弧で囲まれていない)braced-init-listの場合、オブジェクトまたは参照はリスト初期化されます(8.5.4)。
そのため、最初と3番目の例の実際の初期化は同じように行われ、どちらもコピーctor(またはmove ctor)を必要としません。どちらの場合も、§8.5.4/3の4番目の箇条書きを扱っています。
—それ以外の場合、Tがクラス型の場合、コンストラクターが考慮されます。該当するコンストラクターが列挙され、オーバーロード解決(13.3、13.3.1.7)によって最適なコンストラクターが選択されます。引数のいずれかを変換するためにナローイング変換(以下を参照)が必要な場合、プログラムの形式が正しくありません。
...したがって、どちらも引数としてstd::vector
をとる'ctorを使用します。std::initializer_list<T>
ただし、上記の引用で述べたように、これは「(括弧で囲まれていない)braced-init-list」のみを扱います。括弧で囲まれたbraced-init-listを使用した2番目の例では、§8.5/ 16の6番目の箇条書きの最初のサブ箇条書きに到達します(geeze-実際にそれらの番号の追加について誰かと話す必要があります)。
—初期化が直接初期化である場合、またはソース型のcv非修飾バージョンが宛先のクラスと同じクラスまたはその派生クラスであるコピー初期化である場合、コンストラクターが考慮されます。該当するコンストラクターが列挙され(13.3.1.3)、過負荷解決(13.3)によって最適なコンストラクターが選択されます。そのように選択されたコンストラクターは、初期化子式または式リストを引数として、オブジェクトを初期化するために呼び出されます。コンストラクターが適用されない場合、または過負荷の解決があいまいな場合、初期化の形式が正しくありません。
これは直接初期化の構文を使用し、括弧内の式はbraced-initializer-listでありstd::vector
、初期化子リストを受け取るctorを持っているため、これが選択されたオーバーロードです。
結論:そこに到達するための標準を通るルートは異なりますが、3つすべてがstd::vector
のコンストラクターオーバーロードを使用することになりstd::initializer_list<T>
ます。実用的な観点からは、3つの間に違いはありません。3つすべてが呼び出さvector::vector(std::initializer_list<T>
れ、コピーやその他の変換は発生しません(省略される可能性が高く、実際には理論上のみ発生する変換も発生しません)。
わずかに異なる値であると思いますが、わずかな違いが1つあります(または少なくともある可能性があります)。変換の絞り込みの禁止は§8.5.4/3にあるため、2番目の例(いわば§8.5.4/ 3を通過しない)では、他の2つが明らかに許可しない変換の絞り込みが許可されるはずです。しかし、私が熱心なギャンブラーであったとしても、コンパイラーがこの区別を実際に認識し、1つのケースではナロー変換を許可し、他のケースでは許可しないことに賭けることはありません(少し意外なことに、それが許可されることを意図しています)。
上記の例では、std :: vectorは単なるプレースホルダーであり、一般的な回答に興味があります。
どのくらい「一般的な」答えが欲しいですか?それが何を意味するかは、初期化するタイプとそれらが持つコンストラクターに実際に依存するためです。
例えば:
T a{ 2, 3, 5, 7};
T b( { 2, 3, 5, 7} );
これらは2つの異なるものである可能性があります。またはそうでないかもしれません。コンストラクターが何T
を持っているかによって異なります。T
単一のコンストラクターinitializer_list<int>
(または、が整数型である他のコンストラクター)がある場合、これらの両方がそのコンストラクターを呼び出しinitializer_list<U>
ます。U
ただし、それがない場合、これら2つは異なることを行います。1つ目は、整数リテラルで生成できる4つの引数を取るコンストラクターを呼び出そうとします。2つ目は、 1つの引数を取るコンストラクターを呼び出そうとします。このコンストラクターは、で初期化しようとします{2, 3, 5, 7}
。これは、各1引数コンストラクターを通過し、その引数の型が何であるかを把握し、それを構築しようとすることを意味します。R{2, 3, 5, 7}
これらのいずれも機能しない場合は、それを。として渡そうとしinitializer_list<int>
ます。そしてそれがうまくいかない場合、それは失敗します。
initializer_list
コンストラクターが常に優先されます。
コンストラクターは、すべての要素が同じ型を持つbraced-init-listであるinitializer_list
ため、機能しているだけであることに注意してください。{2, 3, 5, 7}
があった場合、コンストラクター{2, 3, 5.3, 7.9}
はチェックされません。initializer_list
T c = { 2, 3, 5, 7};
これはa
、どのような種類の変換を行うかを除いて、のように動作します。これはcopy-list-initializationであるため、initializer_listコンストラクターを呼び出そうとします。そのようなコンストラクターが使用できない場合は、4引数のコンストラクターを呼び出そうとしますが、for引数を型パラメーターに暗黙的に変換することしかできません。
それが唯一の違いです。コピー/移動コンストラクターなどは必要ありません(仕様では、コピーリストの初期化は3か所でしか言及されていません。コピー/移動の構築が利用できない場合は、いずれも禁止されていません)。a
引数で許可される変換の種類を除いて、ほぼ正確に同等です。
これが、一般に「均一初期化」と呼ばれる理由です。これは、どこでもほぼ同じように機能するためです。
から抽象化しましょうstd::vector
。そしてそれを呼んでT
ください。
T t{a, b, c};
T t = { a, b, c };
T t({a, b, c});
最初の2つの形式はリストの初期化です(これらの唯一の違いT
は、がクラスの場合、2番目のexplicit
コンストラクターの呼び出しが禁止されていることです。1つが呼び出されると、プログラムの形式が正しくなくなります)。最後の形式は、C ++ 03からわかるように、通常の直接初期化です。
T t(arg);
{a, b, c}
as argが表示されるということは、コンストラクター呼び出しの引数が中括弧の初期化子リストであることを意味します。この3番目の形式には、リストの初期化にあるような特別な処理はありません。ブレースされたinitリストに引数が1つしかない場合でも、クラスタイプであるT
必要があります。この場合、C++11をリリースする前に明確なルールを設定できてうれしいです。
コンストラクターが3番目に呼び出されるという点で、仮定しましょう
struct T {
T(int);
T(std::initializer_list<int>);
};
T t({1});
直接初期化はオーバーロードされたコンストラクターの呼び出しにすぎないため、これを次のように変換できます。
void ctor(int);
void ctor(std::initializer_list<int>);
void ctor(T const&);
void ctor(T &&);
両方の末尾の関数を使用できますが、これらの関数を選択した場合は、ユーザー定義の変換が必要になります。パラメータを初期化するためT ref
に、リストの初期化が使用されます。これは、parensを使用した直接の初期化ではないためです(したがって、パラメータの初期化はと同等ですT ref t = { 1 }
)。最初の2つの関数は完全に一致します。ただし、このような場合、一方の関数がに変換されstd::initializer_list<T>
、もう一方の関数が変換されない場合、前者の関数が優先されると標準は述べています。そのため、このシナリオでは、2番目ctor
が使用されます。このシナリオでは、最初に初期化子リストctorのみを使用して2フェーズの過負荷解決を実行しないことに注意してください。これは、リスト初期化のみが実行します。
最初の2つでは、リストの初期化を使用し、コンテキストに依存する処理を実行します。が配列の場合T
、配列を初期化します。クラスのこの例を取る
struct T {
T(long);
T(std::initializer_list<int>);
};
T t = { 1L };
この場合、2相の過負荷解決を行います。最初に初期化リストコンストラクターのみを検討し、1つが一致するかどうかを確認します。引数として、ブレースされたinitリスト全体を取得します。2番目のctorが一致するので、それを選択します。最初のコンストラクターは無視します。初期化子リストctorがない場合、または一致するものがない場合は、すべてのctorと初期化子リストの要素を取得します
struct T {
T(long);
template<typename A = std::initializer_list<int>>
T(A);
};
T t = { 1L };
この場合、1L
に変換できないため、最初のコンストラクターを選択しstd::initializer_list<int>
ます。
std::initializer_list
コンストラクターを取り入れたカスタムクラスを使用して、gcc4.7.2で少し遊んだ。私はそれらすべてのシナリオとそれ以上を試しました。これらの3つのステートメントについて、そのコンパイラーで観察可能な結果に実際には違いはないようです。
編集:これは私がテストに使用した正確なコードです:
#include <iostream>
#include <initializer_list>
class A {
public:
A() { std::cout << "A::ctr\n"; }
A(const A&) { std::cout << "A::ctr_copy\n"; }
A(A&&) { std::cout << "A::ctr_move\n"; }
A &operator=(const A&) { std::cout << "A::=_copy\n"; return *this; }
A &operator=(A&&) { std::cout << "A::=_move\n"; return *this; }
~A() { std::cout << "A::dstr\n"; }
};
class B {
B(const B&) { std::cout << "B::ctr_copy\n"; }
B(B&&) { std::cout << "B::ctr_move\n"; }
B &operator=(const B&) { std::cout << "B::=copy\n"; return *this; }
B &operator=(B&&) { std::cout << "B::=move\n"; return *this; }
public:
B(std::initializer_list<A> init) { std::cout << "B::ctr_ user\n"; }
~B() { std::cout << "B::dstr\n"; }
};
int main()
{
B a1{ {}, {}, {} };
B a2({ {}, {}, {} });
B a3 = { {}, {}, {} };
// B a4 = B{ {}, {}, {} }; // does not compile on gcc 4.7.2, gcc 4.8 and clang (top version)
std::cout << "--------------------\n";
}
a1
、gcc 4.7.2、gcc 4.8、および最新のclangで正常にコンパイルされますa2
。a3
また、3つのケースすべてについて、リストメンバーに対して実行された操作の数の間に観察可能な結果は見られません。最後のケース(質問からではありません)は、B
コピー/移動コンストラクターをプライベート/削除するとコンパイルされません。