それはばかげた質問のように思えますが、return xxx;
が明確に定義された関数の中で「実行」される正確な瞬間ですか?
次の例を参考にしてください( ここに住んでいる )。
#include <iostream>
#include <string>
#include <utility>
//changes the value of the underlying buffer
//when destructed
class Writer{
public:
std::string &s;
Writer(std::string &s_):s(s_){}
~Writer(){
s+="B";
}
};
std::string make_string_ok(){
std::string res("A");
Writer w(res);
return res;
}
int main() {
std::cout<<make_string_ok()<<std::endl;
}
make_string_ok
が呼ばれている間に私が素朴に起こることを期待しているもの:
res
のコンストラクタが呼び出されます(res
の値は"A"
です)w
のコンストラクタが呼び出されるreturn res
が実行されます。 resの現在の値(res
の現在の値をコピーすることによって)、すなわち"A"
を返す必要があります。w
のデストラクタが呼び出されると、res
の値は"AB"
になります。res
のデストラクタが呼び出されます。だから私は"A"
を結果として期待するでしょう、しかし"AB"
をコンソールに印刷されるでしょう。
一方、make_string
のわずかに異なるバージョンの場合:
std::string make_string_fail(){
std::pair<std::string, int> res{"A",0};
Writer w(res.first);
return res.first;
}
結果は予想通りです - "A"
( liveを参照 )。
規格では、上記の例でどの値を返すべきかを規定していますか、それとも未指定ですか。
これはRVO(+コピーを一時的なものとして返すためにコピーが返される)です。これは、表示される動作を変更できるようにするための最適化の1つです。
10.9.5コピー/移動の選択(強調は私のものです):
特定の基準が満たされると、実装は、クラスオブジェクトのコピー/移動構築を省略することができます、たとえコピー/移動操作のために選択されたコンストラクタやオブジェクトのデストラクタがsideを持っていても効果**。そのような場合、実装は省略されたコピー/移動操作のソースとターゲットを同じオブジェクトを参照する2つの異なる方法として扱うを扱います。
コピーの削除と呼ばれるこのコピー/移動操作の削除は、次の状況で許可されます(複数のコピーを削除するために組み合わせることができます)。
- クラス戻り型を持つ関数のreturn文の中で式が不揮発性自動オブジェクトの名前である場合(関数パラメータまたはの宣言宣言によって導入された変数を除く) handler)関数の戻り型と同じ型(cv-qualificationを無視)の場合、自動オブジェクトを関数呼び出しの戻りオブジェクトに直接構築することでコピー/移動操作を省略できます。
- [...]
それが適用されたかどうかに基づいてあなたの全体の前提が悪くなる。 1ではres
のc'torが呼び出されますが、オブジェクトはmake_string_ok
の内側または外側に存在する可能性があります。
箇条書き2と3はまったく起こらないかもしれませんが、これはサイドポイントです。ターゲットは、影響を受けるWriter
s副作用があり、make_string_ok
の外側にありました。これはたまたま評価operator<<(ostream, std::string)
のコンテキストでmake_string_ok
を使って作成された一時的なものです。コンパイラは一時値を作成してから関数を実行しました。一時的にその外側に住んでいるので、これは重要です。したがって、Writer
のターゲットはmake_string_ok
に対してローカルではなく、operator<<
に対してローカルです。
一方、2番目の例は、型が異なるため、基準には適合しません(簡潔にするために省略したものも適合しません)。だから作家は死にます。それがpair
の一部であるならば、それも死にます。そのため、ここではres.first
のコピーが一時オブジェクトとして返され、Writer
のdtorが元のres.first
に影響を及ぼします。
Copyによって返されるオブジェクトも破棄されるので、デストラクタを呼び出す前にコピーが作成されることは明らかです。そうでなければコピーすることはできません。
Writer
のd'torは、最適化が適用されているかどうかに応じて、外側のオブジェクトまたはローカルのオブジェクトに対して機能するため、結局のところRVOになります。
いいえ、最適化はオプションですが、観察可能な動作が変わる可能性があります。適用するかしないかは、コンパイラの裁量に任されています。コンパイラが観察可能な振る舞いを変えないような変換を行うことを許可されているという「一般的なas-if」規則からの免除です。
それについてのケースはc ++ 17で必須になりました、しかしあなたのものではありません。必須のものは戻り値が名前のない一時的なものであるところです。
Return Value Optimization(RVO) のため、std::string res
内のmake_string_ok
のデストラクタは呼び出せません。 string
オブジェクトは呼び出し側で構築でき、関数は値を初期化するだけです。
コードは以下と同等になります。
void make_string_ok(std::string& res){
Writer w(res);
}
int main() {
std::string res("A");
make_string_ok(res);
}
そのため、戻り値は "AB"になります。
2番目の例では、RVOは適用されず、値はreturnの呼び出し時に返される値に正確にコピーされ、Writer
のデストラクタはコピーが行われた後にres.first
上で実行されます。
6.6ジャンプ文
スコープから出ると(ただし実行されると)、そのスコープ内で宣言されている自動格納期間(3.7.2)を持つすべての構築済みオブジェクトに対して、宣言の逆の順序でデストラクタ(12.4)が呼び出されます。ループからの転送、ブロックからの転送、または自動保存期間を持つ初期化された変数への転送は、転送元の時点で有効範囲にある自動保存期間を持つ変数の破棄を伴います。
...
6.6.3 returnステートメント
返された実体のコピー初期化はreturn文のオペランドによって確立された全式の終わりでの一時的なものの破壊の前に順序付けされます。 return文を囲むブロック.
...
12.8クラスオブジェクトのコピーと移動
ある基準が満たされるとき、たとえオブジェクトのためのコピー/移動コンストラクタやデストラクタが副作用を持っていても、実装はクラスオブジェクトのコピー/移動コンストラクトを省略することが許されます。そのような場合、実装は省略されたコピー/移動操作のソースとターゲットを単に同じオブジェクトを参照する2つの異なる方法として扱い、そのオブジェクトの破棄は2つのオブジェクトが破棄された後の時点で行われます。 (123)コピーの削除と呼ばれるこのコピー/移動操作の削除は、次の状況で許可されます(複数のコピーを削除するために組み合わせることができます)。
- クラスの戻り型がある関数のreturn文で、式が関数の戻り型と同じcvunqualified型の不揮発性自動オブジェクト(関数またはcatch節パラメータ以外)の名前である場合自動オブジェクトを関数の戻り値に直接構築することで、コピー/移動操作を省略できます。
123)2つではなく1つのオブジェクトしか破壊されず、1つのコピー/移動コンストラクタが実行されないため、構築された各オブジェクトに対して1つのオブジェクトが破壊されたままになります。
C++にはelisionという概念があります。
Elisionは、見かけ上異なる2つのオブジェクトを受け取り、それらのアイデンティティと存続期間をマージします。
c ++ 17 の前には、次のような問題が発生する可能性があります。
Foo
を返す関数内に非パラメータ変数Foo f;
があり、return文が単純なreturn f;
であった場合。
匿名のオブジェクトが他のほとんどのオブジェクトの構築に使用されているとき。
c ++ 17 において#2のすべての(ほとんど?)のケースは新しいprvalueの規則によって排除されます。一時オブジェクトの作成に使用されていたものが発生しなくなったため、elisionは発生しなくなりました。代わりに、 "一時的な"オブジェクトの構築は、恒久的なオブジェクトの場所に直接結び付けられます。
さて、コンパイラがコンパイルするABIを考えると、elisionは必ずしも可能ではありません。それが可能である2つの一般的なケースは戻り値最適化と名前付き戻り値最適化として知られています。
RVOはこのような場合です。
Foo func() {
return Foo(7);
}
Foo foo = func();
ここでは戻り値Foo(7)
が返され、その値は外部変数foo
に渡されます。 3つのオブジェクト(foo()
の戻り値、return
行の値、およびFoo foo
)に見えるものは、実際には実行時に1です。
c ++ 17 より前では、コピー/移動コンストラクタはここに存在しなければならず、省略はオプションです。 in c ++ 17 新しいprvalueの規則のために、コピー/移動の構築は必要なく、コンパイラにはオプションがありません。ここでは1の値が必要です。
もう1つの有名なケースは戻り値最適化、NRVOです。これは上記の(1)の脱落の場合です。
Foo func() {
Foo local;
return local;
}
Foo foo = func();
繰り返しますが、elisionはFoo local
の存続期間とID、func
からの戻り値、およびfunc
の外側のFoo foo
をマージすることができます。
c ++ 17 でも、2番目のマージ(func
の戻り値とFoo foo
の間)はオプションではありません(技術的には、func
から返されるprvalueは決してオブジェクトではなく、単なる式です。 Foo foo
)を作成しますが、最初のものはオプションのままで、移動またはコピーコンストラクターが存在する必要があります。
たとえそれらのコピー、破壊、構造を排除しても観察可能な副作用があるとしても、Elisionは起こりうる規則です。これは、「as-if」最適化ではありません。その代わりに、素朴な人がC++コードが意味するとは思わないかもしれない微妙な変化です。それを「最適化」と呼ぶのは、ちょっとした誤称です。
それがオプションであり、微妙なことがそれを打破することができるという事実はそれに関する問題です。
Foo func(bool b) {
Foo long_lived;
long_lived.futz();
if (b)
{
Foo short_lived;
return short_lived;
}
return long_lived;
}
上記の場合、コンパイラがFoo long_lived
とFoo short_lived
の両方を排除することは合法ですが、両方のオブジェクトが両方の寿命をfunc
の戻り値とマージすることはできないため、実装上の問題により基本的に不可能です。 short_lived
とlong_lived
を一緒に排除することは合法ではなく、それらの寿命は重なります。
あなたはまだas-ifの下でそれを行うことができますが、あなたがデストラクタ、コンストラクタおよび.futz()
のすべての副作用を調べて理解することができる場合に限ります。