web-dev-qa-db-ja.com

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

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

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

私の質問は次のとおりです。

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

私があなたの質問に答える前に、あなたが間違っているように思われる1つのこと:C++ 11で値をとることは常にコピーを意味するわけではありません。右辺値が渡された場合、それはコピーされずにmoved(実行可能な移動コンストラクターが存在する場合)になります。そして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

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

まとめると、右辺値に対して2つの動き、左辺値に対して1つのコピーと1つの動きです。

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

まず、前述したように、最初のものは常にコピーではありません。これは、答えは次のとおりです。「効率的であるため(std::stringオブジェクトは安価であり、シンプルです)。

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

つまり、値による取得は、左辺値が指定されている場合はconstへの左辺値参照による取得と同等であり、右辺値が指定されている場合はより適切です。

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

96
Andy Prowl

これが良いパターンである理由を理解するために、C++ 03とC++ 11の両方の選択肢を検討する必要があります。

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

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

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

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

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

これは「移動セマンティクス」の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または一時的に呼び出す必要があります。これは、呼び出し元explicitlyの外部にコピーを作成する必要があります呼び出し、そのコピーを関数またはコンストラクターに移動します。

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

次に、コピーと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

このサイドバイサイドを「最も最適な」バージョンと比較する場合、move!を1つ追加します。余分なcopyを一度もしません。

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

また、2〜10個の引数を取る場合、コードの削減は指数関数的です。1個の引数で2倍、2個で4x、3個で8x、4個で1024x、10個の引数で1024xです。

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

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

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

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

「値による取得」手法のもう1つの利点は、移動コンストラクターがnoexceptであることが多いということです。これは、値によって取得して引数から移動する関数が多くの場合noexceptであり、throwsを本体から移動し、 (場合によっては、直接の構築によって回避するか、アイテムとmoveを引数に構築して、スローが発生する場所を制御できます。メソッドnothrowを作成することは価値があります。

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

13
Joe

移動用のコンストラクターとコピー用のコンストラクターを作成することで、自分自身を繰り返したくありません。

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の ユニバーサルリファレンス に関する投稿を参照してください。

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

11
Philipp Claßen