web-dev-qa-db-ja.com

値渡しvs右辺値参照渡し

次のように関数を宣言する必要がある場合:

void foo(Widget w);

とは対照的に

void foo(Widget&& w);

これが唯一のオーバーロードであると仮定します(たとえば、両方ではなく、どちらかを選択し、他のオーバーロードは選択しません)。関係するテンプレートはありません。関数fooにはWidgetの所有権が必要であると想定します(たとえば、const Widget&はこの議論の一部ではありません)。これらの状況の範囲外の回答には興味がありません。これらの制約が問題の一部であるwhyについては、投稿の最後の補遺をご覧ください。

同僚と私が思いつく主な違いは、右辺値参照パラメーターにより、コピーについて明示的に指定することです。呼び出し元は、明示的なコピーを作成し、コピーが必要なときにstd::moveでそれを渡す責任があります。値渡しの場合、コピーのコストは隠されます:

    //If foo is a pass by value function, calling + making a copy:
    Widget x{};
    foo(x); //Implicit copy
    //Not shown: continues to use x locally

    //If foo is a pass by rvalue reference function, calling + making a copy:
    Widget x{};
    //foo(x); //This would be a compiler error
    auto copy = x; //Explicit copy
    foo(std::move(copy));
    //Not shown: continues to use x locally

その違い以外。関数を呼び出すときに取得する構文糖度をコピーして変更することを人々に明示的に強制する以外に、これらはどのように異なりますか?彼らはインターフェースについてどう違うと言っていますか?それらは互いに多かれ少なかれ効率的ですか?

私の同僚と私がすでに考えた他のこと:

  • 右辺値参照パラメーターは、引数を移動することができますが、必須ではないことを意味します。呼び出しサイトで渡した引数は、その後元の状態になる可能性があります。また、関数が移動コンストラクターを呼び出さずに引数を食べたり変更したりする可能性もありますが、それが右辺値参照であるため、呼び出し元が制御を放棄したと想定します。値を渡し、そこに移動する場合は、移動が発生したと想定する必要があります。選択の余地はありません。
  • 省略がないと仮定すると、単一の移動コンストラクター呼び出しは、右辺値による受け渡しで除去されます。
  • コンパイラは、値渡しでコピー/ムーブを排除するより良い機会を持っています。誰もこの主張を実証できますか?できれば、標準の行ではなくgcc/clangから最適化された生成コードを示すgcc.godbolt.orgへのリンクを使用してください。これを示す私の試みはおそらく動作をうまく分離できなかったでしょう: https://godbolt.org/g/4yomtt

補遺:なぜこの問題をそんなに制約しているのですか?

  • オーバーロードなし-他のオーバーロードがある場合、これは値渡しとconst参照と右辺値参照の両方を含むオーバーロードのセットの議論に発展し、その時点でオーバーロードのセットは明らかにより効率的で勝ちます。これはよく知られているため、面白くありません。
  • テンプレートなし-転送参照がどのように写真に収まるのか興味がありません。転送参照がある場合は、とにかくstd :: forwardを呼び出します。転送参照の目標は、受信したものを渡すことです。代わりに左辺値を渡すだけなので、コピーは関係ありません。それはよく知られていて、面白くありません。
  • fooにはWidgetの所有権が必要です(別名const Widget&)-読み取り専用関数については話していません。関数が読み取り専用である場合、またはWidgetのライフタイムを所有または延長する必要がない場合、答えは簡単にconst Widget&になります。また、なぜオーバーロードについて説明したくないのかを紹介します。
47
Mark

右辺値参照パラメーターを使用すると、コピーについて明示的に指定できます。

はい、pass-by-rvalue-referenceはポイントを獲得しました。

右辺値参照パラメーターは、引数を移動することはできますが、必須ではないことを意味します。

はい、値渡しにはポイントがあります。しかし、これはまた、パスバイバイに例外保証を処理する機会を与えます:fooがスローされる場合、widget値は消費される必要はありません。

移動専用タイプの場合(std::unique_ptr)、値渡しが標準であるようです(主に2番目のポイントで、最初のポイントはとにかく適用されません)。

編集:標準ライブラリは、私の前の文、shared_ptrのコンストラクターはstd::unique_ptr<T, D>&&

コピー/移動の両方を持つタイプの場合(std::shared_ptr)、以前のタイプとの一貫性を選択するか、コピー時に明示的にするかを選択できます。

不要なコピーがないことを保証する場合を除き、一貫性のために値渡しを使用します。 あなたが保証されたおよび/または即時のシンクを望んでいない限り、私は右辺値を使用します。

既存のコードベースについては、一貫性を保ちます。

9
Jarod42

インターフェースとコピーについて、右辺値の使用法は何を言いますか? rvalueは、関数が値を所有することを望んでいることと、呼び出し元に行った変更を知らせる意図がないことを呼び出し元に示唆します。次のことを考慮してください(あなたの例では左辺値の参照はないと言っていますが、我慢してください):

//Hello. I want my own local copy of your Widget that I will manipulate,
//but I don't want my changes to affect the one you have. I may or may not
//hold onto it for later, but that's none of your business.
void foo(Widget w);

//Hello. I want to take your Widget and play with it. It may be in a
//different state than when you gave it to me, but it'll still be yours
//when I'm finished. Trust me!
void foo(Widget& w);

//Hello. Can I see that Widget of yours? I don't want to mess with it;
//I just want to check something out on it. Read that one value from it,
//or observe what state it's in. I won't touch it and I won't keep it.
void foo(const Widget& w);

//Hello. Ooh, I like that Widget you have. You're not going to use it
//anymore, are you? Please just give it to me. Thank you! It's my
//responsibility now, so don't worry about it anymore, m'kay?
void foo(Widget&& w);

別の見方をすると:

//Here, let me buy you a new car just like mine. I don't care if you wreck
//it or give it a new Paint job; you have yours and I have mine.
void foo(Car c);

//Here are the keys to my car. I understand that it may come back...
//not quite the same... as I lent it to you, but I'm okay with that.
void foo(Car& c);

//Here are the keys to my car as long as you promise to not give it a
//Paint job or anything like that
void foo(const Car& c);

//I don't need my car anymore, so I'm signing the title over to you now.
//Happy birthday!
void foo(Car&& c);

現在、ウィジェットが一意である必要がある場合(たとえば、GTKの実際のウィジェットのように)、最初のオプションは機能しません。データの実際の表現はまだ1つしかないため、2番目、3番目、4番目のオプションは理にかなっています。とにかく、それはそれらのセマンティクスがコードで見たときに私に言うことです。

さて、効率については:依存します。 Widgetが、ポイント先のコンテンツがかなり大きくなる可能性のあるデータメンバへのポインタを持っている場合(配列を考えると)、右辺値参照は多くの時間を節約できます。呼び出し元が右辺値を使用したため、彼らは彼らがもうあなたに与えているものを気にしないと言っています。そのため、発信者のウィジェットのコンテンツをウィジェットに移動する場合は、ポインタを取得します。ポインタが指すデータ構造内の各要素を細かくコピーする必要はありません。これにより、速度がかなり向上する可能性があります(もう一度考えてください)。しかし、Widgetクラスにそのようなものがない場合、この利点はどこにも見当たりません。

うまくいけば、それがあなたが求めていたものに到達することです。そうでない場合は、おそらく物事を拡張/明確化できます。

10
Altainia

型が移動専用型でない限り、通常は参照から定数に渡すオプションがあり、「議論の一部ではない」ようにするのはarbitrary意的なようですが、試してみます。

この選択は、パラメータでfooが何をするかによってある程度決まると思います。

関数にはローカルコピーが必要です

Widgetがイテレータであり、独自の std::next 関数を実装するとします。 nextは、先に進んで戻るために独自のコピーを必要とします。この場合、選択は次のようになります。

Widget next(Widget it, int n = 1){
    std::advance(it, n);
    return it;
}

Widget next(Widget&& it, int n = 1){
    std::advance(it, n);
    return std::move(it);
}

ここでは、値ごとの方が優れていると思います。署名から、コピーを取っていることがわかります。呼び出し元がコピーを避けたい場合、std::moveを実行して変数の移動元を保証できますが、必要であれば左辺値を渡すことができます。 pass-by-rvalue-referenceを使用すると、呼び出し元は変数の移動元を保証できません。

コピーへの移動割り当て

クラスWidgetHolderがあるとしましょう:

class WidgetHolder {
    Widget widget;
   //...
};

setWidgetメンバー関数を実装する必要があります。参照から定数へのオーバーロードが既にあると仮定します。

WidgetHolder::setWidget(const Widget& w) {
    widget = w;
}

しかし、パフォーマンスを測定した後、r値を最適化する必要があると判断します。次のものに置き換えるかどうかを選択できます。

WidgetHolder::setWidget(Widget w) {
    widget = std::move(w);
}

またはでオーバーロード:

WidgetHolder::setWidget(Widget&& widget) {
    widget = std::move(w);
}

これはもう少し注意が必要です。右辺値と左辺値の両方を受け入れ、2つのオーバーロードを必要としないため、値渡しを選択するのは魅力的です。ただし、無条件にコピーを取得するため、メンバー変数の既存の容量を利用することはできません。 reference-to-constによる受け渡しとr-value参照による受け渡しオーバーロードは、コピーを取得せずにassignmentを使用します。

コピーの移動構築

ここで、WidgetHolderのコンストラクタを書いているとしましょう。以前のように、constへの参照を取るコンストラクタを既に実装しています。

WidgetHolder::WidgetHolder(const Widget& w) : widget(w) {
}

前と同様に、パフォーマンスを測定し、右辺値に対して最適化する必要があると判断しました。次のものに置き換えるかどうかを選択できます。

WidgetHolder::WidgetHolder(Widget w) : widget(std::move(w)) {
}

またはでオーバーロード:

WidgetHolder::WidgetHolder(Widget&& w) : widget(std:move(w)) {
}

この場合、メンバー変数はコンストラクタであるため、既存の容量を持つことはできません。あなたはmove-constuctingコピーです。また、コンストラクターは多くのパラメーターを受け取ることが多いため、オーバーロードのさまざまな順列をすべて作成してr値参照を最適化するのは非常に面倒です。したがって、この場合、特にコンストラクターがそのようなパラメーターを多数使用する場合は、値渡しを使用することをお勧めします。

unique_ptrを渡す

unique_ptrを使用すると、移動が非常に安く、容量がないため、効率の問題はそれほど重要ではありません。さらに重要なのは、表現力と正確さです。 unique_ptrhere を渡す方法についての良い議論があります。

10
Chris Drew

他の回答で言及されていない問題の1つは、例外安全性の考え方です。

一般に、関数が例外をスローする場合、理想的には強力な例外保証が必要です。つまり、呼び出しは例外を発生させる以外の効果はありません。値渡しで移動コンストラクターを使用する場合、そのような効果は本質的に避けられません。そのため、場合によっては、右辺値参照引数の方が優れている場合があります。 (もちろん、強力な例外保証がどちらの方法でも達成できないさまざまなケースがあります。また、ノースロー保証がどちらの方法でも利用可能なさまざまなケースがあります。時々。)

4
ruakh

右辺値参照オブジェクトを渡すと、ライフタイムが複雑になります。呼び出し先が引数から移動しない場合、引数の破棄は遅延します。これは2つの場合に興味深いと思います。

まず、RAIIクラスがあります

void fn(RAII &&);

RAII x{underlying_resource};
fn(std::move(x));
// later in the code
RAII y{underlying_resource};

yを初期化するときに、xが右辺値参照から移動しない場合、リソースはfnによって保持される可能性があります。値渡しコードでは、xが外に移動し、fnxを解放することがわかります。これはおそらく、値で渡したい場合であり、コピーコンストラクタは削除される可能性が高いため、誤ってコピーすることを心配する必要はありません。

次に、引数がラージオブジェクトであり、関数が移動しない場合、ベクターデータの寿命は値渡しの場合よりも長くなります。

vector<B> fn1(vector<A> &&x);
vector<C> fn2(vector<B> &&x);

vector<A> va;  // large vector
vector<B> vb = fn1(std::move(va));
vector<C> vc = fn2(std::move(vb));

上記の例では、fn1およびfn2xから移動しないでください。そうすれば、すべてのベクター内のすべてのデータがまだ生きていることになります。代わりに値で渡す場合、最後のベクターのデータのみが引き続き有効です(ベクター移動コンストラクターがソースベクターをクリアすると仮定)。

4
Nick

By-valueとby-rvalue-refを選択することは、他のオーバーロードなしでは意味がありません。

値渡しの場合、実引数は左辺値式にすることができます。

Rvalue-refによるパスでは、実際の引数は右辺値でなければなりません。


関数が引数のコピーを保存している場合は、値渡しと、ref-to-constおよびpass-by-rvalue-refを渡すオーバーロードのセットの間で賢明な選択ができます。実引数としての右辺値式の場合、オーバーロードのセットにより1つの移動を回避できます。マイクロ最適化が追加された複雑さとタイピングの価値があるかどうかは、エンジニアリングの直感的な決定です。