c++ - 意味 - 移動セマンティクスとは何ですか?




右辺値参照 使い方 (8)

私はちょうどC++0xに関するScott Meyersとのソフトウェアエンジニアリングラジオポッドキャストインタビューを聞いて終了しました。 新機能のほとんどは私にとって意味をなさないものでしたが、実際にはC ++ 0xには興奮しています。 私はまだ移動セマンティクスを取得していない...彼らは正確に何ですか?


移動セマンティクスは、誰もソース値を必要としなくなったときにコピーするのではなく、リソースを転送することです。

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

そのようなコピー/破棄ペアが高価な場合、通常、オブジェクトが重量のあるリソースを所有しているためです。例えばvector<string>stringオブジェクトの配列を含む動的に割り当てられたメモリブロックを所有していてもよい。各メモリブロックには、それ自体の動的メモリがある。このようなオブジェクトをコピーするには、コストがかかります。ソース内の動的に割り当てられたブロックごとに新しいメモリを割り当て、すべての値をコピーする必要があります。次に、コピーしたメモリをすべて解放する必要があります。しかし、大規模に移動するvector<string>ということは、いくつかのポインタ(ダイナミックメモリブロックを参照する)をデスティネーションにコピーし、ソース内でそれらをゼロにすることだけです。


コピーセマンティクスのようなものですが、すべてのデータを複製して、移動したオブジェクトからデータを盗むことになります。


私の最初の答えは、セマンティクスを移動するための非常に簡単な紹介であり、多くの詳細は単純なものにするために意図的に省略されていました。 しかし、セマンティクスを移動するにはさらに多くのことがあります。ギャップを埋めるためには2番目の答えが必要です。 最初の答えはすでにかなり古く、まったく別のテキストに置き換えるだけではうまくいかないと感じました。 私はそれがまだ最初の導入としてうまく機能していると思います。 しかし、もっと深く掘り下げたいのであれば、読んでください:)

Stephan T. Lavavejは貴重なフィードバックを提供しました。 ありがとう、ステファン!

前書き

移動セマンティクスを使用すると、特定の条件下でオブジェクトが他のオブジェクトの外部リソースの所有権を取得することができます。 これは2つの点で重要です。

  1. 高価なコピーを安く動かす。 私の最初の答えを見てください。 オブジェクトが少なくとも1つの外部リソースを(直接的または間接的にメンバーオブジェクトを通して)管理しない場合、移動セマンティクスはコピーセマンティクスに優位性を提供しないことに注意してください。 その場合、オブジェクトをコピーしてオブジェクトを移動することは、まったく同じことを意味します。

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    
  2. 安全な「移動専用」タイプの実装。 すなわち、コピーは意味をなさないが動くタイプのタイプです。 例には、固有の所有権セマンティクスを持つロック、ファイルハンドル、スマートポインタなどがあります。 注:この回答では、廃止予定のC ++ 98標準ライブラリテンプレートであるstd::auto_ptrについて説明します。これはC ++ 11ではstd::unique_ptrに置き換えられました。 中級C ++プログラマはおそらくstd::auto_ptr多少親しんでいるでしょうし、それが表示する「移動セマンティクス」のために、C ++ 11での移動セマンティクスについて議論するのは良い出発点のようです。 YMMV。

動きは何ですか?

C ++ 98標準ライブラリは、 std::auto_ptr<T>という固有の所有権セマンティクスを持つスマートポインタを提供します。 auto_ptrに慣れていない場合、その目的は例外があっても常に動的に割り当てられたオブジェクトが解放されることを保証することです:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

auto_ptrに関する珍しいことは、その "コピー"動作です。

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

bbの初期化で三角形をコピーaなく 、三角形の所有権をbからbどのように転送するかに注意してください。 また、「 ab 移動した 」、または「三角形がa から b 移動a 」とも言います。 これは、三角形自体が常にメモリ内の同じ場所にとどまるので、混乱しているように聞こえるかもしれません。

オブジェクトを移動するとは、管理対象のリソースの所有権を別のオブジェクトに移すことです。

auto_ptrのコピーコンストラクタはおそらく次のようになります(いくらか簡略化されています):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

危険で無害な動き

auto_ptrの危険なことは、構文的にはコピーのように見えるのは実際には動きだということです。 移動されたauto_ptrメンバ関数を呼び出そうとすると、未定義の動作がauto_ptrので、移動した後にauto_ptrを使わないように注意する必要があります:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

しかし、 auto_ptr必ずしも危険なわけではありませ 。 ファクトリ関数は、 auto_ptr完全に良い使用例です。

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

どちらの例も同じ統語パターンに従うことに注意してください。

auto_ptr<Shape> variable(expression);
double area = expression->area();

しかし、一方は未定義のビヘイビアを呼び出すのに対し、もう一方は未定義のビヘイビアを呼び出します。 では、式amake_triangle()の違いは何ですか? 彼らは同じ種類の両方ではありませんか? 確かに彼らは存在しますが、彼らは異なる価値カテゴリーを持っています

値のカテゴリ

明らかに、 auto_ptr変数を表す式aと、 auto_ptrを値で返す関数の呼び出しを示す式make_triangle()との間には、いくつかの大きな違いがあるmake_triangle()です。したがって、呼び出されるたびに新しい一時的なauto_ptrオブジェクトが作成されます。 a左辺値の例ですが、 make_triangle()右辺値の例です。

後でaを介しaメンバー関数を呼び出そうとすると、定義されていない振る舞いを呼び出すことができるのでaなどの左辺値からの移動は危険です。 一方、 make_triangle()ような値からの移動は完全に安全です。なぜなら、コピーコンストラクターがそのジョブを完了した後、再び一時的に使用することができないからです。 一時的にそのことを示す表現はありません。 単にmake_triangle()もう一度書くと、 別の一時的なものが得られます。 実際には、移動した一時的なものはすでに次の行に移動しています。

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

文字lrは、割り当ての左側と右側に歴史的な起点を持つことに注意してください。 これはC ++ではもはや真ではありません。なぜなら、代入の左辺に現れないlvalues(代入演算子を持たない配列やユーザ定義の型のような)がありますし、rvaluesがあります(クラス型のすべての値代入演算子を使用して)。

クラス型の右辺値は、評価が一時オブジェクトを作成する式です。 通常の状況下では、同じスコープ内の他の式は同じ一時オブジェクトを表しません。

値の参照

左辺値からの移動は潜在的に危険ですが、右辺値からの移動は無害です。 C ++にlvalue引数とrvalue引数を区別するための言語サポートがある場合、lvaluesからの移動を完全に禁止するか、コールサイトで明示的にlvaluesから移動することで、もはや偶然に動かないようにすることができます。

この問題に対するC ++ 11の答えは右辺値です 。 rvalue参照は、rvaluesにのみバインドする新しい種類の参照であり、構文はX&&です。 古き良きリファレンスX&は現在、 左辺リファレンスとして知られています 。 ( X&&は参照への参照ではなく、C ++ではそのようなことはありません)。

mixにconst 、すでに4種類の参照があります。 タイプXのどのような種類の表現をバインドできますか?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

実際には、 const X&&忘れることができます。 右辺値からの読み取りに制限されていることはあまり有用ではありません。

値の参照X&&は、rvaluesにのみバインドする新しい種類の参照です。

暗黙のコンバージョン

値の参照はいくつかのバージョンを通過しました。 バージョン2.1以来、値の参照X&&は、異なるタイプYすべての値カテゴリにもバインドされます。ただし、 YからXへの暗黙的な変換がある場合に限ります。 その場合、一時的なタイプXが作成され、右辺値の参照はその一時的なものにバインドされます。

void some_function(std::string&& r);

some_function("hello world");

上の例では、 "hello world"const char[12]型の左辺値です。 const char[12]からconst char*からstd::stringへの暗黙的な変換があるので、一時的な型のstd::stringが作成され、 rはその一時的なものにバインドされます。 これは、値(表現)と一時(オブジェクト)の区別が少しぼやけている場合の1つです。

コンストラクタを移動する

X&&パラメータを持つ関数の便利な例は、 移動コンストラクタ X::X(X&& source)です。 その目的は、管理対象リソースの所有権をソースから現在のオブジェクトに移すことです。

C ++ 11では、 std::auto_ptr<T>がrvalue参照を利用するstd::unique_ptr<T>置き換えられました。 私は、 unique_ptr簡略化されたバージョンを開発し、議論します。 まず、生のポインタをカプセル化して、演算子->*をオーバーロードします。その結果、クラスはポインタのように感じます:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

コンストラクタはオブジェクトの所有権を持ち、デストラクタはそれを削除します。

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

ここで面白い部分は、移動コンストラクタになります:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

この移動コンストラクタは、 auto_ptrコピーコンストラクタが行ったものとまったく同じですが、rvaluesでのみ指定できます。

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

2番目の行はコンパイルに失敗します。なぜならaは左辺値ですが、パラメータunique_ptr&& sourceはrvaluesにのみバインドできます。 これはまさに私たちが望んでいたものです。 危険な動きは決して暗黙であってはならない。 make_triangle()make_triangle() 、3行目はうまくコンパイルされます。 移動コンストラクタは、所有権を一時的なものからc移しc 。 繰り返しますが、これはまさに私たちが求めていたものです。

移動コンストラクタは、管理対象リソースの所有権を現在のオブジェクトに転送します。

代入演算子の移動

最後に欠けているのは、移動代入演算子です。 その仕事は、古いリソースを解放し、その引数から新しいリソースを取得することです:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

move代入演算子のこの実装は、デストラクタと移動コンストラクタの両方のロジックをどのように複製するかに注意してください。 あなたはコピーアンドスワップのイディオムに精通していますか? 移動とスワップのイディオムとしてセマンティクスを移動する場合にも適用できます。

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

そのsourceunique_ptr型の変数であり、移動コンストラクタによって初期化されます。 つまり、引数はパラメータに移動されます。 引数はrvalueでなければなりません。なぜなら、移動コンストラクタ自体にrvalue参照パラメータがあるからです。 制御フローがoperator=閉じ括弧に達すると、 sourceは範囲外になり、古いリソースが自動的に解放されます。

移動割り当て演算子は、管理対象リソースの所有権を現在のオブジェクトに転送し、古いリソースを解放します。 ムーブ・アンド・スワップイディオムは実装を簡素化します。

左辺値からの移動

時には、私たちは左利きから移動したい。 つまり、コンパイラがlvalueをrvalueのように扱うようにしたい場合があるため、安全ではない可能性があるにもかかわらず、移動コンストラクタを呼び出すことができます。 この目的のために、C ++ 11はヘッダ<utility>内部にstd::moveという標準ライブラリ関数テンプレートを提供しています。 この名前はちょっと残念ですが、 std::moveは左辺値を右辺値に単純にキャストします。 それ自体は何も動かされませ 。 それ動くだけです。 たぶん、 std::cast_to_rvalueまたはstd::enable_moveという名前になっているはずですが、今は名前がついています。

左辺値から明示的に移動する方法は次のとおりです。

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

3行目以降は、もはや三角形を所有していないことに注意してください。 これは大丈夫ですstd::move(a) 明示的に記述することで、私たちは意図を明確にしました: "親愛なるコンストラクタ、 cを初期化するためには何でもしてください;私はもう気にしません。あなたのやり方で。 "

std::move(some_lvalue)は、左辺値を右辺値にキャストし、後続の移動を可能にします。

X値

std::move(a)がrvalueであっても、その評価は一時的なオブジェクトを作成しないことに注意してください。 この難題は、委員会に第3の価値カテゴリーを導入させた。 rvalue参照にバインドできるものは、従来の意味でのrvalueではないものの、 xvalue (eXpiring値)と呼ばれます。 従来の評価額は、 評価額 (純評価額)に変更されました

prvaluesとxvaluesはいずれも右辺値です。 X値と左値は両方ともglvalues (一般化された左辺値)です。 関係は図で把握しやすくなります。

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

xvaluesのみが本当に新しいことに注意してください。 残りの部分は名前の変更とグループ化だけによるものです。

C ++ 98の値は、C ++ 11ではprvaluesとして知られています。 精神的には、前の段落の「rvalue」のすべての出現を「prvalue」に置き換えます。

関数の移動

これまでは、ローカル変数と関数パラメータへの移動が見られました。 しかし、反対方向にも移動が可能です。 関数が値によって返された場合、呼び出しサイトのオブジェクト(おそらくローカル変数または一時的ですが、任意の種類のオブジェクトである可能性があります)は、 returnコンストラクタの後の式で移動コンストラクタの引数として初期化されます。

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

おそらく驚くべきことに、自動オブジェクト( staticとして宣言されていないローカル変数)も暗黙的に関数から移動できます。

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

どのようにして、移動コンストラクタがlvalueのresultを引数として受け取るのでしょうか? resultのスコープはほぼ終了し、スタックの巻き戻し中に破棄されます。 誰も後でresultが何とか変わったと後で不平を言う可能性があります。 制御フローが発信者に戻ったとき、 resultはもう存在しません! そのため、C ++ 11には、 std::moveを記述することなく関数から自動オブジェクトを返すことを可能にする特別なルールがあります。 実際には、関数から自動オブジェクトを移動するためにstd::moveを使用しないでください。これは、「名前付き戻り値の最適化」(NRVO)を禁止するためです。

自動オブジェクトを関数から移動するためにstd::moveを使用しないでください。

どちらのファクトリ関数でも、戻り値の型は値であり、rvalue参照ではありません。 Rvalue参照は依然として参照であり、いつものように、自動オブジェクトへの参照を返すべきではありません。 あなたがコードを受け入れるようにコンパイラを騙した場合、呼び出し元は手間のかかる参照で終わるでしょう:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

rvalue参照で自動オブジェクトを返さないでください。 移動はstd::moveではなく、単にrvalueをrvalue参照にバインドするのではなく、moveコンストラクタによって排他的に実行されます。

メンバーに移動する

遅かれ早かれ、あなたは次のようなコードを書きます:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

基本的に、コンパイラはそのparameterが左辺値であると不満を持ちます。 その型を見ると、rvalue参照が表示されますが、rvalue参照は単に「rvalueにバインドされた参照」を意味します。 参照自体が右辺値であることを意味するものではありませ 。 実際、 parameterは名前を持つ通常の変数に過ぎません。 あなたはコンストラクタの本体の中で好きなだけ頻繁にparameterを使うことができ、常に同じオブジェクトを示します。 暗黙のうちにそれから移動することは危険であり、したがって言語はそれを禁じている。

名前付きrvalue参照は、他の変数と同様にlvalueです。

解決方法は、手動で移動を有効にすることです。

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

memberの初期化後にparameterがもう使用されないと主張できparameterstd::moveを返す特別な規則がないのはなぜですか? おそらく、それはコンパイラの実装者にとって負担が大きすぎるからでしょう。 たとえば、コンストラクタ本体が別の翻訳単位に含まれていた場合はどうなりますか? 対照的に、戻り値ルールは、単に戻り値キーワードの後の識別子が自動オブジェクトを示すかどうかを判定するために記号テーブルをチェックしなければならない。

値でparameterを渡すこともできます。 unique_ptrような移動のみのタイプの場合、まだ確立されたイディオムはないようです。 個人的には、私は値段の方が優先されます。なぜなら、それはインターフェイスの混乱を少なくするからです。

特別なメンバー関数

C ++ 98は、オンデマンドで3つの特別なメンバ関数、つまりコピーコンストラクタ、コピー代入演算子、およびデストラクタのどこかに必要なときに、3つの特別なメンバ関数を暗黙的に宣言します。

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

値の参照はいくつかのバージョンを通過しました。 バージョン3.0以降、C ++ 11では、必要に応じて2つの特別なメンバ関数が追加されています:移動コンストラクタと移動代入演算子です。 VC10もVC11もまだバージョン3.0に準拠していないので、自分で実装する必要があります。

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

これら2つの新しい特別なメンバー関数は、特別なメンバー関数のどれもが手動で宣言されていない場合にのみ、暗黙的に宣言されます。 また、独自の移動コンストラクタや代入演算子を宣言すると、コピーコンストラクタもコピー代入演算子も暗黙的に宣言されることはありません。

実際にこれらのルールは何を意味していますか?

管理されていないリソースを持たないクラスを作成する場合、5つの特別なメンバー関数を自分で宣言する必要はなく、正しいコピーセマンティクスとセマンティクスを無料で得ることができます。 それ以外の場合は、特別なメンバー関数を自分で実装する必要があります。 もちろん、クラスが移動セマンティクスの恩恵を受けていない場合、特殊な移動操作を実装する必要はありません。

コピー代入演算子と移動代入演算子は、その引数を値でとって、単一の代入代入演算子に融合できます。

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

このように、実装する特別なメンバ関数の数は5から4に減少します。 ここでは例外安全性と効率性の間にトレードオフがありますが、私はこの問題の専門家ではありません。

転送参照( previouslyユニバーサル参照と呼ばれていました

次の関数テンプレートを考えてみましょう。

template<typename T>
void foo(T&&);

T&&はr値にのみバインドすると考えられます。一見すると、r値の参照のように見えるからです。 T&&もlvaluesにバインドされます。

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

引数がX型のrvalueの場合、 TXと推定されます。したがって、 T&&X&&意味します。 これは誰もが期待するものです。 しかし、引数がX型の左辺値である場合、特別な規則のために、 TX&と推定されます。したがって、 T&&X& &&ようなものを意味します。 しかし、C ++には参照への参照の概念がまだないので、 X& &&型はX& &&れます。 これは最初は混乱して無意味に思えるかもしれませんが、参照の崩壊は完全な転送のために不可欠です(ここでは説明しません)。

T &&はr値の参照ではなく、転送参照です。 また、左辺値にもバインドされます。この場合、 TT&&はともに左辺値参照です。

関数テンプレートをrvaluesに制限する場合は、 SFINAEと型特性を組み合わせることができます。

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

移動の実装

参照の崩壊を理解したので、 std::move実装方法を次に示します。

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

ご覧のように、 moveは転送参照T&&おかげであらゆる種類のパラメータを受け取り、rvalue参照を返します。 そうでなければ、タイプX左辺値の場合、戻り値の型はX std::remove_reference<T>::typeなり、 X& &&崩壊するので、 std::remove_reference<T>::typeメタ関数呼び出しが必要です。 tは常にlvalue(名前付きrvalue参照はlvalueであることに注意してください)ですが、 tをrvalue参照にバインドしたいので、 tを正しい戻り型に明示的にキャストする必要があります。 rvalue参照を返す関数の呼び出し自体はxvalueです。 今あなたはxvaluesがどこから来るか知っています;)

rvalue参照を返す関数の呼び出し( std::moveなど)はxvalueです。

この例では、rvalue参照による戻り値は、 tが自動オブジェクトではなく、呼び出し元によって渡されたオブジェクトであることに注意してください。


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

移動セマンティクスは、大きなオブジェクトの不要なコピーを避けるために作成されました。Bjarne Stroustrupの著書「The C ++ Programming Language」では、不要なコピーがデフォルトで発生する2つの例があります:1つは、2つの大きなオブジェクトの交換、もう1つはメソッドからの大きなオブジェクトの返却です。

2つの大きなオブジェクトをスワップするには、最初のオブジェクトを一時オブジェクトにコピーし、2番目のオブジェクトを最初のオブジェクトにコピーし、一時オブジェクトを2番目のオブジェクトにコピーします。ビルトインタイプの場合、これは非常に高速ですが、大きなオブジェクトの場合、これらの3つのコピーには長い時間がかかります。 "移動割り当て"を使用すると、プログラマはデフォルトのコピー動作をオーバーライドして、オブジェクトへの参照をスワップすることができます。つまり、コピーがまったくなく、スワップ操作がはるかに高速です。移動割り当ては、std :: move()メソッドを呼び出すことによって呼び出すことができます。

メソッドからオブジェクトを返すのは、ローカルオブジェクトとその関連データのコピーを呼び出し元がアクセスできる場所に作成することです(ローカルオブジェクトは呼び出し元からアクセスできず、メソッドが終了すると消えます)。組み込み型が返されている場合、この操作は非常に高速ですが、大きなオブジェクトが返された場合、これには時間がかかることがあります。移動コンストラクターは、プログラマーがこのデフォルトの振る舞いをオーバーライドできるようにし、ローカル・オブジェクトに関連付けられたヒープ・データを呼び出し元に戻すオブジェクトを指すことによって、ローカル・オブジェクトに関連付けられたヒープ・データを代わりに「再利用」します。したがって、コピーは必要ありません。

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


移動セマンティクスは、 値の参照に基づいています
右辺値は一時的なオブジェクトであり、式の最後に破棄されます。 現在のC ++では、rvaluesはconst参照にのみバインドされます。 C ++ 1xでは、rvalueオブジェクトへの参照であるT&&スペルのない非const値の参照が許可されます。
右辺値は式の最後で消滅するため、そのデータ盗むことができます 。 別のオブジェクトにコピーするのではなく、そのオブジェクトにデータを移動します。

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

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


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

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

class A
{
   int i, *p;

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

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

しかし、危険ではないですか?もちろん、動的オブジェクトを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());

このような状況では、匿名オブジェクトが作成され、次に関数パラメーターにコピーされ、その後で削除されます。したがって、ここではオブジェクトを移動するほうが匿名オブジェクトは不要で時間とメモリを節約できます。

これは、 "rvalue"参照の概念につながります。受け取ったオブジェクトが匿名であるかどうかを検出するためにのみ、C ++ 11に存在します。私はあなたが既に知っていると思う "lvalue"は、割り当て可能なエンティティ(=演算子の左部分)ので、lvalueとして機能することができるオブジェクトへの名前付き参照が必要です。右辺値は正反対で、名前付き参照がないオブジェクトです。そのため、匿名オブジェクトとrvalueは同義語です。そう:

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"コピー"する必要がある場合、コンパイラは、渡されたオブジェクトに名前が付けられているかどうかに従って、左辺値参照または右辺値参照を作成します。そうでない場合は、移動コンストラクタが呼び出され、オブジェクトが一時的であることがわかっていて、動的オブジェクトをコピーする代わりに移動して、領域とメモリを節約できます。

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

あなたのオブジェクトが複雑で、デストラクタがライブラリの関数を呼び出す、他のグローバル関数を呼び出す、それが何であれ他の副次的な効果がある場合は、おそらくフラグを付けて動きを知らせる方が良いでしょう。

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&&?もちろん、最初のケースではオブジェクトを修正できますが、2番目のケースではありませんが、実際的な意味ですか?2番目のケースでは、それを変更することはできませんので、オブジェクトを無効にする方法はありません(変更可能なフラグなどを除いて)。コピーコンストラクタには実際的な違いはありません。

完璧なフォワーディングは何ですか?"rvalue reference"は "呼び出し元のスコープ"内の名前付きオブジェクトへの参照であることを知っておくことが重要です。実際のスコープでは、rvalue参照はオブジェクトの名前であるため、名前付きオブジェクトとして機能します。右値参照を別の関数に渡すと、名前付きオブジェクトを渡すので、オブジェクトは一時オブジェクトのように受け取られません。

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

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

other_function(std::move(a));

この線で、rvalueにstd::moveキャストaother_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&&`

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


コピーセマンティクスが意味するものはあなたが知っていますか?これはあなたがコピー可能な型を持っていることを意味します。ユーザー定義型の場合は、コピーコンストラクタと代入演算子を明示的に購入するか、コンパイラが暗黙的に生成します。これでコピーが作成されます。

移動セマンティクスは基本的に、r値参照(&&(2つのアンパサンド)を使用して新しいタイプの参照)を取るコンストラクタを持つユーザー定義型です。これは非constです。これは移動コンストラクタと呼ばれ、代入演算子も同じです。それで、移動コンストラクタは何をするのですか?それを元の引数からメモリをコピーするのではなく、メモリを移動元から移動先に移動します。

いつあなたはそれをしたいですか?よくstd :: vectorが例です、あなたは一時的なstd :: vectorを作成し、関数からそれを返すと言う:

std::vector<foo> get_foos();

関数が返るときにコピーコンストラクタからオーバーヘッドを得ようとしています。もしstd :: vectorがコピーする代わりに移動コンストラクタを持っていれば、ポインタとポインタをセットして動的に割り当てられますメモリを新しいインスタンスに追加します。これは、std :: auto_ptrを使った所有権移転のセマンティクスのようなものです。


移動セマンティクスの詳細な説明に興味があれば、C ++言語に移動セマンティクスサポートを追加することを提案してください。

それは非常にアクセスしやすく、読みやすいし、彼らが提供する利点のための優秀なケースを作る。WG21のウェブサイトで利用可能な移動セマンティクスに関する最近の最新の論文がありますが、これはトップレベルの視点からアプローチするため、おそらく最も簡単です。







move-semantics