web-dev-qa-db-ja.com

コピーよりもオブジェクトの移動を速くするものは何ですか?

スコット・マイヤーズが「std::move()は何も動かない」と言うのを聞いたことがありますが、それが何を意味するのか理解できていません。

したがって、私の質問を特定するには、次のことを考慮してください。

_class Box { /* things... */ };

Box box1 = some_value;
Box box2 = box1;    // value of box1 is copied to box2 ... ok
_

どうですか:

_Box box3 = std::move(box1);
_

私は左辺値と右辺値のルールを理解していますが、私が理解していないのは、実際にメモリ内で何が起こっているのですか?何か別の方法で値をコピーするだけですか、アドレスを共有するのですか?より具体的には、何がコピーよりも速く動くのですか?

これを理解することですべてが明確になると感じています。前もって感謝します!

編集:std::move()の実装や構文的なものについては聞いていないことに注意してください。

33
Laith

@ gudok前に回答済み のように、すべてが実装に含まれています...その後、ユーザーコードに少し含まれています。

実装

現在のクラスに値を割り当てるコピーコンストラクターについて話していると仮定しましょう。

提供する実装では、2つのケースが考慮されます。

  1. パラメーターはl値であるため、定義により、それに触れることはできません。
  2. パラメータはr値であるため、暗黙的に、temporaryはそれを使用するよりも長くは生存しません。そのため、コンテンツをコピーする代わりに、コンテンツを盗むことができます。

どちらもオーバーロードを使用して実装されます。

Box::Box(const Box & other)
{
   // copy the contents of other
}

Box::Box(Box && other)
{
   // steal the contents of other
}

ライトクラスの実装

クラスに2つの整数が含まれているとしましょう:それらは単純な生の値であるため、それらをstealすることはできません。 seemstealingのように値をコピーするだけです、元の値をゼロなどに設定します...これは単純な整数には意味がありません。なぜその余分な仕事をするのですか?

したがって、ライト値クラスの場合、実際には2つの特定の実装(l値用とr値用)を提供することは意味がありません。

L値の実装のみを提供するだけで十分です。

より重いクラスの実装

しかし、いくつかの重いクラス(つまり、std :: string、std :: mapなど)の場合、コピーは、通常は割り当てでの潜在的なコストを意味します。したがって、理想的には、できる限り避けたいと思います。ここで、stealing一時データが興味深いものになります。

Boxには、コピーにコストがかかるHeavyResourceへの生のポインターが含まれていると想定します。コードは次のようになります。

Box::Box(const Box & other)
{
   this->p = new HeavyResource(*(other.p)) ; // costly copying
}

Box::Box(Box && other)
{
   this->p = other.p ; // trivial stealing, part 1
   other.p = nullptr ; // trivial stealing, part 2
}

1つのコンストラクター(コピーコンストラクター、割り当てを必要とする)は、別のコンストラクター(移動コンストラクター、生のポインターの割り当てのみを必要とする)よりもかなり遅いです。

「盗む」のはいつ安全ですか?

重要なのは、デフォルトでは、パラメーターが一時的な場合にのみコンパイラーが「高速コード」を呼び出すことです(もう少し微妙ですが、我慢してください...)。

どうして?

コンパイラーは、オブジェクトが一時的である(またはいずれにせよすぐに破棄される)場合、問題なくオブジェクトを盗むことができることを保証できるためonly。他のオブジェクトの場合、スチールとは、有効であるが不特定の状態にあるオブジェクトが突然あることを意味します。クラッシュまたはバグにつながる可能性があります:

Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething();         // Oops! You are using an "empty" object!

ただし、パフォーマンスが必要な場合もあります。それで、どのようにそれをしますか?

ユーザーコード

あなたが書いたように:

Box box1 = some_value;
Box box2 = box1;            // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???

Box2で起こることは、box1がl値であるため、最初の「遅い」コピーコンストラクターが呼び出されることです。これは通常のC++ 98コードです。

さて、box3では、何か面白いことが起こります:std :: moveは、同じbox1を返しますが、l値ではなくr値参照として返します。だから行:

Box box3 = ...

... box1でcopy-constructorを呼び出しません。

INSTEADを呼び出して、box1のスチールコンストラクター(正式にはmove-constructor)を呼び出します。

また、Boxのmoveコンストラクターの実装がbox1のコンテンツを「スチール」するため、式の最後で、box1は有効ですが指定されていない状態(通常は空)になり、box3には(前の) box1のコンテンツ。

移転されたクラスの有効だが指定されていない状態はどうですか?

もちろん、l-valueにstd :: moveを記述することは、そのl-valueを再び使用しないという約束をすることを意味します。または、非常に慎重に行います。

C++ 17標準ドラフトの引用(C++ 11は17.6.5.15):

20.5.5.15ライブラリタイプの移動元の状態[lib.types.movedfrom]

C++標準ライブラリで定義されている型のオブジェクトは、(15.8)から移動できます。移動操作は明示的に指定することも、暗黙的に生成することもできます。特に指定がない限り、そのような移動元オブジェクトは、有効だが指定されていない状態に置かれます。

これは標準ライブラリの型に関するものでしたが、これは独自のコードで従うべきものです。

つまり、移動された値は、空、ゼロ、またはランダムな値から任意の値を保持できるということです。例えば。実装者が適切な解決策であると感じた場合、文字列「Hello」は空の文字列「」になるか、「Hell」または「さようなら」になります。ただし、すべての不変条件が考慮された有効な文字列でなければなりません。

したがって、最終的に、(タイプの)実装者が移動後に特定の動作を明示的にコミットしない限り、nothingのように行動する必要があります(そのタイプの)移動された値について。

結論

上記のように、std :: moveはnothingを行います。コンパイラーに次のように伝えるだけです:「そのl値が表示されますか?少しだけr値と考えてください」。

だから、で:

Box box3 = std::move(box1); // ???

...ユーザーコード(つまり、std :: move)は、この式のr値と見なすことができるパラメーターをコンパイラーに伝え、したがって、移動コンストラクターが呼び出されます。

コード作成者(およびコードレビュアー)にとって、コードは実際には、box1のコンテンツを盗んで、box3に移動してもかまいません。コードの作成者は、box1が使用されないようにする(または非常に慎重に使用する)必要があります。それは彼らの責任です。

しかし、最終的には、主にパフォーマンスに違いをもたらすのは、移動コンストラクターの実装です。移動コンストラクターが実際にr値の内容を盗む場合、違いがわかります。それが他に何かをしたら、著者はそれについて嘘をついたが、これは別の問題である...

49
paercebal

実装がすべてです。単純な文字列クラスを検討してください。

class my_string {
  char* ptr;
  size_t capacity;
  size_t length;
};

copyのセマンティクスでは、動的メモリ内の別の配列の割り当てと、*ptrコンテンツのコピーを含む文字列の完全なコピーを作成する必要がありますが、これは高価です。

moveのセマンティクスでは、stringの内容を複製せずに、ポインター自体の値を新しいオブジェクトに転送するだけで済みます。

もちろん、クラスが動的メモリまたはシステムリソースを使用しない場合、パフォーマンスに関して移動とコピーの間に違いはありません。

15
gudok

std::move()関数は、対応する右辺値型に対するcastとして理解される必要があります。つまり、enablesコピーする代わりにオブジェクト。


まったく違いはないかもしれません:

_std::cout << std::move(std::string("Hello, world!")) << std::endl;
_

ここでは、文字列はすでに右辺値であるため、std::move()は何も変更しませんでした。


移動は可能かもしれませんが、それでもコピーが発生する可能性があります。

_auto a = 42;
auto b = std::move(a);
_

整数を単純にコピーする効率的な方法はありません。


それがwill移動を引き起こす場所は、引数が

  1. 左辺値、または左辺値参照です。
  2. move constructorまたはmove assignment演算子、および
  3. (暗黙的または明示的に)構築または割り当てのソースです。

この場合でも、実際にデータを移動するのはmove()自体ではなく、データを移動するのは、構築または割り当てです。 std:move()は、開始する左辺値がある場合でも、それを可能にする単純なキャストです。また、右辺値で開始した場合、_std::move_がなくても移動できます。それがマイヤーズの声明の背後にある意味だと思います。

1
Toby Speight