102

オブジェクトをコピーし、その後クラスのデータ メンバーに移動することを決定したコードをどこかで見ました。これは、移動の要点はコピーを避けることだと思っていたという点で、私を混乱させました。次に例を示します。

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

ここに私の質問があります:

  • への右辺値参照を取得しないのはなぜstrですか?
  • 特に次のような場合、コピーは高くなりませんstd::stringか?
  • 著者がコピーを作成してから移動することを決定した理由は何ですか?
  • いつ自分でこれを行う必要がありますか?
4

4 に答える 4

101

あなたの質問に答える前に、あなたが間違っているように思われることが 1 つあります。それは、C++11 での値による取得は、常にコピーを意味するとは限らないということです。右辺値が渡されると、コピーされるのではなく、移動されます (実行可能な移動コンストラクターが存在する場合) そしてstd::string移動コンストラクタがあります。

C++03 とは異なり、C++11 では、以下で説明する理由から、パラメーターを値で受け取るのが慣用的であることがよくあります。また、パラメーターを受け入れる方法に関するより一般的な一連のガイドラインについては、StackOverflow に関するこの Q&Aも参照してください。

への右辺値参照を取得しないのはなぜstrですか?

それは、次のような左辺値を渡すことが不可能になるためです。

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!

S右辺値を受け入れるコンストラクターしかない場合、上記はコンパイルされません。

特に次のような場合、コピーは高くなりませんstd::stringか?

右辺値を渡すと、それは に移動されstr、最終的には に移動されdataます。コピーは行いません。一方、左辺値を渡すと、その左辺値は にコピーさstr、次に に移動されdataます。

要約すると、右辺値に対して 2 つの移動、左辺値に対して 1 つのコピーと 1 つの移動です。

著者がコピーを作成してから移動することを決定した理由は何ですか?

まず第一に、上で述べたように、最初のものは常にコピーであるとは限りません。これは、答えは次のとおりです。「それは効率的であり(std::stringオブジェクトの移動は安価です)、単純だからです」。

移動が安価であるという仮定の下では (ここでは SSO を無視します)、この設計の全体的な効率を考慮すると、移動は事実上無視できます。そうする場合、左辺値のコピーが 1 つあり ( への左辺値参照を受け入れた場合と同様const)、右辺値のコピーはありません (一方、 への左辺値参照を受け入れた場合はまだコピーがありますconst)。

これは、値による取得は、左辺値が提供されている場合は左辺値参照による取得と同じくらい優れておりconst、右辺値が提供されている場合はより優れていることを意味します。

PS: コンテキストを提供するために、これはOP が参照している Q&Aだと思います。

于 2013-05-23T22:05:55.923 に答える
51

これが良いパターンである理由を理解するには、C++03 と C++11 の両方で代替案を調べる必要があります。

を取得する C++03 メソッドがありstd::string const&ます。

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};

この場合、常に単一のコピーが実行されます。生の C 文字列から構築する場合、astd::stringが構築され、再度コピーされます: 2 つの割り当て。

std::stringへの参照を取得し、それを local にスワップするC++03 メソッドがありますstd::string

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};

これは「移動セマンティクス」の C++03 バージョンであり、swap多くの場合、非常に安価に実行できるように最適化できます (a によく似ていますmove)。また、コンテキストで分析する必要があります。

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal

非一時的な を形成することを強制しstd::string、それを破棄します。(テンポラリstd::stringは非 const 参照にバインドできません)。ただし、割り当ては 1 つだけです。C++11 バージョンは を取り、それを、または一時的&&に呼び出す必要があります。これには、呼び出し元が呼び出しの外で明示的にコピーを作成し、そのコピーを関数またはコンストラクターに移動する必要があります。std::move

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};

使用する:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal

次に、コピーと の両方をサポートする完全な C++11 バージョンを実行できますmove

struct S
{
  std::string data; 
  S(std::string const& str) : data(str) {} // lvalue const, copy
  S(std::string && str) : data(std::move(str)) {} // rvalue, move
};

次に、これがどのように使用されるかを調べます。

S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data

std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data

std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data

この 2 つのオーバーロード手法が、上記の 2 つの C++03 スタイルよりも効率的ではないにしても、少なくとも同じくらい効率的であることは明らかです。この 2 オーバーロード バージョンを「最適な」バージョンと呼びます。

次に、テイク バイ コピー バージョンを調べます。

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};

これらのシナリオのそれぞれで:

S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data

std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data

std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data

これを「最適な」バージョンと並べて比較すると、ちょうど 1 つの追加が行われmoveます。余分なことは一度もありませんcopy

したがって、これが安価であると仮定するとmove、このバージョンは最も最適なバージョンとほぼ同じパフォーマンスを実現しますが、コードは 2 倍少なくなります。

また、たとえば 2 ~ 10 個の引数を使用する場合、コードの削減は指数関数的です。引数が 1 の場合は 2 倍、2 の場合は 4 倍、3 の場合は 8 倍、4 の場合は 16 倍、引数が 10 の場合は 1024 倍になります。

これで、完全転送と SFINAE を介してこれを回避できます。これにより、10 個の引数を受け取る単一のコンストラクターまたは関数テンプレートを作成し、SFINAE を実行して引数が適切な型であることを確認し、それらを必要に応じてローカル状態。これにより、プログラム サイズの問題が数千倍に増加するのを防ぐことができますが、このテンプレートから大量の関数が生成される可能性があります。(テンプレート関数のインスタンス化は関数を生成します)

また、多数の関数が生成されるということは、実行可能なコードのサイズが大きくなることを意味し、それ自体がパフォーマンスを低下させる可能性があります。

move秒のコストで、より短いコードとほぼ同じパフォーマンスが得られ、多くの場合、コードを理解しやすくなります。

これが機能するのは、関数 (この場合はコンストラクター) が呼び出されたときに、その引数のローカル コピーが必要になることがわかっているからです。コピーを作成することがわかっている場合は、コピーを作成していることを呼び出し元に引数リストに入れて知らせる必要があります。次に、彼らは私たちにコピーを提供しようとしているという事実を中心に最適化できます(たとえば、私たちの議論に移ることによって)。

「値による取得」手法のもう 1 つの利点は、ムーブ コンストラクターが noexcept であることが多いことです。つまり、値で取得して引数の外に移動する関数は、多くの場合、noexcept である可能性があり、すべてthrowの s を本体から呼び出しスコープに移動します。 (直接構築することで回避できる場合もあれば、アイテムを構築moveして引数に入れ、スローが発生する場所を制御することもできます) メソッドを非スローにすることは、多くの場合、価値があります。

于 2013-05-23T22:38:07.250 に答える
13

これはおそらく意図的なもので、copy and swap イディオムに似ています。基本的に、文字列はコンストラクターの前にコピーされるため、コンストラクター自体は一時的な文字列 str のみを交換 (移動) するため、例外セーフです。

于 2013-05-23T22:10:47.343 に答える
11

移動用のコンストラクターとコピー用のコンストラクターを記述して、自分自身を繰り返したくはありません。

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}

これは、特に複数の引数がある場合、定型的なコードです。あなたのソリューションは、不必要な移動のコストでその重複を回避します。(ただし、移動操作は非常に安価なはずです。)

競合するイディオムは、完全転送を使用することです。

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}

テンプレート マジックは、渡されたパラメーターに応じて移動またはコピーを選択します。基本的に、両方のコンストラクターが手動で記述された最初のバージョンに展開されます。背景情報については、ユニバーサル リファレンスに関する Scott Meyer の投稿を参照してください。

パフォーマンスの観点からは、完全転送バージョンは不要な移動を回避するため、あなたのバージョンよりも優れています。ただし、あなたのバージョンの方が読みやすく、書きやすいと主張する人もいます。いずれにせよ、パフォーマンスへの影響の可能性は、ほとんどの状況では問題にならないはずなので、最終的にはスタイルの問題のようです。

于 2013-05-25T16:31:39.690 に答える