GCC の実装std::initializer_list
は、return full-expression の最後で関数から返された配列を破棄します。これは正しいです?
このプログラムの両方のテスト ケースは、値が使用される前にデストラクタが実行されることを示しています。
#include <initializer_list>
#include <iostream>
struct noisydt {
~noisydt() { std::cout << "destroyed\n"; }
};
void receive( std::initializer_list< noisydt > il ) {
std::cout << "received\n";
}
std::initializer_list< noisydt > send() {
return { {}, {}, {} };
}
int main() {
receive( send() );
std::initializer_list< noisydt > && il = send();
receive( il );
}
プログラムは機能するはずだと思います。しかし、基礎となる標準は少し複雑です。
return ステートメントは、宣言されているかのように戻り値オブジェクトを初期化します。
std::initializer_list< noisydt > ret = { {},{},{} };
これにより、指定された一連のイニシャライザから 1 つの一時initializer_list
ストレージとその下にある配列ストレージが初期化され、次にinitializer_list
最初のものから別のストレージが初期化されます。アレイの寿命は? initializer_list
「配列の寿命はオブジェクトの寿命と同じです。」しかし、そのうちの 2 つがあります。どちらが曖昧です。8.5.4/6 の例は、宣伝どおりに機能する場合、コピー先オブジェクトの有効期間が配列にあるというあいまいさを解決する必要があります。次に、戻り値の配列も呼び出し元の関数に残り、名前付き参照にバインドして保存できるようにする必要があります。
LWSでは、GCC は返される前に配列を誤って削除しますが、例に従って名前付きを保持しますinitializer_list
。Clang も例を正しく処理しますが、リスト内のオブジェクトが破棄されることはありません。これにより、メモリ リークが発生します。ICC はまったくサポートinitializer_list
していません。
私の分析は正しいですか?
C++11 §6.6.3/2:
波括弧初期化リストを持つ return ステートメントは、指定された初期化子リストからのコピー リスト初期化 (8.5.4) によって、関数から返されるオブジェクトまたは参照を初期化します。
8.5.4/1:
… copy-initialization コンテキストでの list-initialization はcopy-list-initializationと呼ばれます。
8.5/14:
T x = a;
…の形式で行われる初期化は、 copy-initializationと呼ばれます。
8.5.4/3 に戻る:
タイプ T のオブジェクトまたは参照のリスト初期化は、次のように定義されます: …</p>
— それ以外の場合、T が の特殊化である
std::initializer_list<E>
場合、initializer_list
オブジェクトは以下で説明するように構築され、同じ型のクラスからのオブジェクトの初期化の規則に従ってオブジェクトを初期化するために使用されます (8.5)。
8.5.4/5:
型のオブジェクトは、実装が型EのN 個
std::initializer_list<E>
の要素の配列を割り当てたかのように、初期化子リストから構築されます。ここで、Nは初期化子リスト内の要素の数です。その配列の各要素は、初期化子リストの対応する要素でコピー初期化され、オブジェクトはその配列を参照するように構築されます。要素のいずれかを初期化するために縮小変換が必要な場合、プログラムは不適切な形式です。std::initializer_list<E>
8.5.4/6:
initializer_list
配列の寿命は、オブジェクトの寿命と同じです。[例:typedef std::complex<double> cmplx; std::vector<cmplx> v1 = { 1, 2, 3 }; void f() { std::vector<cmplx> v2{ 1, 2, 3 }; std::initializer_list<int> i3 = { 1, 2, 3 }; }
v1
との場合、 forで作成さv2
れたinitializer_list
オブジェクトと配列{ 1, 2, 3 }
は完全な式の有効期間を持ちます。の場合i3
、initializer_list オブジェクトと配列には自動有効期間があります。— 最後の例]
ブレース初期化リストを返すことについて少し説明
中括弧で囲まれた裸のリストを返すと、
波括弧初期化リストを持つ return ステートメントは、指定された初期化子リストからのコピー リスト初期化 (8.5.4) によって、関数から返されるオブジェクトまたは参照を初期化します。
これは、呼び出しスコープに返されたオブジェクトが何かからコピーされたことを意味するものではありません。たとえば、これは有効です。
struct nocopy {
nocopy( int );
nocopy( nocopy const & ) = delete;
nocopy( nocopy && ) = delete;
};
nocopy f() {
return { 3 };
}
これではありません:
nocopy f() {
return nocopy{ 3 };
}
copy-list-initialization はnocopy X = { 3 }
、戻り値を表すオブジェクトを初期化するために構文と同等のものを使用することを意味します。これはコピーを呼び出さず、配列の有効期間が延長される 8.5.4/6 の例とたまたま同じです。
そして、Clang と GCC はこの点で一致しています。
その他の注意事項
N2640のレビューでは、このコーナー ケースについての言及はありません。ここに組み合わされた個々の機能については広範な議論がありましたが、それらの相互作用については何もわかりません。
オプションの可変長配列を値で返すことになるため、これを実装するのは面倒です。はそのコンテンツを所有していないためstd::initializer_list
、関数は所有する別のものも返す必要があります。関数に渡す場合、これは単なるローカルの固定サイズの配列です。std::initializer_list
ただし、逆方向では、VLA をのポインターと共にスタックに返す必要があります。次に、シーケンスを破棄するかどうか (シーケンスがスタック上にあるかどうか) を呼び出し元に通知する必要があります。
この問題は、ラムダ関数からブレース初期化リストを返すことで、非常に簡単に遭遇します。これは、いくつかの一時オブジェクトがどのように含まれているかを気にせずに返す「自然な」方法です。
auto && il = []() -> std::initializer_list< noisydt >
{ return { noisydt{}, noisydt{} }; }();
確かに、これは私がここに来た方法と似ています。ただし、->
ラムダの戻り値の型推定は式が返されたときにのみ発生し、波括弧初期化リストは式ではないため、trailing-return-type を省略するとエラーになります。