C ++での移動コンストラクターの動機付けと使用


17

私は最近、C ++の移動コンストラクターについて読んでいます(たとえば、こちらを参照)。それらがどのように機能し、いつ使用するかを理解しようとしています。

私の知る限り、移動コンストラクターは、大きなオブジェクトのコピーによって引き起こされるパフォーマンスの問題を軽減するために使用されます。ウィキペディアのページには、「C ++ 03の慢性的なパフォーマンスの問題は、オブジェクトが値渡しされたときに暗黙的に発生する可能性のある、コストが高く不必要なディープコピーです。」

私は通常そのような状況に対処します

  • オブジェクトを参照渡しすることにより、または
  • スマートポインター(boost :: shared_ptrなど)を使用してオブジェクトをやり取りします(オブジェクトの代わりにスマートポインターがコピーされます)。

上記の2つの手法では不十分で、移動コンストラクターを使用するほうが便利な状況は何ですか?


1
移動セマンティクスがはるかに達成できるという事実に加えて(答えで述べたように)、参照渡しまたはスマートポインター渡しでは不十分な状況を尋ねるべきではありませんが、それらの手法が本当に最良で最もクリーンな方法である場合そうするために(shared_ptr高速コピーのためだけに注意してください)、ムーブセマンティクスがコーディング、セマンティクス、クリーンさのペナルティーをほとんど伴わずに同じことを達成できる場合。
クリスは、モニカを復活させる

回答:


16

移動セマンティクスは、C ++に次元全体を導入します-値を安く返せるようにするだけではありません。

たとえば、move-semantics std::unique_ptrがないと動作しません-を参照してくださいstd::auto_ptr。これはmove-semanticsの導入で非推奨になり、C ++ 17で削除されました。リソースの移動は、コピーとは大きく異なります。一意のアイテムの所有権を譲渡できます。

たとえば、std::unique_ptrかなりよく議論されているので、見てはいけません。たとえば、OpenGLの頂点バッファーオブジェクトを見てみましょう。頂点バッファーはGPU上のメモリを表します-特別な関数を使用して割り当ておよび割り当て解除する必要があります。おそらく、存続可能期間に厳しい制約があります。また、1人の所有者のみがそれを使用することも重要です。

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

現在、これstd::shared_ptr - で実行できますが、このリソースは共有されません。これにより、共有ポインタを使用するのが混乱します。を使用することもできますstd::unique_ptrが、それでも移動のセマンティクスが必要です。

明らかに、私は移動コンストラクターを実装していませんが、あなたはそのアイデアを得ます。

ここで関連することは、いくつかのリソースがコピーできないということです。移動する代わりにポインターを渡すことができますが、unique_ptrを使用しない限り、所有権の問題があります。コードの意図をできるだけ明確にすることは価値があります。そのため、おそらくムーブコンストラクターが最善のアプローチです。


答えてくれてありがとう。ここで共有ポインタを使用するとどうなりますか?
ジョルジオ

私は自分自身に答えようとします:共有ポインタを使用すると、オブジェクトの寿命を制御することはできませんが、オブジェクトは特定の時間だけ生きることができる必要があります。
ジョルジオ

3
@Giorgio 共有ポインターを使用できますが、意味的に間違っています。バッファを共有することはできません。また、これにより、本質的にポインターをポインターに渡すことができます(vboは基本的にGPUメモリへの一意のポインターであるため)。後でコードを表示している人は、「ここに共有ポインターがあるのはなぜですか?共有リソースですか?それはバグかもしれません!」元の意図が何であったかをできるだけ明確にすることが望ましいです。
マックス

@Giorgioはい、それも要件の一部です。この場合の「レンダラー」がリソースの割り当てを解除する場合(GPU上の新しいオブジェクトに十分なメモリがない可能性があります)、メモリへの他のハンドルがあってはなりません。範囲外に渡されるshared_ptrを使用することは、他の場所に保持しない場合は機能しますが、可能な場合は完全に明らかにしないのはなぜですか?
マックス

@Giorgio明確にするための別の試みについては、私の編集をご覧ください。
マックス

5

移動セマンティクスは、値を返す際に必ずしもそれほど大きな改善とは限りませshared_ptrん。また、おそらく(または類似の)を使用する場合は、早すぎるペシマイズになります。現実には、ほぼすべての合理的に最新のコンパイラーは、Return Value Optimization(RVO)およびNamed Return Value Optimization(NRVO)と呼ばれるものを実行します。つまり、実際に値コピーするのではなく、値を返すときに、戻り値の後に値が割り当てられる場所への非表示のポインタ/参照を渡すだけで、関数はそれを使用して最終的に値を作成します。C ++標準にはこれを許可する特別な規定が含まれているため、たとえコピーコンストラクターに目に見える副作用があったとしても、値を返すためにコピーコンストラクターを使用する必要はありません。例えば:

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

ここでの基本的な考え方は非常に単純です:可能な場合はコピーを避けたい(コンテンツにstd::vector32767のランダムな整数を入れて)十分なコンテンツを持つクラスを作成します。明示的なコピーアクターがあり、コピーされた場合/コピーされた場合に表示されます。また、オブジェクト内のランダムな値を使用して何かを行うためのコードがもう少しあります。そのため、オプティマイザーは、クラスが何もしないからといって、クラスに関するすべてを削除しません

次に、関数からこれらのオブジェクトの1つを返すコードを用意し、合計を使用して、オブジェクトが完全に無視されるだけでなく、実際に作成されるようにします。はい、私はかなり確信してしても、高速コピーがあることだ-私たちはそれを実行すると、少なくとも最近の/近代的なコンパイラで、我々は我々が書いたコピーコンストラクタが、すべてで動作しないことを見つけるshared_ptr何のコピーをやっていないよりもまだ遅いですまったく。

移動すると、それなしでは(直接)できなかったかなりの数のことができます。外部マージソートの「マージ」部分を考えてみましょう。たとえば、一緒にマージする8つのファイルがあるとします。理想的にはあなたにこれらのファイルのすべての8を入れたいのですがvector-ので、しかしvector(のように、C ++ 03)は要素をコピーできるようにする必要があり、そしてifstreamsはあなたには、いくつかとしている立ち往生し、コピーすることはできませんunique_ptr/ shared_ptr、またはその順序で何かをベクトルに入れることができます。なお、でも私たちの(例えば)の場合reserveのスペースvector私たちは必ず私たちのしているので、ifstreamコードはいてもコンパイルされませんので、sが実際にコピーされることはありません、コンパイラはそれを知ることができません我々はコピーコンストラクタはなることはありません知っていますとにかく使用されます。

まだコピーできませんが、C ++ 11では移動ifstream できます。この場合、オブジェクトはおそらく移動されませんが、必要に応じてコンパイラーを満足させることができるので、スマートポインターハッキングなしでifstreamオブジェクトをvector直接配置できます。

ベクトルませ拡大は、移動の意味が本当に/かかわらず便利ですできる時間のかなりまともな例です。この場合、関数からの戻り値(または非常に類似したもの)を処理していないため、RVO / NRVOは役に立ちません。オブジェクトを保持するベクターが1つあり、それらのオブジェクトを新しい大きなメモリチャンクに移動する必要があります。

C ++ 03では、新しいメモリにオブジェクトのコピーを作成してから、古いメモリの古いオブジェクトを破壊することでそれを行いました。ただし、古いコピーを破棄するためだけにすべてのコピーを作成するのは、時間の無駄です。C ++ 11では、代わりに移動することが期待できます。これにより、通常、本質的に、(一般的にはるかに遅い)ディープコピーの代わりにシャローコピーを実行できます。言い換えれば、文字列またはベクトルを使用して(ほんの2、3の例のみ)、ポインターが参照するすべてのデータのコピーを作成するのではなく、オブジェクト内のポインターをコピーするだけです。


非常に詳細な説明をありがとう。正しく理解すれば、動きが発生するすべての状況は通常のポインターで処理できますが、毎回すべてのポインターをジャグリングするのは安全ではありません(複雑でエラーが発生しやすい)。そのため、代わりに、内部にunique_ptr(または同様のメカニズム)があり、移動セマンティクスにより、1日の終わりにはポインターのコピーのみが行われ、オブジェクトのコピーは行われません。
ジョルジオ

@Giorgio:はい、それはほとんど正しいです。この言語は実際には移動セマンティクスを追加しません。右辺値参照を追加します。右辺値参照(明らかに)は右辺値にバインドできます。この場合、データの内部表現を「盗み」、ディープコピーを行う代わりにポインタをコピーするだけで安全であることがわかります。
ジェリーコフィン

4

考慮してください:

vector<string> v;

vに文字列を追加すると、必要に応じて展開され、再割り当てのたびに文字列をコピーする必要があります。移動コンストラクターでは、これは基本的に問題ではありません。

もちろん、次のようなこともできます。

vector<unique_ptr<string>> v;

しかし、それはstd::unique_ptrムーブコンストラクターを実装するためだけにうまく機能します。

std::shared_ptr所有権を実際に共有している(まれな)状況でのみ使用することは理にかなっています。


しかし、30個のデータメンバーがstringあるFoo場所のインスタンスがあった場合はどうでしょうか。unique_ptrバージョンは、より効率的ではないでしょうか?
ヴァシリス

2

戻り値は、ある種の参照の代わりに値で渡すことを最もよく望む場所です。パフォーマンスを大幅に低下させることなく、オブジェクトを「スタック上」に迅速に返すことができれば嬉しいです。一方、これを回避するのは特に難しいことではありません(共有ポインターの使い方はとても簡単です...)。そのため、オブジェクトで余分な作業を行うだけの価値があるかどうかはわかりません。


また、通常、スマートポインターを使用して、関数/メソッドから返されるオブジェクトをラップします。
ジョルジオ

1
@Giorgio:それは間違いなく難読化と低速の両方です。
DeadMG

あなたは、単純なオン・ザ・スタックオブジェクトを返す場合現代のコンパイラは、そうで共有PTRSなどの必要がない、自動移動を実行すべきである
クリスチャン・セヴェリン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.