web-dev-qa-db-ja.com

C ++の "return"の正確な瞬間 - 関数

それはばかげた質問のように思えますが、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が呼ばれている間に私が素朴に起こることを期待しているもの:

  1. resのコンストラクタが呼び出されます(resの値は"A"です)
  2. wのコンストラクタが呼び出される
  3. return resが実行されます。 resの現在の値(resの現在の値をコピーすることによって)、すなわち"A"を返す必要があります。
  4. wのデストラクタが呼び出されると、resの値は"AB"になります。
  5. 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を参照 )。

規格では、上記の例でどの値を返すべきかを規定していますか、それとも未指定ですか。

66
ead

これはRVO(+コピーを一時的なものとして返すためにコピーが返される)です。これは、表示される動作を変更できるようにするための最適化の1つです。

10.9.5コピー/移動の選択(強調は私のものです)

特定の基準が満たされると、実装は、クラスオブジェクトのコピー/移動構築を省略することができます、たとえコピー/移動操作のために選択されたコンストラクタやオブジェクトのデストラクタがsideを持っていても効果**。そのような場合、実装は省略されたコピー/移動操作のソースとターゲットを同じオブジェクトを参照する2つの異なる方法として扱うを扱います。

コピーの削除と呼ばれるこのコピー/移動操作の削除は、次の状況で許可されます(複数のコピーを削除するために組み合わせることができます)。

  • クラス戻り型を持つ関数のreturn文の中で式が不揮発性自動オブジェクトの名前である場合(関数パラメータまたはの宣言宣言によって導入された変数を除く) handler)関数の戻り型と同じ型(cv-qualificationを無視)の場合、自動オブジェクトを関数呼び出しの戻りオブジェクトに直接構築することでコピー/移動操作を省略できます。
  • [...]

それが適用されたかどうかに基づいてあなたの全体の前提が悪くなる。 1ではresのc'torが呼び出されますが、オブジェクトはmake_string_okの内側または外側に存在する可能性があります。

ケース1.

箇条書き2と3はまったく起こらないかもしれませんが、これはサイドポイントです。ターゲットは、影響を受けるWriters副作用があり、make_string_okの外側にありました。これはたまたま評価operator<<(ostream, std::string)のコンテキストでmake_string_okを使って作成された一時的なものです。コンパイラは一時値を作成してから関数を実行しました。一時的にその外側に住んでいるので、これは重要です。したがって、Writerのターゲットはmake_string_okに対してローカルではなく、operator<<に対してローカルです。

ケース2.

一方、2番目の例は、型が異なるため、基準には適合しません(簡潔にするために省略したものも適合しません)。だから作家は死にます。それがpairの一部であるならば、それも死にます。そのため、ここではres.firstのコピーが一時オブジェクトとして返され、Writerのdtorが元のres.firstに影響を及ぼします。

Copyによって返されるオブジェクトも破棄されるので、デストラクタを呼び出す前にコピーが作成されることは明らかです。そうでなければコピーすることはできません。

Writerのd'torは、最適化が適用されているかどうかに応じて、外側のオブジェクトまたはローカルのオブジェクトに対して機能するため、結局のところRVOになります。

規格では、上記の例でどの値を返すべきかを規定していますか、それとも未指定ですか。

いいえ、最適化はオプションですが、観察可能な動作が変わる可能性があります。適用するかしないかは、コンパイラの裁量に任されています。コンパイラが観察可能な振る舞いを変えないような変換を行うことを許可されているという「一般的なas-if」規則からの免除です。

それについてのケースはc ++ 17で必須になりました、しかしあなたのものではありません。必須のものは戻り値が名前のない一時的なものであるところです。

27
luk32

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つのオブジェクトが破壊されたままになります。

36
Shloim

C++にはelisionという概念があります。

Elisionは、見かけ上異なる2つのオブジェクトを受け取り、それらのアイデンティティと存続期間をマージします。

c ++ 17 の前には、次のような問題が発生する可能性があります。

  1. Fooを返す関数内に非パラメータ変数Foo f;があり、return文が単純なreturn f;であった場合。

  2. 匿名のオブジェクトが他のほとんどのオブジェクトの構築に使用されているとき。

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_livedFoo short_livedの両方を排除することは合法ですが、両方のオブジェクトが両方の寿命をfuncの戻り値とマージすることはできないため、実装上の問題により基本的に不可能です。 short_livedlong_livedを一緒に排除することは合法ではなく、それらの寿命は重なります。

あなたはまだas-ifの下でそれを行うことができますが、あなたがデストラクタ、コンストラクタおよび.futz()のすべての副作用を調べて理解することができる場合に限ります。