なぜコピーしてから移動するのですか?


98

誰かがオブジェクトをコピーし、その後それをクラスのデータメンバーに移動することにしたコードをどこかに見ました。これは、移動の全体のポイントがコピーを避けることであると思ったので、私を混乱させました。次に例を示します。

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

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

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

私にとってはばかげた間違いのように見えますが、この件についてもっと知識のある人がそれについて何か言いたいことがあるかどうか興味があります。
デイブ


最初にリンクするのを忘れていたこのQ&Aもトピックに関連している可能性があります。
Andy Prowl 2013年

回答:


97

質問に答える前に、間違っているように思われることが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だと思います。


2
言及する価値があるのは、const T&引数の受け渡しを置き換えるC ++ 11パターンです。最悪の場合(左辺値)これは同じですが、一時ファイルの場合は、一時ファイルを移動するだけで済みます。Win-Win。
サイアム2013年

3
@ user2030677:参照を保存しない限り、そのコピーを回避することはできません。
ベンジャミンリンドリー

5
@ user2030677:(あなたが保持したい場合は、とあなたがコピーは限り、あなたはそれを必要としてどのように高価な気は誰のコピーを、あなたの中でdataメンバー)?lvalueの参照を使用する場合でも、コピーが作成されますconst
Andy Prowl 2013年

3
@BenjaminLindley:予備として、「動きが安いという仮定の下で、この設計の全体的な効率を考慮するとき、それらは実質的に無視することができます。」したがって、そうです、移動のオーバーヘッドがありますが、これが単純な設計をより効率的なものに変更することを正当化する真の懸念であるという証拠がない限り、無視できると考えるべきです。
Andy Prowl 2013年

1
@ user2030677:しかし、それは完全に異なる例です。あなたの質問の例では、常にコピーをdata
Andy Prowl 2013年

51

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

C ++ 03のメソッドを使用してstd::string const&

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

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

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

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

これは「move semantics」のC ++ 03バージョンでswapあり、(のように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

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

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分の1になります。

そして、たとえば2から10個の引数をとる場合、コードの削減は指数関数的です-1つの引数で2倍、2で4倍、8で3、16で4、1024で10の引数。

これで、完全な転送とSFINAEを介してこれを回避できるため、10個の引数を取得する単一のコンストラクターまたは関数テンプレートを記述し、SFINAEを使用して引数が適切な型であることを確認してから、それらを必要に応じてローカル状態。これにより、プログラムサイズの問題が1000倍に増えるのを防ぎますが、このテンプレートから生成された関数の山がまだ残っている可能性があります。(テンプレート関数のインスタンス化により関数が生成されます)

また、生成された関数の多くは、実行可能コードのサイズが大きくなるため、パフォーマンスが低下する可能性があります。

move秒のコストで、コードが短くなり、パフォーマンスはほぼ同じになり、コードを理解しやすくなります。

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

「値による取得」手法のもう1つの利点は、多くの場合、移動コンストラクターが例外ではないことです。つまり、値によって取得し、引数から移動する関数は、多くの場合、例外なく、throwsを本体から呼び出しスコープに移動します。 (だれがmoveスローを発生させるかを制御するために、直接構築を介して回避したり、アイテムを構築して引数に組み込んだりできます。)メソッドをスローしないようにすることは、多くの場合価値があります。


コピーを作成することがわかっている場合は、コンパイラーが常にそれを知っているため、コンパイラーにそれを実行させる必要があります。
Rayniery 2013年

6
私がこれを書いたので、もう1つの利点が指摘されました。多くの場合、コピーコンストラクターがスローできるのに対し、移動コンストラクターは多くの場合noexceptです。データをコピーして取得することで、関数を作成し、noexceptコピーの構築によって発生する可能性のあるスロー(メモリ不足など)を関数呼び出しの外部で発生させることができます。
Yakk-Adam Nevraumont 2014

3オーバーロード手法で「lvalue non-const、copy」バージョンが必要なのはなぜですか?「左辺値const、copy」は非constの場合も処理しませんか?
Bruno Martinez

@BrunoMartinez私たちはしません!
Yakk-Adam Nevraumont 2014

13

これはおそらく意図的なものであり、コピーとスワップのイディオムに似ています。基本的に文字列はコンストラクターの前にコピーされるため、コンストラクター自体は一時的な文字列strを交換(移動)するだけなので、例外的に安全です。


コピーアンドスワップパラレルの場合は+1。実際、それは多くの類似点を持っています。
サイアム2013年

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の投稿を参照してください

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.