オブジェクトをコピーするよりも移動する方が速いのはなぜですか? 質問する

オブジェクトをコピーするよりも移動する方が速いのはなぜですか? 質問する

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);

lvalue と rvalue のルールは理解していますが、メモリ内で実際に何が起こっているのか理解できません。単に値を別の方法でコピーしているだけなのか、アドレスを共有しているだけなのか、それとも何か他のことなのか。もっと具体的に言うと、移動がコピーよりも高速になる理由は何でしょうか。

これを理解すればすべてが明らかになると思います。よろしくお願いします!

編集:std::move()実装や構文に関することについては質問していないことに注意してください。

ベストアンサー1

としてグドク 以前に回答したすべては実装にあります...そして少しはユーザー コードにあります。

実装

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

提供する実装では、次の 2 つのケースを考慮します。

  1. パラメータは左辺値なので、定義上は変更できません。
  2. パラメータはr値なので、暗黙的に、一時変数はそれを使用した後はあまり長くは存続しないので、その内容をコピーする代わりに、その内容を盗むことができます。

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

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

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

軽量クラスの実装

クラスに2つの整数が含まれているとします。窃盗これらは単純な生の値だからです。思われるのように窃盗値をコピーして、元の値をゼロに設定するなどです...これは単純な整数では意味がありません。なぜそのような余分な作業を行うのでしょうか?

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

l 値の実装のみを提供すれば十分でしょう。

より重いクラスの実装

しかし、重いクラス(std::string、std::mapなど)の場合、コピーは潜在的にコスト(通常は割り当て)を伴います。したがって、理想的には、可能な限りコピーを避ける必要があります。ここで窃盗一時データから得られるデータが興味深いものになります。

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
}

あるコンストラクター (割り当てを必要とするコピー コンストラクター) が別のコンストラクター (生のポインターの割り当てのみを必要とする移動コンストラクター) よりもはるかに遅いことは明らかです。

「盗む」のはいつが安全でしょうか?

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

なぜ?

コンパイラは、問題なくオブジェクトから盗むことができることを保証できるからですのみそのオブジェクトが一時的なものである場合(またはいずれにせよすぐに破棄される場合)は、盗むことは不可能です。他のオブジェクトの場合、盗むということは、有効ではあるが不特定の状態のオブジェクトが突然手に入ることを意味し、コードのさらに下ではまだ使用される可能性があります。クラッシュやバグにつながる可能性があります。

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 は左辺値なので、最初の「遅い」コピー コンストラクターが呼び出されます。これは通常の C++98 コードです。

さて、box3 では面白いことが起こります。std::move は同じ box1 を返しますが、左辺値ではなく右辺値参照として返します。つまり、次の行になります。

Box box3 = ...

...はbox1でコピーコンストラクターを呼び出しません。

これは、box1 のスティーリング コンストラクター (正式には移動コンストラクターと呼ばれます) の代わりに呼び出されます。

また、Box の移動コンストラクターの実装では、式の最後で box1 の内容を「盗む」ため、box1 は有効ではあるが指定されていない状態 (通常は空) になり、box3 には box1 の (以前の) 内容が含まれます。

移動されたクラスの有効だが未指定の状態はどうなりますか?

もちろん、左辺値に std::move を記述するということは、その左辺値を再度使用しないことを約束することを意味します。あるいは、非常に慎重に使用することになります。

C++17 標準ドラフト (C++11 は 17.6.5.15) を引用します。

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

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

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

つまり、移動された値は、空、ゼロ、またはランダムな値など、任意の値を保持できるということです。たとえば、実装者が正しい解決策だと感じれば、文字列「Hello」は空の文字列「」になったり、「Hell」になったり、「Goodbye」になったりするかもしれません。ただし、すべての不変条件が尊重された有効な文字列である必要があります。

つまり、結局のところ、実装者(型)が移動後に特定の動作を明示的にコミットしない限り、あなたは知っているかのように行動するべきです。何もない移動された値(そのタイプ)について。

結論

上で述べたように、std::moveは何もないコンパイラーに「この l-値が見えますか? ちょっとの間、これを r-値として考えてください」と伝えるだけです。

つまり、

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

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

コード作成者 (およびコード レビュー担当者) にとって、コードは実際には、box1 の内容を盗んで box3 に移動しても問題ないことを伝えています。コード作成者は、box1 が今後使用されないように (または非常に慎重に使用されるように) する必要があります。これは作成者の責任です。

しかし、最終的には、主にパフォーマンスにおいて、移動コンストラクターの実装が違いを生みます。移動コンストラクターが実際に r 値の内容を盗む場合、違いが見られます。それ以外のことを行う場合、作成者はそれについて嘘をついていることになりますが、これは別の問題です...

おすすめ記事