web-dev-qa-db-ja.com

移動意味論とは何ですか?

C++ 0x について、Software Engineeringのラジオ Scott Meyersとのポッドキャストインタビュー を聞いたばかりです。新機能の大部分は私にとって意味があり、私は実際にC++ 0xに興奮しています。私はまだ セマンティクスを動かさない ...正確には何ですか?

1538
dicroce

サンプルコードを使用すると、移動のセマンティクスを理解するのが最も簡単です。ヒープに割り当てられたメモリブロックへのポインタのみを保持する非常に単純な文字列クラスから始めましょう。

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

メモリを自分で管理することを選択したため、 rule of three に従う必要があります。ここでは、代入演算子の作成を延期し、デストラクタとコピーコンストラクタのみを実装します。

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

コピーコンストラクターは、文字列オブジェクトのコピーの意味を定義します。パラメーターconst string& thatは、次の例でコピーを作成できる文字列型のすべての式にバインドします。

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

次に、移動セマンティクスに関する重要な洞察が得られます。 xをコピーする最初の行でのみ、このディープコピーが本当に必要であることに注意してください。これは、後でxを調べたい場合があり、xが何らかの形で変更された場合は非常に驚かされるためですxを3回(この文を含めると4回)言い、毎回exact same objectを意味していることに気づきましたか? xなどの式を「lvalues」と呼びます。

2行目と3行目の引数は左辺値ではなく右辺値です。これは、基になる文字列オブジェクトに名前がないため、クライアントが後でそれらを再度検査する方法がないためです。右辺値は、次のセミコロンで破壊される一時オブジェクトを示します(より正確には、右辺値を字句的に含む完全式の最後)。これは、bおよびcの初期化中に、ソース文字列を使用して必要なことを行うことができ、クライアントが違いを認識できなかった!であるため重要です。

C++ 0xでは、「rvalue reference」と呼ばれる新しいメカニズムが導入されました。これにより、とりわけ、関数のオーバーロードを介して右辺値引数を検出できます。私たちがしなければならないのは、右辺値参照パラメーターを持つコンストラクターを書くことです。そのコンストラクター内で、ソースを使用してsomethingを行うことができます。ただし、someの有効な状態のままにしておきます。

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

ここで何をしましたか?ヒープデータを深くコピーする代わりに、ポインタをコピーしてから元のポインタをnullに設定しました(ソースオブジェクトのデストラクタからの「delete []」が「盗まれたデータ」を解放しないようにします)。実際には、元の文字列に属していたデータを「盗み」ました。繰り返しますが、重要な洞察は、いかなる状況下でも、クライアントがソースが変更されたことを検出できないということです。ここでは実際にコピーを行わないため、このコンストラクターを「移動コンストラクター」と呼びます。その仕事は、リソースをコピーするのではなく、あるオブジェクトから別のオブジェクトに移動することです。

おめでとうございます、これでムーブセマンティクスの基本を理解できました!代入演算子を実装して続けましょう。 コピーアンドスワップイディオム に慣れていない場合は、例外の安全性に関連する素晴らしいC++イディオムであるため、学習して戻ってください。

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

ほら、それだけ? 「右辺値参照はどこにありますか?」あなたが尋ねるかもしれません。 「ここでは必要ありません!」私の答えです:)

パラメータthatby valueを渡すため、thatは他の文字列オブジェクトと同様に初期化する必要があります。 thatはどのように初期化されますか? C++ 98 の昔、答えは「コピーコンストラクターによる」でした。 C++ 0xでは、コンパイラーは、代入演算子への引数が左辺値であるか右辺値であるかに基づいて、コピーコンストラクターと移動コンストラクターを選択します。

したがって、a = bと言うと、コピーコンストラクターthatを初期化し(式bは左辺値であるため)、代入演算子は新しく作成されたディープコピーと内容を交換します。これがまさにコピーとスワップのイディオムの定義です。コピーを作成し、内容をコピーと交換し、スコープを離れてコピーを取り除きます。ここに新しいものはありません。

ただし、a = x + yと言うと、move constructorthatを初期化します(式x + yは右辺値であるため)。したがって、深いコピーは含まれず、効率的な移動のみです。 thatはまだ引数から独立したオブジェクトですが、ヒープデータをコピーする必要はなく、移動するだけなので、その構築は簡単でした。 x + yは右辺値であるため、コピーする必要はありませんでした。また、右辺値で示される文字列オブジェクトから移動してもかまいません。

要約すると、コピーコンストラクターはディープコピーを作成します。これは、ソースが変更されないようにするためです。一方、移動コンストラクターは、ポインターをコピーして、ソース内のポインターをnullに設定するだけです。クライアントにはオブジェクトを再度検査する方法がないため、この方法でソースオブジェクトを「無効化」してもかまいません。

この例が主要なポイントになったことを願っています。参照を右クリックし、セマンティクスを移動するには、単純にするために意図的に省略したものがまだあります。詳細が必要な場合は、 私の補足回答 をご覧ください。

2326
fredoverflow

移動の意味は右辺値参照に基づいています。
右辺値は一時的なオブジェクトであり、式の終わりに破壊されます。現在のC++では、右辺値はconst参照にのみバインドされます。 C++ 1xでは、非const右辺値の参照(T&&)を使用できます。これは右辺値オブジェクトへの参照です。
式の終わりに右辺値が消滅しようとしているので、あなたはそのデータを盗むすることができます。別のオブジェクトにコピーする代わりに、そのオブジェクトにデータを移動します。

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

上記のコードでは、古いコンパイラでは、xのコピーコンストラクタを使用して、_ f()の結果はコピーinto Xになります。コンパイラが移動セマンティクスをサポートし、Xに移動コンストラクタがある場合は、代わりにそれが呼び出されます。そのrhs引数はrvalueなので、もはや必要ではなく、その値を盗むことができます。
そのため、値は移動済みとなり、_ f()からxに返されます(xのデータは空のXは、一時的なものに移動され、割り当て後に破棄されます。

74
sbi

実体を返す関数があるとします。

Matrix multiply(const Matrix &a, const Matrix &b);

あなたがこのようなコードを書くとき:

Matrix r = multiply(a, b);

その後、通常のC++コンパイラはmultiply()の結果の一時オブジェクトを作成し、コピーコンストラクタを呼び出してrを初期化し、次に一時戻り値を破棄します。 C++ 0xの移動セマンティクスでは、 "moveコンストラクタ"を呼び出してその内容をコピーすることによってrを初期化し、次に一時値を破棄せずに破棄することができます。

これは、(おそらく上のMatrixの例のように)コピーされるオブジェクトがその内部表現を格納するためにヒープ上に追加のメモリを割り当てる場合に特に重要です。コピーコンストラクタは、内部表現のフルコピーを作成するか、または参照カウントとコピーオンライトセマンティクスを対話的に使用する必要があります。移動コンストラクタは、ヒープメモリをそのままにして、ポインタをMatrixオブジェクト内にコピーするだけです。

58
Greg Hewgill

もしあなたが本当にMove Semanticsの詳細な説明に興味があるなら、私はそれらについての原著論文を読むことを強くお勧めします、 "Move SemanticsサポートをC++言語に追加する提案"

それは非常にアクセスしやすく、そして読みやすいです、そしてそれは彼らが提供する利点のために優れたケースを作ります。 WG21ウェブサイト で利用可能な移動意味論に関する他のより最近のそして最新の論文がありますが、これはトップレベルのビューから物事に近づき、あまり得られないのでおそらくこれはおそらく最も簡単です。ざらざらした言語の詳細に。

30
James McNellis

移動セマンティクス について - リソースをコピーするのではなく転送する 誰もソース値を必要としない場合 - .

C++ 03では、オブジェクトはしばしばコピーされ、コードがその値を再び使用する前に破棄されたり割り当てられたりします。たとえば、RVOが起動しない限り、関数から値で戻ると、返す値は呼び出し元のスタックフレームにコピーされ、その後スコープ外になり破棄されます。これは多くの例の1つにすぎません。ソースオブジェクトが一時的な場合は値渡し、項目を並べ替えるだけのsortのようなアルゴリズム、capacity()を超えた場合のvectorの再割り当てなどを参照してください。

そのようなコピー/破棄のペアが高価になるとき、それは一般的にオブジェクトがいくつかのヘビー級のリソースを所有しているためです。たとえば、vector<string>は、それぞれ独自の動的メモリーを持つstringオブジェクトの配列を含む、動的に割り当てられたメモリーブロックを所有する場合があります。このようなオブジェクトをコピーするのはコストがかかります。ソース内の動的に割り当てられたブロックごとに新しいメモリを割り当て、すべての値をコピーする必要があります。 次に コピーしたメモリすべての割り当てを解除する必要があります。ただし、 moving 大きなvector<string>は、(動的メモリブロックを参照する)いくつかのポインタをコピー先にコピーし、それらをソース内で消去することを意味します。

26
Dave Abrahams

簡単な(実用的な)用語では:

オブジェクトをコピーするとは、その「静的」メンバーをコピーし、その動的オブジェクトに対してnew演算子を呼び出すことを意味します。右?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

しかし、オブジェクトを move とすると(実際には繰り返しますが)、動的オブジェクトのポインタをコピーするだけで、新しいオブジェクトを作成することはできません。

しかし、それは危険ではないですか?もちろん、動的オブジェクトを2回破壊することもできます(セグメンテーション違反)。そのため、それを回避するには、ソースポインタを「無効化」して2回破壊しないようにする必要があります。

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

わかりました、しかし、オブジェクトを移動すると、ソースオブジェクトは役に立たなくなります。もちろん、しかし特定の状況ではそれは非常に便利です。最も明白なものは、私が無名オブジェクト(テンポラル、右辺値オブジェクト、...、あなたはそれを異なる名前で呼び出すことができる)で関数を呼び出すときです。

void heavyFunction(HeavyType());

その場合、無名オブジェクトが作成され、次にfunctionパラメータにコピーされ、その後削除されます。匿名オブジェクトは不要で、時間とメモリを節約できます。

これは「右辺値」参照の概念につながります。それらは、受信したオブジェクトが匿名かどうかを検出するためだけにC++ 11に存在します。 "左辺値"が代入可能な実体(=演算子の左部分)であることはすでにご存知だと思いますので、左辺値として機能するには、オブジェクトへの名前付き参照が必要です。右辺値は正反対の、名前付き参照を持たないオブジェクトです。そのため、無名オブジェクトと右辺値は同義語です。そう:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

この場合、タイプAのオブジェクトを「コピー」する必要がある場合、コンパイラーは渡されたオブジェクトが名前付きかどうかに応じて左辺値参照または右辺値参照を作成します。そうでない場合は、あなたのmove-constructorが呼ばれ、オブジェクトが一時的であることを知っているので、動的オブジェクトをコピーする代わりに動かすことができ、スペースとメモリを節約できます。

「静的」オブジェクトは常にコピーされることを覚えておくことが重要です。静的オブジェクト(スタック内のオブジェクトでヒープ上ではない)を「移動」する方法はありません。そのため、オブジェクトに動的メンバーがない場合(直接的または間接的)の「移動」/「コピー」の区別は関係ありません。

オブジェクトが複雑で、デストラクタがライブラリの関数の呼び出し、他のグローバル関数の呼び出しなど、他の副次的効果を持っている場合は、フラグを付けて移動を通知するほうが良いでしょう。

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

そのため、コードは短く(各動的メンバーに対してnullptr代入を行う必要はありません)、より一般的です。

他の典型的な質問:A&&const A&&の違いは何ですか?もちろん、前者の場合はオブジェクトを変更できますが、後者の場合は変更できませんが、実際的な意味はありますか。後者の場合、それを変更することはできません。そのため、オブジェクトを無効にする方法はなく(可変フラグなどを除く)、コピーコンストラクタに実質的な違いはありません。

そして 完全転送 とは何ですか? 「右辺値参照」は「呼び出し元のスコープ」内の名前付きオブジェクトへの参照であることを知っておくことは重要です。しかし、実際のスコープでは、右辺値参照はオブジェクトへの名前なので、名前付きオブジェクトとして機能します。あなたが他の関数への右辺値参照を渡すならば、あなたは名前付きオブジェクトを渡しているので、そのオブジェクトは時間的オブジェクトのように受け取られません。

void some_function(A&& a)
{
   other_function(a);
}

オブジェクトaother_functionの実パラメータにコピーされます。オブジェクトaを一時オブジェクトとして扱い続けたい場合は、std::move関数を使用する必要があります。

other_function(std::move(a));

この行では、std::moveaを右辺値にキャストし、other_functionはそのオブジェクトを名前のないオブジェクトとして受け取ります。もちろん、other_functionが名前のないオブジェクトを扱うための特定のオーバーロードをしていない場合、この区別は重要ではありません。

それは完璧な転送ですか?そうではありませんが、私たちは非常に近いです。完全転送はテンプレートを扱うためだけに有用です。目的を別の関数に渡す必要がある場合、名前付きオブジェクトを受け取る場合はそのオブジェクトは名前付きオブジェクトとして渡される必要があります。名前のないオブジェクトのように渡したいです。

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

これは、C++ 11でstd::forwardを使って実装された、完全転送を使うプロトタイプ関数のシグネチャです。この関数はテンプレートのインスタンス化のいくつかの規則を利用します。

 `A& && == A&`
 `A&& && == A&&`

したがって、TA _ t _ = A&)への左辺値参照である場合、aも( A& && => A&)になります。 TAへの右辺値参照である場合は、aも(A && && => A &&)です。どちらの場合も、aは実際のスコープ内の名前付きオブジェクトですが、Tには呼び出し側スコープの観点から見た「参照型」の情報が含まれています。この情報(T)はテンプレートパラメータとしてforwardに渡され、 'a'はTの型に従って移動されるかどうかにかかわらず移動されます。

23
Peregring-lk

コピーセマンティクスに似ていますが、「移動」元のオブジェクトからデータを盗むために、すべてのデータを複製する必要はありません。

19
Terry Mahaffey

あなたはコピー意味論が正しいことを何を知っていますか?コピー可能な型があることを意味します。これを定義するユーザー定義型の場合は、コピーコンストラクタと代入演算子を明示的に記述して購入するか、コンパイラがそれらを暗黙的に生成します。これでコピーができます。

Move Semanticsは基本的に、constではないr値参照(&&(yes 2アンパサンド)を使用する新しい型の参照)をとるコンストラクタを持つユーザ定義型です。これは、移動演算子と呼ばれます。そのため、moveコンストラクタは、ソース引数からメモリをコピーするのではなく、ソースからデスティネーションにメモリを「移動」します。

いつそれをしたいですか? well std :: vectorは一例です。一時std :: vectorを作成し、それを関数sayから返すとします。

std::vector<foo> get_foos();

Std :: vectorがコピーする代わりに移動コンストラクタを持っていて、ポインタを設定して動的に割り当てられる 'move'であれば、関数が戻るときにコピーコンストラクタからのオーバーヘッドがあるでしょう。新しいインスタンスへのメモリ。これはstd :: auto_ptrを使った所有権譲渡の意味論のようなものです。

13
snk_kid

Move Semanticsの必要性を説明するために、Move Semanticsのないこの例を考えましょう:

これはT型のオブジェクトを受け取り、同じ型Tのオブジェクトを返す関数です。

T f(T o) { return o; }
  //^^^ new object constructed

上記の関数は値による呼び出しを使用しています。つまり、この関数が呼び出されるとき、そのオブジェクトがその関数で使用されるには作成済みでなければなりません。
この関数も値で戻るなので、戻り値に対して別の新しいオブジェクトが構築されます。

T b = f(a);
  //^ new object constructed

2つの newオブジェクトが作成されました。そのうちの1つは、関数の実行中にのみ使用される一時的なオブジェクトです。

新しいオブジェクトが戻り値から作成されるとき、コピーコンストラクタはcopyテンポラリオブジェクトの内容を新しいオブジェクトに呼び出します。関数が完了すると、その関数で使用されている一時オブジェクトは範囲外になり、破棄されます。


それでは、コピーコンストラクタが何をするのか考えてみましょう。

最初にオブジェクトを初期化してから、すべての関連データを古いオブジェクトから新しいオブジェクトにコピーする必要があります。
クラスによっては、非常に多くのデータを格納しているコンテナである場合、timememory usageを表すことがあります。

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

moveセマンティクス を使うと、コピーではなくデータを移動するだけで、この作業の大部分を不快にさせることが可能になりました。

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

データを移動するには、データを新しいオブジェクトに再度関連付ける必要があります。そしてコピーは行われません _まったく。

これはrvalue参照で実現されています。
rvalue参照はlvalue参照とほとんど同じように機能しますが、1つの重要な違いがあります。
右辺値参照は移動できます左辺値は移動できません。

からcppreference.com

強力な例外保証を可能にするために、ユーザー定義の移動コンストラクターは例外をスローしないでください。実際、標準的なコンテナは通常、コンテナ要素を移動する必要があるときに移動とコピーのどちらかを選択するためにstd :: move_if_noexceptに依存しています。コピーコンストラクタと移動コンストラクタの両方が指定されている場合、引数が右辺値(名前のないテンポラリなどのprvalueまたはstd :: moveの結果などのxvalue)である場合はオーバーロード解決によって移動コンストラクタが選択されます。引数は左辺値(名前付きオブジェクト、または左辺値参照を返す関数/演算子)です。コピーコンストラクタのみが提供されている場合、すべての引数カテゴリがそれを選択します(rvaluesがconst参照にバインドできるため、constへの参照を取る限り)。これにより、移動が使用できないときに移動のためのフォールバックのコピーが行われます。多くの場合、動きのコンストラクタは、目に見える副作用があるとしても最適化されています。コンストラクターは、右辺値参照をパラメーターとしてとるとき、「移動コンストラクター」と呼ばれます。何かを移動することは義務付けられていません。クラスが移動されるリソースを持つ必要はなく、 'move constructor'はパラメータが許容される(しかし賢明ではない)場合のようにリソースを移動できないかもしれません。 const右辺値参照(const T &&).

7
Andreas DM

私はそれを正しく理解するためにこれを書いています。

不必要なラージオブジェクトのコピーを避けるために、移動セマンティクスが作成されました。彼の著書「C++プログラミング言語」のBjarne Stroustrupは、デフォルトで不要なコピーが行われる2つの例を使用します。1つは2つのラージオブジェクトのスワップ、もう2つはメソッドからのラージオブジェクトの戻りです。

2つの大きなオブジェクトを交換するには、通常、最初のオブジェクトを一時オブジェクトにコピーし、2番目のオブジェクトを最初のオブジェクトにコピーし、その一時オブジェクトを2番目のオブジェクトにコピーします。組み込み型の場合、これは非常に高速ですが、大きなオブジェクトの場合、これら3つのコピーにはかなりの時間がかかります。 「移動代入」を使用すると、プログラマはデフォルトのコピー動作をオーバーライドし、代わりにオブジェクトへの参照を交換できます。つまり、コピーはまったく行われず、交換操作ははるかに高速になります。移動割り当ては、std :: move()メソッドを呼び出すことによって呼び出すことができます。

デフォルトでメソッドからオブジェクトを返すには、呼び出し元からアクセス可能な場所にローカルオブジェクトとその関連データのコピーを作成する必要があります(ローカルオブジェクトは呼び出し元からアクセスできないため、メソッドが終了すると表示されなくなります)。組み込み型が返される場合、この操作は非常に高速ですが、ラージオブジェクトが返される場合、これには長い時間がかかります。移動コンストラクタを使用すると、プログラマはこのデフォルトの動作を無効にして、代わりにローカルオブジェクトに関連付けられたヒープデータを呼び出し元に返すことで、ローカルオブジェクトに関連付けられたヒープデータを「再利用」できます。したがって、コピーは不要です。

ローカルオブジェクト(つまり、スタック上のオブジェクト)の作成を許可しない言語では、すべてのオブジェクトがヒープ上に割り当てられ、常に参照によってアクセスされるため、この種の問題は発生しません。

5
Chris B