67

例として、次の方法を見てみましょう。

void Asset::Load( const std::string& path )
{
    // complicated method....
}

このメソッドの一般的な使用方法は次のとおりです。

Asset exampleAsset;
exampleAsset.Load("image0.png");

ほとんどの場合、Path は一時的な右辺値であることがわかっているため、このメソッドの右辺値バージョンを追加することは理にかなっていますか? もしそうなら、これは正しい実装ですか?

void Asset::Load( const std::string& path )
{
    // complicated method....
}
void Asset::Load( std::string&& path )
{
     Load(path); // call the above method
}

これは、メソッドの右辺値バージョンを作成するための正しいアプローチですか?

4

6 に答える 6

79

あなたの特定のケースでは、2番目のオーバーロードは役に立ちません。

のオーバーロードが 1 つしかない元のコードではLoad、この関数は左辺値と右辺値に対して呼び出されます。

新しいコードでは、最初のオーバーロードが左辺値に対して呼び出され、2 番目が右辺値に対して呼び出されます。ただし、2 番目のオーバーロードは最初のオーバーロードを呼び出します。最後に、どちらかを呼び出すことの効果は、同じ操作 (最初のオーバーロードが行うものは何でも) が実行されることを意味します。

したがって、元のコードと新しいコードの効果は同じですが、最初のコードの方が単純です。

関数が値、左辺値参照、または右辺値参照のいずれによって引数を取る必要があるかの決定は、それが何をするかに大きく依存します。渡された引数を移動する場合は、右辺値参照を取るオーバーロードを提供する必要があります。ムーブ セマンチンについては参考文献がいくつかあるので、ここでは説明しません。

ボーナス:

私の要点を説明するために、この単純なprobeクラスを検討してください。

struct probe {
    probe(const char*  ) { std::cout << "ctr " << std::endl; }
    probe(const probe& ) { std::cout << "copy" << std::endl; }
    probe(probe&&      ) { std::cout << "move" << std::endl; }
};

次に、この関数を考えてみましょう:

void f(const probe& p) {
    probe q(p);
    // use q;
}

を呼び出すf("foo");と、次の出力が生成されます。

ctr
copy

ここで驚くことではありません: をprobe渡す一時的な を作成しますconst char* "foo"。したがって、最初の出力行です。次に、このテンポラリが にバインドされp、 のコピーqp内に作成されfます。したがって、2 番目の出力行です。

ここで、値による取得を検討してくださいp。つまり、次のように変更fします。

void f(probe p) {
    // use p;
}

の出力f("foo");は現在

ctr

この場合、コピーがないことに驚く人もいるでしょう。一般に、参照によって引数を取得し、それを関数内にコピーする場合は、値によって引数を取得することをお勧めします。この場合、一時変数を作成してコピーする代わりに、コンパイラは引数 (pこの場合) を入力 ( ) から直接構築でき"foo"ます。詳細については、「速度が必要ですか? 」を参照してください。値渡し。デイブ・エイブラハムズ著。

このガイドラインには、コンストラクターと代入演算子の 2 つの注目すべき例外があります。

このクラスを考えてみましょう:

struct foo {
    probe p;
    foo(const probe& q) : p(q) { }
};

コンストラクターは、probeconst 参照を受け取り、それを にコピーしpます。この場合、上記のガイドラインに従ってもパフォーマンスは向上せず、probeとにかく のコピー コンストラクターが呼び出されます。ただし、q値を取得すると、これから説明する代入演算子を使用した場合と同様のオーバーロード解決の問題が発生する可能性があります。

クラスprobeにスローしないswapメソッドがあるとします。次に、その代入演算子の推奨される実装 (当面は C++03 の用語で考えます) は次のとおりです。

probe& operator =(const probe& other) {
    probe tmp(other);
    swap(tmp);
    return *this;
}

では、上記のガイドラインに従って、このように書く方が良いでしょう

probe& operator =(probe tmp) {
    swap(tmp);
    return *this;
}

ここで、右辺値参照とムーブ セマンティクスを使用して C++11 に入ります。移動代入演算子を追加することにしました。

probe& operator =(probe&&);

一時的に代入演算子を呼び出すと、両方のオーバーロードが実行可能であり、どちらも優先されないため、あいまいさが生じます。この問題を解決するには、代入演算子の元の実装を使用します (const 参照によって引数を取ります)。

実際、この問題はコンストラクターと代入演算子に固有のものではなく、どの関数でも発生する可能性があります。(ただし、コンストラクターと代入演算子でそれを経験する可能性が高くなります。) たとえば、次の 2 つのオーバーロードがあるg("foo");whengを呼び出すと、あいまいさが生じます。

void g(probe);
void g(probe&&);
于 2013-03-28T04:55:40.787 に答える
8

の左辺値参照バージョンを呼び出す以外のことをしていない限りLoad、右辺値は const 左辺値参照にバインドされるため、2 番目の関数は必要ありません。

于 2013-03-28T04:11:19.967 に答える
5

ほとんどの場合、Path は一時的な右辺値であることがわかっているため、このメソッドの右辺値バージョンを追加することは理にかなっていますか?

Load()おそらくそうではありません... const 以外のパラメーターを必要とする内部でトリッキーなことをする必要がない限り。たとえば、std::move(Path)別のスレッドに入りたいとします。その場合、移動セマンティクスを使用するのが理にかなっています。

これは、メソッドの右辺値バージョンを作成するための正しいアプローチですか?

いいえ、逆の方法で行う必要があります。

void Asset::load( const std::string& path )
{
     auto path_copy = path;
     load(std::move(path_copy)); // call the below method
}
void Asset::load( std::string&& path )
{
    // complicated method....
}
于 2016-11-22T08:36:25.587 に答える
1

メンバー関数が受信文字列から割り当てられない場合Loadは、単にvoid Asset::Load(const std::string& Path).

メンバー変数など、incoming から代入する場合はpath、提供するほうが少し効率的であるシナリオもありvoid Asset::Load(std::string&& Path)ますが、 ala を代入する別の実装が必要になりますloaded_from_path_ = std::move(Path);

潜在的な利点は、呼び出し元にとっての利点です。この&&バージョンでは、メンバー変数が所有していたフリーストア領域を受け取る可能性があり、delete[]内部のそのバッファーの悲観的なイオンを回避し、void Asset::Load(const std::string& Path)次に呼び出し元の文字列が割り当てられたときに再割り当てされる可能性を回避できます。に (バッファーが次の値にも適合するのに十分な大きさであると仮定します)。

あなたが述べたシナリオでは、通常、文字列リテラルを渡しています。このような呼び出し元は、既存のデータ メンバーのバッファーを受け取る&&呼び出し元所有のインスタンスがないため、オーバーロードの恩恵を受けません。std::string

于 2015-05-19T04:02:39.397 に答える
1

一般的には、受信オブジェクトのコピーを内部的に (明示的または暗黙的に) 作成する (T&&引数を提供する) か、それをそのまま使用する (に固執する[const] T&) かが問題になります。

于 2013-03-28T18:29:01.980 に答える
0

関数のシグネチャを決定しようとするときに私がすることは次のとおりです

  1. (const std::string& const_lvalue)引数は読み取り専用です
  2. (std::string& lvalue)私は引数を変更することができます(通常は何かを入れます)ので、変更は呼び出し元に表示されます
  3. (std::string&& rvalue)引数を変更できます (通常はから何かを盗みます)。呼び出し元がこの引数を表示/使用しなくなるため、結果はゼロです (関数が返された後に自己破壊されると考えてください)。

3 つすべてが「参照渡し」ですが、異なる意図を示しています。2 + 3 は似ています。どちらも引数を変更できますが、2 は呼び出し元に変更が表示されることを望んでいますが、3 はそうではありません。

// (2) caller sees the change argument
void ModifyInPlace(Foo& lvalue){
  delete lvalue.data_pointer;
  lvalue.data_pointer = nullptr; 
}

// (3) move constructor, caller ignores the change to the argument
Foo(Foo&& rvalue)
{
  this->data_pointer = that.data_pointer;
  that.data_pointer = nullptr;
}

于 2021-10-20T07:31:27.740 に答える