3回目の試み。2番目は、状況の隅々まで説明する過程で非常に長くなりました. でもねえ、私もその過程で多くのことを学びました。それがポイントだと思いますよね?:) ともかく。それ自体は有用な参考資料ですが、「明確な説明」には及ばないため、長い回答をそのままにして、質問を新たに取り上げます。
ここで何を扱っているのですか?
f
g
些細な状況ではありません。彼らは、あなたが彼らに出会った最初の数回を理解し、感謝するのに時間がかかります. 問題となるのは、オブジェクトの有効期間、戻り値の最適化、オブジェクト値を返すことの混乱、参照型のオーバーロードとの混乱です。それぞれに対処し、それらの関連性を説明します。
参考文献
最初のことが最初です。参考書とは?それらは構文のない単なるポインターではありませんか?
それらはそうですが、重要な意味で、それらはそれ以上のものです。ポインターは文字通り、一般的にメモリの場所を参照します。ポインターが設定されている場所にある値についての保証は、あるとしてもほとんどありません。一方、参照は実際の値のアドレスにバインドされます。値は、アクセスできる期間中存在することを保証しますが、他の方法 (一時的なものなど) でアクセスできる名前を持たない場合があります。
経験則として、「そのアドレスを取得」できる場合は、参照を扱っていることになりますlvalue
。左辺値に割り当てることができます。これが*pointer = 3
機能する理由です。オペレーター*
は、ポイントされているアドレスへの参照を作成します。
これにより、参照が指すアドレスよりも多かれ少なかれ有効になるわけではありませんが、C++ で自然に見つかる参照には、 (適切に作成された C++ コードと同様に) この保証があります。それらとのやり取りの間、その寿命について知る必要がない方法。
オブジェクトの有効期間
次のようなことを求めて、いつ c'tors と d'tors が呼び出されるかを、私たちは皆知っているはずです。
{
A temp;
temp.property = value;
}
temp
のスコープが設定されます。私たちはそれがいつ作られ、いつ破壊されたかを正確に知っています。破壊されたことを確認できる1つの方法は、これが不可能であるためです。
A & ref_to_temp = temp; // nope
A * ptr_to_temp = &temp; // double nope
コンパイラは、そのオブジェクトがまだ存在することを非常に明確に期待するべきではないため、それをやめさせます。これは、参照を使用するたびに微妙に発生する可能性があります。そのため、参照を使用して何をしているのかがわかるまで(または参照を完全に理解することをあきらめて、自分の生活を続けたいと思っている場合は完全に参照を避けることを提案する人がいることがあります)。 )。
表現の範囲
一方で、一時変数は、一時変数が含まれる最も外側の式が完了するまで存在することにも注意する必要があります。つまり、セミコロンまでです。たとえば、コンマ演算子の LHS に存在する式は、セミコロンまで破棄されません。すなわち:
struct scopetester {
static int counter = 0;
scopetester(){++counter;}
~scopetester(){--counter;}
};
scopetester(), std::cout << scopetester::counter; // prints 1
scopetester(), scopetester(), std::cout << scopetester::counter; // prints 2
これはまだ実行の順序付けの問題を回避できず、++i++
演算子の優先順位や、あいまいなケースを強制したときに発生する可能性のある恐ろしい未定義の動作i++ = ++i
(例: ) などに対処する必要があります。重要なことは、作成されたすべての一時変数がセミコロンまで存在し、もはや存在しないことです。
2 つの例外があります - elision / in-place-construction (aka RVO) とreference-assignment-from-temporaryです。
値によるリターンと省略
エリシオンとは?RVO などを使用する理由 これらはすべて、「インプレース構築」という、はるかに理解しやすい 1 つの用語に分類されます。関数呼び出しの結果を使用して、オブジェクトを初期化または設定したとします。例えば:
A x (void) {return A();}
A y( x() );
ここで発生する可能性のある最長の一連のイベントを考えてみましょう。
- 新しい
A
が構築されますx
- によって返される一時的な値は、以前の値への参照を使用して初期化され
x()
た新しいA
- 新しい
A
- y
- は一時的な値を使用して初期化されます
可能であれば、中間体にアクセスできない、または不要であると安全に想定できるように、できるだけ少数A
の中間体が構築されるように、コンパイラーは物事を再配置する必要があります。問題は、どのオブジェクトがなくてもできるかということです。
ケース #1 は明示的な新しいオブジェクトです。これが作成されないようにする場合は、既に存在するオブジェクトへの参照が必要です。これは最も単純なものであり、これ以上言う必要はありません。
#2では、何らかの結果を構築することを避けることはできません。結局のところ、value で返されます。ただし、2 つの重要な例外があります (スローされたときに影響を受ける例外自体は含まれません) : NRVOとRVOです。これらは #3 で起こることに影響しますが、#2 に関しては重要な結果とルールがあります...
これは、 elisionの興味深い癖によるものです。
ノート
コピー省略は、観察可能な副作用を変更できる最適化の唯一の許可された形式です。一部のコンパイラは、許可されているすべての状況 (デバッグ モードなど) でコピー省略を実行するわけではないため、コピー/移動コンストラクタおよびデストラクタの副作用に依存するプログラムは移植できません。
コピーの省略が行われ、コピー/移動コンストラクターが呼び出されない場合でも、(最適化がまったく行われていないかのように) 存在し、アクセス可能でなければなりません。
(C++11以上)
return ステートメントまたは throw 式で、コンパイラーがコピー省略を実行できないが、ソースが関数パラメーターであることを除いて、コピー省略の条件が満たされているか満たされる場合、コンパイラーは移動コンストラクターの使用を試みます。オブジェクトは左辺値によって指定されます。詳細については、return ステートメントを参照してください。
そしてそれについては、return ステートメントのメモで詳しく説明します。
ノート
値による戻りは、コピー省略が使用されない限り、一時オブジェクトの構築とコピー/移動を伴う場合があります。
(C++11以上)
がexpression
左辺値式であり、関数パラメーターの名前を除いて、コピー省略の条件が満たされるか、満たされる可能expression
性がある場合、戻り値の初期化に使用するコンストラクターを選択するためのオーバーロード解決が 2 回実行されますexpression
。右辺値式 (したがって、const を参照するムーブ コンストラクターまたはコピー コンストラクターを選択する場合があります)。適切な変換が利用できない場合は、左辺値式を使用して 2 回目のオーバーロードの解決が実行されます (したがって、参照を取るコピー コンストラクターを選択する場合があります)。非定数に)。
上記の規則は、関数の戻り値の型が の型と異なる場合でも適用されますexpression
(コピー省略には同じ型が必要です) 。
コンパイラは、複数のエリシオンを連鎖させることさえできます。つまり、中間オブジェクトを含む移動/コピーの 2 つの側面が、相互に直接参照される可能性があり、同じオブジェクトになる可能性さえあるということです。コンパイラがいつこれを行うことを選択するかはわかりませんし、知る必要もありません。これは最適化の 1 つですが、重要なことに、移動およびコピー コンストラクターなどは「最後の手段」の使用法と考える必要があります。
観察可能な動作が同じであれば、最適化における不要な操作の数を減らすことが目標であることに同意できます。Move コンストラクターと Copy コンストラクターは、Move操作と Copy 操作が発生する場所で使用されます。コンパイラが最適化として Move/Copy 操作自体を削除するのに適していると判断した場合はどうなるでしょうか。副作用のためだけに、機能的に不要な中間オブジェクトが最終プログラムに存在する必要がありますか? 現在の標準とコンパイラの方法は次のようです。いいえ - move および copy コンストラクターは、 whenまたはwhyではなく、これらの操作のhowを満たします。
短いバージョン: 最初に気にする必要のない一時的なオブジェクトが少ないため、それらを見逃す必要はありません。それらを見逃した場合、コードが中間コピーに依存しており、指定された目的やコンテキストを超えて何かを実行している可能性があります。
最後に、省略されたオブジェクトは、その開始場所ではなく、受信場所に常に格納 (および構築) されることに注意する必要があります。
このリファレンスを引用-
名前付き戻り値の最適化
関数がクラス型を値で返し、return ステートメントの式が、自動保存期間を持つ不揮発性オブジェクトの名前であり、関数パラメーターでも catch 句パラメーターでもなく、同じ型 (関数の戻り値の型としてトップレベルの cv-qualification を無視する場合、copy/move は省略されます。そのローカル オブジェクトが構築されると、関数の戻り値が移動またはコピーされるストレージに直接構築されます。このコピー省略の変種は、NRVO、「名前付き戻り値の最適化」として知られています。
戻り値の最適化
参照にバインドされていない名前のない一時オブジェクトが同じタイプのオブジェクトに移動またはコピーされる場合 (トップレベルの cv 修飾を無視)、コピー/移動は省略されます。その一時が構築されると、それが移動またはコピーされるストレージに直接構築されます。名前のない一時が return ステートメントの引数である場合、このコピー省略の変種は RVO (「戻り値の最適化」) として知られています。
参照の有効期間
すべきでないことの 1 つは、次のことです。
A & func() {
A result;
return result;
}
何かの暗黙的なコピーを回避できるので魅力的ですが (アドレスを渡すだけですよね?)、近視眼的なアプローチでもあります。上記のコンパイラが、このようなものを で防止していたことを覚えていtemp
ますか? ここでも同じです -result
を使い終わったら消えてしまいますfunc
。
できない理由は、result
out of にアドレスを渡すことができないためですfunc
(参照またはポインターとして)。それを有効なメモリと見なすことができません。もう気絶することはありませんA*
。
この状況では、オブジェクト コピーの戻り値の型を使用し、コンパイラが適切であると判断したときに発生する移動、省略、またはその両方に依存するのが最善です。コピー コンストラクターとムーブ コンストラクターは常に「最後の手段」と考えてください。コンパイラーはコピー操作とムーブ操作を完全に回避する方法を見つけることができるため、それらの使用をコンパイラーに依存するべきではありません。これらのコンストラクターの副作用はもう発生しません。
ただし、前述の特殊なケースがあります。
参照は実際の値への保証であることを思い出してください。これは、参照の最初の出現でオブジェクトが初期化され、最後に (コンパイル時にわかる限り) 範囲外になるとオブジェクトが破棄されることを意味します。
大まかに、これは 2 つの状況をカバーします: 関数からテンポラリを返す場合。関数の結果から代入するとき。最初の一時的なものを返すことは、基本的にエリシオンが行うことですが、呼び出しチェーンでポインターを渡すように、参照を渡すことで明示的に省略できます。リターン時にオブジェクトを構築しますが、変更点は、スコープを離れた後にオブジェクトが破棄されなくなったことです (return ステートメント)。もう一方の端では、2 番目の種類が発生します。関数呼び出しの結果を格納する変数は、スコープ外になったときに値を破棄するという名誉を持っています。
ここで重要な点は、省略と参照渡しが関連する概念であることです。初期化されていない変数の格納場所 (既知の型) へのポインターを使用して省略をエミュレートできます。
参照型のオーバーロード
参照を使用すると、非ローカル変数をローカル変数のように扱うことができます。アドレスを取得し、そのアドレスに書き込み、そのアドレスから読み取ることができます。また、重要なことに、適切なタイミングでオブジェクトを破棄できます。何にでも到達できなくなります。
通常の変数は、スコープを離れると、それらへの唯一の参照が消え、その時点ですぐに破棄されます。参照変数は通常の変数を参照できますが、省略/RVO の状況を除いて、元のオブジェクトのスコープには影響しません。参照先のオブジェクトが早期にスコープ外になったとしても、動的メモリへの参照を行うと発生する可能性があります。そして、それらの参照を自分で管理することに注意を払っていません。
これは、参照によって明示的に式の結果を取得できることを意味します。どのように?これは最初は奇妙に思えるかもしれませんが、上記を読めば、これが機能する理由が理解できるでしょう。
class A {
/* assume rule-of-5 (inc const-overloads) has been followed but unless
* otherwise noted the members are private */
public:
A (void) { /* ... */ }
A operator+ ( const A & rhs ) {
A res;
// do something with `res`
return res;
}
};
A x = A() + A(); // doesn't compile
A & y = A() + A(); // doesn't compile
A && z = A() + A(); // compiles
なんで?どうしたの?
A x = ...
- コンストラクターと代入がプライベートであるため、できません。
A & y = ...
-スコープが現在のスコープより大きいか等しい値への参照ではなく、値を返すため、できません。
A && z = ...
- xvalues を参照できるため、それが可能です。この割り当ての結果として、一時値の有効期間はこのキャプチャー左辺値まで延長されます。これは、実質的に左辺値参照になっているためです。おなじみですか?私がそれを何かと呼ぶなら、それは明示的な省略です。これは、この構文が新しい値を含む必要があり、その値を参照に割り当てる必要があることを考慮すると、より明白になります。
すべてのコンストラクターと代入が公開される 3 つのケースすべてでres
、結果を格納する変数と常に一致するアドレスを持つ、常に 3 つのオブジェクトのみが構築されます。(とにかく私のコンパイラでは、最適化は無効になっています、-std = gnu ++ 11、g ++ 4.9.3)。
つまり、実際の違いは、関数の引数自体の保存期間だけに帰着します。省略および移動操作は、純粋な式、期限切れの値、または「期限切れの値」参照オーバーロードの明示的なターゲット以外では発生しませんType&&
。
再検討f
し、g
両方の関数の状況に注釈を付けて、それぞれの (再利用可能な) コードを生成するときにコンパイラが注意する仮定のリストを作成しました。
A f( A && a ) {
// has storage duration exceeding f's scope.
// already constructed.
return a;
// can be elided.
// must be copy-constructed, a exceeds f's scope.
}
A g( A a ) {
// has storage duration limited to this function's scope.
// was just constructed somehow, whether by elision, move or copy.
return a;
// elision may occur.
// can move-construct if can't elide.
// can copy-construct if can't move.
}
について確かに言えることは、移動した値または式型の値を取得することを期待しているということですf
。a
なぜならf
、式参照 (prvalues) または消えそうな左辺値参照 (xvalues) または移動された左辺値参照 ( を介して xvalues に変換される) のいずれかを受け入れることができ、3 つのケースすべての の処理において同種でなければならないstd::move
ため、への呼び出しよりも寿命が長く存在するメモリ領域への最初の参照。つまり、呼び出した 3 つのケースのどれを内から区別することはできないため、コンパイラは、どのケースでも必要な最長のストレージ期間を想定し、のストレージ期間について何も想定しないことが最も安全であると判断します。f
a
a
f
f
f
a
のデータです。
の状況とは異なりg
ます。ここでは、a
-その値に応じて発生しますが-への呼び出しを超えてアクセスできなくなりますg
。この場合、xvalue と見なされるため、返すことは移動することと同じです。それをコピーすることも、おそらく削除することもできます。それは、その時点で何が許可/定義されているかによって異なりますA
。
の問題f
// we can't tell these apart.
// `f` when compiled cannot assume either will always happen.
// case-by-case optimizations can only happen if `f` is
// inlined into the final code and then re-arranged, or if `f`
// is made into a template to specifically behave differently
// against differing types.
A case_1() {
// prvalues
return f( A() + A() );
}
A make_case_2() {
// xvalues
A temp;
return temp;
}
A case_2 = f( make_case_2() )
A case_3(A & other) {
// lvalues
return f( std::move( other ) );
}
使用方法が曖昧であるため、コンパイラと標準はf
、すべての場合に一貫して使用できるように設計されています。常に新しい式である、またはその引数などにA&&
のみ使用するという仮定はありません。コードの外部に作成され、呼び出しシグネチャのみを残すと、それはもはや言い訳にはなりません。ターゲットへのオーバーロードを参照する関数シグネチャは、関数がそれを使用して何をすべきか、およびコンテキストについてどれだけ (または少し) 想定できるかの手がかりです。std::move
f
右辺値参照は、「移動された値」のみをターゲットにするための万能薬ではありません。より多くのものをターゲットにすることができ、それがすべてであると仮定すると、誤ってまたは予期せずにターゲットにされることさえあります。一般的に何かへの参照は、右辺値参照変数を除いて、参照よりも長く存在することが期待され、作成される必要があります。
右辺値参照変数は本質的に、省略演算子です。それらが存在する場所ではどこにでも、何らかの記述のインプレース構築が行われています。
通常の変数として、受け取る xvalue または rvalue のスコープを拡張します。これらは、移動やコピーではなく構築された式の結果を保持し、そこから使用する場合は通常の参照変数と同等です。
関数変数として、それらは省略してインプレースでオブジェクトを構築することもできますが、これには非常に重要な違いがあります:
A c = f( A() );
この:
A && r = f( A() );
違いは、c
がムーブ構築と省略されるという保証はありませんr
が、私たちがバインドしている . このためr
、新しい一時的な値が作成される状況でのみ割り当てることができます。
しかし、A&&a
捕獲された場合、なぜ破壊されないのでしょうか?
このことを考慮:
void bad_free(A && a) {
A && clever = std::move( a );
// 'clever' should be the last reference to a?
}
これはうまくいきません。理由は微妙です。a
のスコープはより長く、右辺値参照の割り当ては有効期間を延長することしかできず、それを制御することはできません。clever
より短い時間存在するためa
、 xvalue 自体ではありません (std::move
再度使用しない限り、同じ状況に戻って、それが続くなど)。
寿命延長
左辺値が右辺値と異なるのは、それ自体よりも寿命が短いオブジェクトにバインドできないことです。すべての左辺値参照は、元の変数か、元の変数よりも寿命が短い参照のいずれかです。
右辺値を使用すると、元の値よりも有効期間が長い参照変数へのバインドが可能になります。これで問題は半分です。検討:
A r = f( A() ); // v1
A && s = f( A() ); // v2
何が起こるのですか?どちらの場合もf
、呼び出しよりも長く存続する一時的な値が与えられ、結果オブジェクト (値によって返されるため) が何らかのf
方法で構築されます (後で説明するように、これは重要ではありません)。v1 では、一時的な結果を使用して新しいオブジェクトを構築しています。これは、移動、コピー、削除の 3 つの方法で行うことができます。v2 では、新しいオブジェクトを構築するのではなく、 の結果の有効期間を のスコープに拡張します。代わりに、同じことを言います。コピーしました。r
f
s
s
f
f
主な違いは、v1では、プロセスが省略されている場合でも、移動コンストラクターとコピー コンストラクター (少なくとも 1 つ) を定義する必要があることです。v2の場合、コンストラクターを呼び出しておらず、一時的な値の有効期間を参照および/または延長したいと明示的に言っています.moveまたはcopyコンストラクターを呼び出さないため、コンパイラーはインプレースでのみ省略/構築できます!
これは に与えられた引数とは何の関係もないことに注意してくださいf
。それは以下と同じように動作しg
ます:
A r = g( A() ); // v1
A && s = g( A() ); // v2
g
引数のテンポラリを作成しA()
、両方のケースを使用して移動構築します。またf
、戻り値のテンポラリも構築しますが、結果はテンポラリ (テンポラリ to g
) を使用して構築されるため、xvalue を使用できます。繰り返しますが、これは問題ではありません。なぜなら、v1 では、コピー構築またはムーブ構築 (どちらか一方が必要ですが、両方ではない) が可能な新しいオブジェクトがあるのに対し、v2 では、構築されたものへの参照を要求していますが、そうしないと消えてしまうからです。つかまらない。
明示的な xvalue キャプチャ
これが理論上可能であることを示す例 (ただし役に立たない):
A && x (void) {
A temp;
// return temp; // even though xvalue, can't do this
return std::move(temp);
}
A && y = x(); // y now refers to temp, which is destroyed
どのオブジェクトy
を参照していますか? コンパイラには選択の余地がありません。y
何らかの関数または式の結果を参照する必要があり、temp
型に基づいて機能するものを指定しました。しかし、移動は行われておらず、temp
を介して使用するまでに割り当てが解除されますy
。
/の場合のtemp
ようにa
、有効期間の延長が開始されなかったのはなぜですか? 返されるもののために、その場で構築する関数を指定することはできませんが、その場で構築される変数を指定できます。また、コンパイラが関数/呼び出しの境界を越えて寿命を決定するのではなく、呼び出し側またはローカルにある変数、それらがどのように割り当てられているか、ローカルの場合はどのように初期化されているかを調べるだけであることも示しています。g
f
すべての疑問を解消したい場合は、これを右辺値参照として渡してみてくださいstd::move(*(new A))
: (つまり、中間体/式)。xvalues は移動構築/移動代入の候補であり、省略できません (既に構築されています) が、他のすべての移動/コピー操作は理論上、コンパイラの気まぐれで省略できます。右辺値参照を使用する場合、コンパイラはアドレスを省略するか渡すしかありません。