web-dev-qa-db-ja.com

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

私は最近C++でのムーブコンストラクター(例: here を参照)について読んでおり、それらがどのように機能し、いつ使用する必要があるかを理解しようとしています。

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

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

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

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

17
Giorgio

移動セマンティクスは、C++に次元全体を導入します。値を安価に返すためだけに存在するわけではありません。

たとえば、移動セマンティクスなしでは _std::unique_ptr_ は機能しません-移動セマンティクスの導入で廃止された _std::auto_ptr_ を見て、 C++ 17で削除されました。リソースの移動は、リソースのコピーとは大きく異なります。一意のアイテムの所有権を譲渡できます。

たとえば、_std::unique_ptr_はかなりよく議論されているので、見てはいけません。たとえば、OpenGLのVertex Bufferオブジェクトを見てみましょう。頂点バッファは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_を使用することもできますが、それでも移動のセマンティクスが必要です。

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

ここで重要なのは、一部のリソースはコピーできないということです。ポインタを移動する代わりに渡すことができますが、unique_ptrを使用しない限り、所有権の問題があります。コードの目的が何であるかをできるだけ明確にすることは価値があるため、おそらくmove-constructorが最善のアプローチです。

16
Max

値を返す場合、移動のセマンティクスは必ずしもそれほど大きく改善されるわけではありません。また、shared_ptr(または類似の何か)を使用する場合は、おそらく時期尚早に悲観的になるでしょう。実際には、ほぼすべての合理的に現代のコンパイラーは、戻り値の最適化(RVO)および名前付き戻り値の最適化(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::vectorは32767個のランダムな整数で埋めます)。コピーされたときに、またはコピーされた場合に表示する明示的なコピーActorがあります。また、オブジェクトのランダムな値を使って何かを実行するためのコードが少しあるので、オプティマイザはクラスを何もしないからといって、クラスに関するすべてを(少なくとも簡単に)削除しません。

次に、関数からこれらのオブジェクトの1つを返すコードをいくつか用意し、合計を使用して、オブジェクトが完全に無視されるだけでなく、実際に作成されていることを確認します。これを実行すると、少なくとも最近の/最新のコンパイラでは、作成したコピーコンストラクタがまったく実行されないことがわかります。そして、確かに、shared_ptrを使用した高速コピーでも、まったくコピーしないよりも遅い。

移動することで、それなしでは(直接)実行できなかった多くのことが実行できます。外部マージソートの「マージ」部分を考えてみます。たとえば、マージするファイルが8つあるとします。これらの8つのファイルすべてをvectorに配置するのが理想的ですが、vector(C++ 03以降)は要素をコピーできる必要があるため、ifstreamsはコピーできません。いくつかのunique_ptr/shared_ptr、またはそれらをベクターに入れるためにその順序で動かなくなっています。 (たとえば)reservevectorスペースがある場合でも、ifstreamsが実際にコピーされることはないと確信している場合でも、コンパイラーはそれを認識しません。したがって、たとえweであってもコピーコンストラクタは決して使用されないことがわかっていても、コードはコンパイルされません。

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

doesを展開するベクトルは、移動のセマンティクスが実際に役立つ/役立つ場合のかなり適切な例です。この場合、関数(または非常によく似たもの)からの戻り値を処理していないため、RVO/NRVOは役に立ちません。いくつかのオブジェクトを保持する1つのベクトルがあり、それらのオブジェクトを新しい大きなメモリチャンクに移動したいとします。

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

5
Jerry Coffin

考慮してください:

vector<string> v;

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

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

vector<unique_ptr<string>> v;

ただし、std::unique_ptrがmoveコンストラクターを実装しているため、これはうまく機能します。

std::shared_ptrの使用は、実際に所有権を共有している(まれな)状況でのみ意味があります。

4

戻り値は、ある種の参照ではなく、値で渡したい場合が最も多い場所です。パフォーマンスを大幅に低下させることなく、オブジェクトを「スタック上」ですばやく返すことができるといいですね。一方、これを回避することは特に難しくありません(共有ポインターは非常に使いやすい...)ので、これを可能にするためだけにオブジェクトに追加の作業を行う価値があるかどうかはわかりません。

2
Michael Kohne