7

クラスAと関数がある場合

A f(A &&a)
{
  doSomething(a);
  return a;
}
A g(A a)
{
  doSomething(a);
  return a;
}

から戻るときはコピー コンストラクタが呼び出されますが、aから戻るfときはムーブ コンストラクタが使用されgます。ただし、私が理解していることから、f安全に移動できるオブジェクトのみを渡すことができます (一時オブジェクトまたは移動可能としてマークされたオブジェクトのいずれか、たとえば を使用std::move)。から戻るときに移動コンストラクターを使用するのが安全ではない例はありますfか? なぜa自動保存期間が必要なのですか?

ここで回答を読みましたが、一番上の回答aは、関数本体の他の関数に渡すときに仕様が移動を許可してはならないことを示しているだけです。戻るときに移動することが安全である理由を説明していませgf。return ステートメントにたどり着いたら、 のa内部はもう必要ありませんf

更新 0

したがって、完全な式の終わりまで一時的にアクセスできることを理解しています。ただし、f静止から戻ったときの動作は、一時または xvalue を移動しても安全であるという言語に根付いたセマンティクスに反しているようです。たとえば、 を呼び出すと、どこかに保存されている一時ファイルへの参照が存在する可能性がありますが、一時ファイルはg(A())for の引数に移動されます。xvaluegで呼び出した場合も同じことが起こります。一時値と xvalue のみが右辺値参照にバインドされるため、一時値または xvalue のいずれかが渡されたことがわかっているため、 から戻るときにg移動する必要があるセマンティクスについては一貫しているようです。afa

4

3 に答える 3

7

二度目の試み。うまくいけば、これはより簡潔で明確です。

この議論では、RVO をほぼ完全に無視します。これは、最適化なしで何が起こるべきかについて本当に混乱させます-これは、移動とコピーのセマンティクスに関するものです。

これを支援するために、 c++11 の値の種類の種類に関する参照がここで非常に役立ちます。

いつ移動しますか?

左辺値

これらは移動されません。それらは、他の場所で参照されている可能性のある変数またはストレージの場所を参照しているため、その内容を別のインスタンスに転送するべきではありません。

価格

上記では、それらを「アイデンティティを持たない式」と定義しています。明らかに、名前のない値を参照できるものは他にないため、これらを移動できます。

右辺値

「右側」の値の一般的なケースであり、唯一確かなことは、それらを移動できることです。名前付き参照がある場合とない場合がありますが、ある場合は、そのような最後の使用法です。

x値

これらは両方の混合のようなものです。それらにはアイデンティティがあり (参照であり) 移動できます。名前付き変数を持つ必要はありません。理由?それらは期限切れの値であり、破壊されようとしています。それらを「最終参照」と考えてください。xvalues は rvalues からのみ生成できます。std::moveこれは、(関数呼び出しの結果を通じて) lvalues を xvalues に変換する際になぜ/どのように機能するかです。

glvalue

右辺値のいとこを持つ別のミュータント型で、xvalue または lvalue のいずれかになります。ID はありますが、これが変数 / ストレージへの最後の参照であるかどうかは不明です。したがって、移動できるかどうかは不明です。 .

解決順序

const lvalue refaまたはのいずれかを受け入れることができるオーバーロードが存在しrvalue ref、右辺値が渡される場合、右辺値はバインドされます。それ以外の場合は、左辺値バージョンが使用されます。(右辺値の場合は移動、それ以外の場合はコピー)。

発生する可能性がある場所

A(すべてのタイプが言及されていない場所であると仮定します)

オブジェクトが「同じタイプの xvalue から初期化される」場合にのみ発生します。xvalues は右辺値にバインドされますが、純粋な式ほど制限されません。言い換えれば、移動可能なものは名前のない参照以上のものであり、コンパイラの認識に関してオブジェクトへの「最後の」参照になることもあります。

初期化

A a = std::move(b); // assign-move
A a( std::move(b) ); // construct-move

関数の引数の受け渡し

void f( A a );
f( std::move(b) );

関数リターン

A f() {
    // A a exists, will discuss shortly
    return a;
}

なぜそれが起こらないのかf

f の次のバリエーションを考えてみましょう。

void action1(A & a) {
    // alter a somehow
}

void action2(A & a) {
    // alter a somehow
}

A f(A && a) {
    action1( a );
    action2( a );
    return a;
}

a内で左辺値として扱うことは違法ではありませんf。これは であるためlvalue、明示的であるかどうかにかかわらず、参照である必要があります。すべての単純な変数は、技術的にはそれ自体への参照です。

それが私たちがつまずくところです。aは の目的のための左辺値であるため、f実際には左辺値を返しています。

右辺値を明示的に生成するには、使用するstd::move(またはA&&別の方法で結果を生成する) 必要があります。

なぜそれが起こるのかg

それを私たちのベルトの下に置いて、検討してくださいg

A g(A a) {
    action1( a ); // as above
    action2( a ); // as above
    return a;
}

はい、とaの目的のための左辺値です。ただし、へのすべての参照は内部にのみ存在するため(コピーまたは移動されたコピー)、返される xvalue と見なすことができます。action1action2ag

しかし、なぜではないのfですか?

に特定の魔法はありません&&。本当に、まず第一にそれを参照として考えるべきです。左辺値参照とは対照的に右辺参照を要求しているという事実は、参照であるため、左辺値でなければならないという事実を変更しません.心配します。fA&af

のストレージは一時的なものであり、が呼び出されたときだけ存在し、それ以外のときは存在しないことはg明らかです。この場合、それは明らかに xvalue であり、移動できます。ag


rvalue refvslvalue refと参照渡しの安全性

両方のタイプの参照を受け入れる関数をオーバーロードするとします。何が起こるでしょうか?

void v( A  & lref );
void v( A && rref );

唯一の時間void v( A&& )は、上記 (「発生する可能性がある場所」) に従って使用されます。それ以外の場合はvoid v( A& ). つまり、右辺値参照は、左辺値参照のオーバーロードが試行される前に、常に右辺値参照署名にバインドしようとします。左辺値参照は、xvalue として扱うことができる場合を除いて、右辺値参照にバインドするべきではありません (現在のスコープで破棄することが保証されています)。

右辺値の場合、渡されるオブジェクトが一時的なものであることは確実にわかっていると言いたくなる。そうではありません。これは、一時オブジェクトのように見えるものへの参照をバインドすることを目的とした署名です。

類推のために、それはやっているようなものですint * x = 23;-それは間違っているかもしれませんが、実行すると(最終的に)悪い結果で強制的にコンパイルされる可能性があります。コンパイラは、あなたがそれについて真剣に考えているのか、それとも足を引っ張っているのかを確実に言うことはできません.

安全性に関しては、これを行う関数を考慮する必要があります (そして、これを行うべきではない理由 - それでもコンパイルできる場合):

A & make_A(void) {
    A new_a;
    return new_a;
}

言語の側面に表面上は何も問題はありませんが、型は機能し、どこかへの参照を取得します-new_aの格納場所は関数内にあるため、関数が戻るとメモリが再利用/無効になります。したがって、この関数の結果を使用するものはすべて、解放されたメモリを処理します。

同様に、A f( A && a )他の何かを本当に強制したい場合は、prvalues または xvalues を受け入れることを意図していますが、それに限定されません。そこでstd::move出番です。それをやりましょう。

A f( A & a ) これが当てはまる理由は、右辺値オーバーロードよりも優先されるコンテキストに関してのみ異なるためです。他のすべての点ではa、コンパイラによる の処理方法は同じです。

それが動きのために予約された署名であることがわかっているという事実は、議論の余地があります。A&&これはA、バインドする「-type パラメーターへの参照」のバージョン、所有権を取得する必要がある並べ替え (右辺値)、または基になるデータの所有権を取得すべきでない並べ替え(左辺値) を決定するために使用されます (つまり、 、それを別の場所に移動し、指定されたインスタンス/参照を消去します)。どちらの場合も、処理しているのはによって制御されないメモリへの参照fです。

実行するかどうかは、コンパイラが判断できるものではありません。使用する意味がないが有効なメモリ位置であるメモリ位置を使用しないなど、プログラミングの「常識」領域に該当します。

コンパイラが知っていることは、作業するアドレス (参照) が与えられるA f( A && a )ため、 の新しいストレージを作成しないことです。aソースアドレスをそのままにしておくことを選択できますが、ここでの全体的な考え方は、宣言A&&することで、コンパイラに「ちょっと、消えようとしているオブジェクトへの参照を与えてください。そうすれば、その前に何かできるかもしれません。起こります」。ここでのキーワードはmightであり、この関数シグネチャを明示的に対象とすることが間違っている可能性があるという事実です。

Amove-constructing 時に古いインスタンスのデータを消去せず、何らかの理由でこれを設計で行ったバージョンがあるかどうかを検討してください (独自のメモリ割り当て関数があり、メモリ モデルがどのように保持されるかを正確に知っているとしましょう)オブジェクトの存続期間を超えたデータ)。

オブジェクトが右辺値バインディングで処理されたときにオブジェクトに何が起こるかを判断するにはコード分析が必要になるため、コンパイラはこれを知ることができません。その時点では人間の判断の問題です。せいぜい、コンパイラは「参照、イェーイ、ここに余分なメモリを割り当てない」ことを認識し、参照渡しのルールに従います。

コンパイラが次のように考えていると仮定するのは安全です。「これは参照です。内部のメモリの有効期間を処理する必要はありません。f一時的なものであるため、f終了後に削除されます」。

その場合、テンポラリが に渡されるfと、そのテンポラリのストレージは を離れるとすぐに消えてしまい、非常に悪いf状況と同じ状況になる可能性があります。A & make_A(void)

セマンティクスの問題...

std::move

の目的std::moveは、右辺値参照を作成することです。概して、それが行うことは (他に何もない場合)、結果の値を左辺値ではなく右辺値に強制的にバインドすることです。この理由は、A&右辺値参照が使用可能になる前のリターン シグネチャであり、演算子のオーバーロード (およびその他の用途) などについてはあいまいでした。

演算子 - 例

class A {
    // ...
  public:
    A & operator= (A & rhs); // what is the lifetime of rhs? move or copy intended?
    A & operator+ (A & rhs); // ditto
    // ...
};

int main() {
    A result = A() + A(); // wont compile!
}

これは、どちらのオペレーターの一時オブジェクトも受け入れないことに注意してください。また、オブジェクトのコピー操作の場合にこれを行うことは意味がありません。おそらく後で変更できるコピーを作成するために、コピーしている元のオブジェクトを変更する必要があるのはなぜですか。これが、元のオブジェクトを変更していないことを保証するために、コピー演算子のパラメーターを宣言する必要がある理由と、参照のコピーを作成する必要があるすべての状況です。const A &

当然、これは移動の問題であり、元のオブジェクトを変更して、新しいコンテナーのデータが時期尚早に解放されないようにする必要があります。(したがって、「移動」操作)。

この混乱を解決するためにT&&、宣言が付属しています。これは、上記のコード例を置き換えるものであり、具体的には、上記のコードがコンパイルされない状況でオブジェクトへの参照を対象としています。しかし、移動操作になるように変更する必要はなく、operator+そうする理由を見つけるのは難しいでしょう (とは思いますが)。繰り返しますが、加算によって元のオブジェクトが変更されるべきではなく、式の左オペランド オブジェクトのみが変更されるという前提のためです。したがって、これを行うことができます:

class A {
    // ...
  public:
    A & operator= (const A & rhs); // copy-assign
    A & operator= (A && rhs); // move-assign
    A & operator+ (const A & rhs); // don't modify rhs operand
    // ...
};

int main() {
    A result = A() + A(); // const A& in addition, and A&& for assign
    A result2 = A().operator+(A()); // literally the same thing
}

ここで注意する必要があるのは、 がテンポラリを返すという事実にもかかわらず、バインドできるだけでなく、予想れる加算のセマンティクス (右側のオペランドを変更しない) のためにバインドできることです。割り当ての 2 番目のバージョンは、引数の 1 つだけを変更する必要がある理由をより明確にしています。A()const A&

また、割り当てで移動が発生しrhs、 in では移動が発生しないことも明らかですoperator+

戻り値のセマンティクスと引数バインディングのセマンティクスの分離

上記の移動が 1 つしかない理由は、関数 (まあ、演算子) の定義から明らかです。重要なことは、明らかに xvalue / rvalue であるものを、紛れもなく lvalue であるものに実際にバインドしていることですoperator+

この点を強調しなければなりません。この例には、彼らの議論operator+を参照するという点で実質的な違いはありません。operator=コンパイラに関する限り、どちらの関数本体内でも引数は効果的const A&に for+およびA&for=です。違いは純粋にconstネスにあります。A&A&&が異なる唯一の方法は、型ではなくシグネチャを区別することです。

シグネチャが異なればセマンティクスも異なります。これは、コードと明確に区​​別できない特定のケースを区別するためのコンパイラのツールキットです。関数自体の動作 (コード本体) も、ケースを区別できない場合があります。

この別の例はoperator++(void)vsoperator++(int)です。前者はインクリメント操作の前に基になる値を返し、後者はその後に返すことを期待しています。渡されることはありませんint。コンパイラが 2 つのシグネチャを使用できるようにするためです。同じ名前の 2 つの同一の関数を指定する方法は他にありません。ご存知かもしれませんが、関数をオーバーロードすることは違法です。あいまいさの同様の理由から、戻り値の型だけで。

右辺値変数とその他の奇妙な状況 - 徹底的なテスト

何が起こっているのかを明確に理解するために、f私は「試みるべきではないが、うまくいくように見える」ものをまとめました。

void bad (int && x, int && y) {
  x += y;
}
int & worse (int && z) {
  return z++, z + 1, 1 + z;
}
int && justno (int & no) {
  return worse( no );
}
int num () {
  return 1;
}
int main () {
  int && a = num();
  ++a = 0;
  a++ = 0;
  bad( a, a );
  int && b = worse( a );
  int && c = justno( b );
  ++c = (int) 'y';
  c++ = (int) 'y';
  return 0;
}

g++ -std=gnu++11 -O0 -Wall -c -fmessage-length=0 -o "src\\basictest.o" "..\\src\\basictest.cpp"

..\src\basictest.cpp: In function 'int& worse(int&&)':
..\src\basictest.cpp:5:17: warning: right operand of comma operator has no effect [-Wunused-value]
   return z++, z + 1, 1 + z;
                 ^
..\src\basictest.cpp:5:26: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
   return z++, z + 1, 1 + z;
                          ^
..\src\basictest.cpp: In function 'int&& justno(int&)':
..\src\basictest.cpp:8:20: error: cannot bind 'int' lvalue to 'int&&'
   return worse( no );
                    ^
..\src\basictest.cpp:4:7: error:   initializing argument 1 of 'int& worse(int&&)'
 int & worse (int && z) {
       ^
..\src\basictest.cpp: In function 'int main()':
..\src\basictest.cpp:16:13: error: cannot bind 'int' lvalue to 'int&&'
   bad( a, a );
             ^
..\src\basictest.cpp:1:6: error:   initializing argument 1 of 'void bad(int&&, int&&)'
 void bad (int && x, int && y) {
      ^
..\src\basictest.cpp:17:23: error: cannot bind 'int' lvalue to 'int&&'
   int && b = worse( a );
                       ^
..\src\basictest.cpp:4:7: error:   initializing argument 1 of 'int& worse(int&&)'
 int & worse (int && z) {
       ^
..\src\basictest.cpp:21:7: error: lvalue required as left operand of assignment
   c++ = (int) 'y';
       ^
..\src\basictest.cpp: In function 'int& worse(int&&)':
..\src\basictest.cpp:6:1: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^
..\src\basictest.cpp: In function 'int&& justno(int&)':
..\src\basictest.cpp:9:1: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^

01:31:46 Build Finished (took 72ms)

これは変更されていない出力 sans ビルド ヘッダーであり、表示する必要はありません :) 見つかったエラーを理解するための演習として残しますが、自分の説明を読み直すと (特に以下の説明で)、各エラーが何であるかが明らかになるはずです。原因と理由、ともかく。

結論 - このことから何を学べるでしょうか?

まず、コンパイラは関数本体を個別のコード単位として扱うことに注意してください。これが基本的にここの鍵です。コンパイラーが関数本体に対して何を行うにしても、関数本体を変更する必要がある関数の動作について仮定することはできません。これらのケースに対処するためにテンプレートがありますが、それはこの議論の範囲を超えています.テンプレートは異なるケースを処理するために複数の関数本体を生成することに注意してください.

第 2 に、右辺値型は主に移動操作のために想定されていました。これは、オブジェクトの代入と構築で発生すると予想される非常に特殊な状況です。右辺値参照バインディングを使用するその他のセマンティクスは、処理するコンパイラの範囲を超えています。言い換えれば、右辺値参照は実際のコードよりもシンタックス シュガーと考えたほうがよいということです。署名はA&&vsで異なりA&ますが、関数本体の目的のための引数の型は異なりません。渡されるオブジェクトを何らかの方法で変更する必要A&があるという意図で常に扱われます。構文的には正しいが、目的の動作を許可しないためです.const A&

この時点で、コンパイラーfが宣言されているかのようにコード本体を生成すると確信できますf(A&)。上記のように、変更可能な参照のバインドをいつ許可するA&&かをコンパイラが選択するのを支援しますが、それ以外の場合、コンパイラは のセマンティクスを考慮せず、を返すものとは異なります。ff(A&)f(A&&)f

長い言い方です: の return メソッドは、f受け取る引数の型に依存しません。

混乱は省略です。実際には、値の戻り値には 2 つのコピーがあります。最初にコピーが一時的なものとして作成され、次にこの一時的なものが何かに割り当てられます (または割り当てられず、純粋に一時的なままです)。2 番目のコピーは、リターンの最適化によって省略される可能性が非常に高いです。最初のコピーは に移動できますが、 には移動gできませんff省略できない状況でfは、元のコードからコピーしてから移動することを期待しています。

これをオーバーライドするにstd::moveは、 を使用して、つまり の return ステートメントで一時を明示的に構築する必要がありますf。ただし、gの関数本体に一時的であることがわかっているものを返しているgため、2 回移動するか、1 回移動してから省略します。

すべての最適化を無効にして元のコードをコンパイルし、診断メッセージを追加してコンストラクターをコピーおよび移動し、省略が要因になる前に値がいつどこで移動またはコピーされるかを監視することをお勧めします。私が間違っていたとしても、使用されたコンストラクター/操作の最適化されていないトレースは、コンパイラーが行ったことの明確な図を描くでしょう。

于 2016-06-17T07:10:33.803 に答える
0

短編小説: のみに依存しdoSomethingます。

doSomething ミディアムストーリー:変わらなけれaf安全です。右辺値参照を受け取り、そこから移動された新しい一時を返します。

長い話: a が return ステートメントで使用される前に未定義の状態にある可能性があるため、移動操作でas をdoSomething使用するとすぐに事態は悪化します - それは同じですが、少なくとも右辺値参照への変換は明示的ag

TL/DR:内で移動操作がない限り、fとの両方が安全です。違いは、Move は ではサイレントに実行されるのに対し、g では(たとえば を使用して) 右辺値参照への明示的な変換が必要になることです。gdoSomethingfstd::move

于 2016-06-23T07:44:36.497 に答える
0

3回目の試み。2番目は、状況の隅々まで説明する過程で非常に長くなりました. でもねえ、私もその過程で多くのことを学びました。それがポイントだと思いますよね?:) ともかく。それ自体は有用な参考資料ですが、「明確な説明」には及ばないため、長い回答をそのままにして、質問を新たに取り上げます。

ここで何を扱っているのですか?

fg些細な状況ではありません。彼らは、あなたが彼らに出会った最初の数回を理解し、感謝するのに時間がかかります. 問題となるのは、オブジェクトの有効期間戻り値の最適化オブジェクト値を返すことの混乱、参照型のオーバーロードとの混乱です。それぞれに対処し、それらの関連性を説明します。

参考文献

最初のことが最初です。参考書とは?それらは構文のない単なるポインターではありませんか?

それらはそうですが、重要な意味で、それらはそれ以上のものです。ポインターは文字通り、一般的にメモリの場所を参照します。ポインターが設定されている場所にある値についての保証は、あるとしてもほとんどありません。一方、参照は実際の値のアドレスにバインドされます。値は、アクセスできる期間中存在することを保証しますが、他の方法 (一時的なものなど) でアクセスできる名前を持たない場合があります。

経験則として、「そのアドレスを取得」できる場合は、参照を扱っていることになります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() );

ここで発生する可能性のある最長の一連のイベントを考えてみましょう。

  1. 新しいAが構築されますx
  2. によって返される一時的な値は、以前の値への参照を使用して初期化されx()た新しいA
  3. 新しいA- y- は一時的な値を使用して初期化されます

可能であれば、中間体にアクセスできない、または不要であると安全に想定できるように、できるだけ少数Aの中間体が構築されるように、コンパイラーは物事を再配置する必要があります。問題は、どのオブジェクトがなくてもできるかということです。

ケース #1 は明示的な新しいオブジェクトです。これが作成されないようにする場合は、既に存在するオブジェクトへの参照が必要です。これは最も単純なものであり、これ以上言う必要はありません。

#2では、何らかの結果を構築することを避けることはできません。結局のところ、value で返されます。ただし、2 つの重要な例外があります (スローされたときに影響を受ける例外自体は含まれません) : NRVORVOです。これらは #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

できない理由は、resultout 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.
}

について確かに言えることは、移動した値または式型の値を取得することを期待しているということですfaなぜならf、式参照 (prvalues) または消えそうな左辺値参照 (xvalues) または移動された左辺値参照 ( を介して xvalues に変換される) のいずれかを受け入れることができ、3 つのケースすべての の処理において同種でなければならないstd::moveため、への呼び出しよりも寿命が長く存在するメモリ領域への最初の参照。つまり、呼び出した 3 つのケースのどれを内から区別することはできないため、コンパイラは、どのケースでも必要な最長のストレージ期間を想定し、のストレージ期間について何も想定しないことが最も安全であると判断します。faafffaのデータです。

の状況とは異なり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::movef

右辺値参照は、「移動された値」のみをターゲットにするための万能薬ではありません。より多くのものをターゲットにすることができ、それがすべてであると仮定すると、誤ってまたは予期せずにターゲットにされることさえあります。一般的に何かへの参照は、右辺値参照変数を除いて、参照よりも長く存在することが期待され、作成される必要があります。

右辺値参照変数は本質的に、省略演算子です。それらが存在する場所ではどこにでも、何らかの記述のインプレース構築が行われています。

通常の変数として、受け取る 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 では、新しいオブジェクトを構築するのではなく、 の結果の有効期間を のスコープに拡張します。代わりに、同じことを言います。コピーしました。rfssff

主な違いは、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、有効期間の延長が開始されなかったのはなぜですか? 返されるもののために、その場で構築する関数を指定することはできませんが、その場で構築される変数を指定できます。また、コンパイラが関数/呼び出しの境界を越えて寿命を決定するのではなく、呼び出し側またはローカルにある変数、それらがどのように割り当てられているか、ローカルの場合はどのように初期化されているかを調べるだけであることも示しています。gf

すべての疑問を解消したい場合は、これを右辺値参照として渡してみてくださいstd::move(*(new A)): (つまり、中間体/式)。xvalues は移動構築/移動代入の候補であり、省略できません (既に構築されています) が、他のすべての移動/コピー操作は理論上、コンパイラの気まぐれで省略できます。右辺値参照を使用する場合、コンパイラはアドレスを省略するか渡すしかありません。

于 2016-06-26T16:31:21.373 に答える