1938

C++0xに関するScottMeyersのソフトウェアエンジニアリングラジオポッドキャストインタビューを聞き終えたところです。新機能のほとんどは私にとって理にかなっており、1つを除いて、私は実際にC++0xに興奮しています。私はまだ移動セマンティクスを取得していません...それは正確には何ですか?

4

11 に答える 11

2757

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

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

自分でメモリを管理することを選択したので、3のルールに従う必要があります。代入演算子の記述を延期し、今のところデストラクタとコピーコンストラクタのみを実装します。

    ~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可能性があるため、この深いコピーが本当に必要なのは、コピーする最初の行だけであることに注意してください。私がちょうど3回(この文を含めると4回)言って、毎回まったく同じオブジェクトを意味していることに気づきましたか?「左辺値」などの式を呼びます。xxxx

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

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

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

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

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

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

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

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

この例が要点を理解したことを願っています。参照を再評価し、セマンティクスを移動する方法は他にもたくさんありますが、単純にするために意図的に省略しました。詳細が必要な場合は、私の補足回答を参照してください。

于 2010-06-24T12:40:47.393 に答える
1173

私の最初の答えは、移動セマンティクスの非常に単純化された導入であり、それを単純にするために多くの詳細が意図的に省略されました。しかし、セマンティクスを動かすにはまだまだたくさんのことがあり、ギャップを埋めるために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標準ライブラリテンプレートについて説明しています。これは、C++11でstd::auto_ptr置き換えられました。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 | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

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

オブジェクトを移動するということは、そのオブジェクトが管理するリソースの所有権を別のオブジェクトに譲渡することを意味します。

のコピーコンストラクタは、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<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();

それでも、一方は未定義の動作を呼び出しますが、もう一方は呼び出しません。aでは、式との違いは何make_triangle()ですか?どちらも同じタイプではないですか?確かにそうですが、異なる値のカテゴリがあります。

値のカテゴリ

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

のような左辺値から移動すると、後で未定義の動作を呼び出して、をa介してメンバー関数を呼び出そうとする可能性があるため、危険です。a一方、のような右辺値からの移動make_triangle()は完全に安全です。これは、コピーコンストラクターがその役割を果たした後は、一時的なものを再び使用できないためです。上記の一時的なものを表す表現はありません。単にmake_triangle()もう一度書くと、別の一時的なものになります。実際、移動元の一時的なものは、次の行ですでになくなっています。

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

文字lrは、課題の左側と右側に歴史的な起源があることに注意してください。これはC++ではもはや当てはまりません。代入の左側に表示できない左辺値(配列や代入演算子のないユーザー定義型など)と、表示できる右辺値(クラス型のすべての右辺値など)があるためです。代入演算子を使用)。

クラス型の右辺値は、評価によって一時オブジェクトが作成される式です。通常の状況では、同じスコープ内の他の式が同じ一時オブジェクトを示すことはありません。

値の参照

左辺値からの移動は潜在的に危険であることがわかりましたが、右辺値からの移動は無害です。C ++が左辺値引数と右辺値引数を区別する言語サポートを備えている場合、左辺値からの移動を完全に禁止するか、少なくとも呼び出しサイトで左辺値からの移動を明示的にして、誤って移動しないようにすることができます。

この問題に対するC++11の答えは、右辺値参照です。右辺値参照は、右辺値にのみバインドする新しい種類の参照であり、構文はX&&です。古き良き参照は、左辺値参照X&として知られるようになりました。(これは参照への参照ではないことに注意してください。C++にはそのようなものはありません。)X&&

ミックスに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&&は、右辺値にのみバインドする新しい種類の参照です。

暗黙の変換

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

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&&パラメーターを持つ関数の便利な例は、 moveコンストラクター X::X(X&& source)です。その目的は、管理対象リソースの所有権をソースから現在のオブジェクトに譲渡することです。

C ++ 11では、右辺値参照を利用std::auto_ptr<T>するに置き換えられました。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コピーコンストラクターが実行したことを正確に実行しますが、右辺値でのみ指定できます。

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

aは左辺値であるため、2行目はコンパイルに失敗しますが、パラメーターは右辺値にunique_ptr&& sourceのみバインドできます。これはまさに私たちが望んでいたことです。危険な動きは決して暗示的であってはなりません。make_triangle()は右辺値であるため、3行目は問題なくコンパイルされます。移動コンストラクターは、所有権を一時的なものからに移し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;
    }
};

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

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

これsourceはタイプの変数であるunique_ptrため、moveコンストラクターによって初期化されます。つまり、引数はパラメータに移動されます。移動コンストラクター自体に右辺値参照パラメーターがあるため、引数は右辺値である必要があります。制御フローがの閉じ括弧に達すると、operator=スコープsource外になり、古いリソースが自動的に解放されます。

ムーブ代入演算子は、管理対象リソースの所有権を現在のオブジェクトに譲渡し、古いリソースを解放します。移動と交換のイディオムは、実装を簡素化します。

左辺値からの移動

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

a3行目以降は、三角形を所有していないことに注意してください。明示的に書くことで、私std::move(a)たちの意図を明確にしたので、それは大丈夫です。acaa

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

Xvalues

std::move(a)は右辺値ですが、その評価では一時オブジェクトは作成されないことに注意してください。この難問により、委員会は3番目の価値カテゴリーを導入することを余儀なくされました。従来の意味では右辺値ではありませんが、右辺値参照にバインドできるものは、x値(eXpiring値)と呼ばれます。従来の右辺値は、prvalues(純粋な右辺値)に名前​​が変更されました。

prvaluesとxvaluesはどちらも右辺値です。X値と左辺値は両方ともglvalues(一般化左辺値)です。関係は、図を使用すると簡単に把握できます。

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

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

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

機能からの移動

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

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
}

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

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

どちらのファクトリ関数でも、戻り値の型は値であり、右辺値の参照ではないことに注意してください。右辺値参照は引き続き参照であり、いつものように、自動オブジェクトへの参照を返さないでください。次のように、コンパイラをだましてコードを受け入れさせた場合、呼び出し元はぶら下がっている参照になってしまいます。

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

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

メンバーに移動する

遅かれ早かれ、次のようなコードを記述します。

class Foo
{
    unique_ptr<Shape> member;

public:

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

基本的に、コンパイラはそれparameterが左辺値であると文句を言います。その型を見ると、右辺値参照が表示されますが、右辺値参照は単に「右辺値にバインドされている参照」を意味します。参照自体が右辺値であるという意味ではありません。確かに、parameterは名前の付いた単なる通常の変数です。コンストラクターの本体内で何度でも使用できparameter、常に同じオブジェクトを示します。暗黙のうちにそこから移動することは危険であるため、言語はそれを禁止しています。

名前付き右辺値参照は、他の変数と同じように左辺値です。

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

class Foo
{
    unique_ptr<Shape> member;

public:

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

parameterの初期化後、それはもう使用されていないと主張することができますmemberstd::move戻り値と同じようにサイレントに挿入する特別なルールがないのはなぜですか?おそらくコンパイラの実装者に負担がかかりすぎるからでしょう。たとえば、コンストラクター本体が別の変換ユニットにある場合はどうなりますか?対照的に、戻り値ルールは、単にシンボルテーブルをチェックして、returnキーワードの後の識別子が自動オブジェクトを示しているかどうかを判断する必要があります。

parameterby値を渡すこともできます。のような移動専用タイプの場合unique_ptr、確立されたイディオムはまだないようです。個人的には、インターフェイスの乱雑さが少なくなるため、値を渡すことを好みます。

特別会員機能

C ++ 98は、オンデマンドで、つまり、コピーコンストラクタ、コピー代入演算子、およびデストラクタの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に減少します。ここでは例外安全性と効率性の間にトレードオフがありますが、私はこの問題の専門家ではありません。

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

次の関数テンプレートについて考えてみます。

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

T&&一見すると右辺値参照のように見えるため、右辺値にのみバインドすることを期待する場合があります。しかし、結局のところ、左辺値にT&&もバインドされます。

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推定TされX、したがって、T&&を意味しX&&ます。これは誰もが期待することです。ただし、引数が型の左辺値である場合、X特別な規則により、Tと推定されるためX&、のT&&ような意味になりX& &&ます。ただし、C ++にはまだ参照への参照の概念がないため、型X& &&はに折りたたまX&ます。これは最初は混乱して役に立たないように聞こえるかもしれませんが、完全な転送には参照の折りたたみが不可欠です(ここでは説明しません)。

T &&は右辺値参照ではなく、転送参照です。また、左辺値にバインドします。この場合T、とT&&は両方とも左辺値の参照です。

関数テンプレートを右辺値に制約する場合は、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&&、右辺値参照を返します。std::remove_reference<T>::typeメタ関数呼び出しが必要なのは、そうでない場合、タイプの左辺値の場合、X戻りタイプが、になりX& &&、がに折りたたまれるためX&です。tは常に左辺値であるため(名前付き右辺値参照は左辺値であることを忘れないでください)、右辺値参照にバインドする必要があるため、正しい戻り型tに明示的にキャストする必要があります。t右辺値参照を返す関数の呼び出しは、それ自体がxvalueです。これで、xvaluesがどこから来るかがわかりました;)

などの右辺値参照を返す関数の呼び出しはstd::movexvalueです。

この例でtは、自動オブジェクトではなく、呼び出し元から渡されたオブジェクトを示しているため、右辺値参照による戻りは問題ないことに注意してください。

于 2012-07-18T11:24:27.390 に答える
63

実質的なオブジェクトを返す関数があるとします。

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

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

Matrix r = multiply(a, b);

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

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

于 2010-06-23T22:53:31.390 に答える
30

移動セマンティクスの詳細な説明に本当に興味がある場合は、元の論文「C++言語に移動セマンティクスサポートを追加する提案」を読むことを強くお勧めします。

それは非常にアクセスしやすく、読みやすく、彼らが提供する利点の優れた事例になります。WG21 Webサイトで利用可能な移動セマンティクスに関する他のより最近の最新の論文がありますが、これはトップレベルの観点から物事にアプローチし、ざらざらした言語の詳細にあまり触れないため、おそらく最も簡単です。

于 2010-06-23T23:32:29.323 に答える
30

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

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

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

于 2012-04-08T19:47:56.793 に答える
28

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

オブジェクトのコピーとは、その「静的」メンバーをコピーし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());

その場合、匿名オブジェクトが作成され、次に関数パラメーターにコピーされ、その後削除されます。したがって、ここでは、匿名オブジェクトを必要とせず、時間とメモリを節約できるため、オブジェクトを移動することをお勧めします。

これは、「右辺値」参照の概念につながります。これらは、受信したオブジェクトが匿名であるかどうかを検出するためにのみ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&&ですか?もちろん、前者の場合はオブジェクトを変更でき、後者の場合は変更できませんが、実際的な意味はありますか?2番目のケースでは、オブジェクトを変更できないため、オブジェクトを無効にする方法はなく(可変フラグなどを除く)、コピーコンストラクターと実際的な違いはありません。

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

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

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

other_function(std::move(a));

この行で、は右辺値std::moveにキャストし、名前のないオブジェクトとしてオブジェクトを受け取ります。もちろん、名前のないオブジェクトを処理するための特定のオーバーロードがない場合、この区別は重要ではありません。aother_functionother_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&)Tへの左辺値参照である場合、 (A &&& => A&)も参照されます。がへの右辺値参照である場合、(A && && => A &&)も参照します。どちらの場合も、は実際のスコープ内の名前付きオブジェクトですが、呼び出し元スコープの観点からは、その「参照タイプ」の情報が含まれています。この情報()はテンプレートパラメータとしてに渡され、「a」はのタイプに応じて移動されるかどうかに応じて移動されます。AaTAaaTTforwardT

于 2013-08-18T15:57:19.767 に答える
24

これはコピーセマンティクスに似ていますが、すべてのデータを複製する代わりに、「移動」元のオブジェクトからデータを盗むことができます。

于 2010-06-23T22:56:46.173 に答える
14

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

移動セマンティクスは基本的に、非定数であるr値参照(&&(はい2アンパサンド)を使用する新しいタイプの参照)を受け取るコンストラクターを持つユーザー定義型です。これは移動コンストラクターと呼ばれ、代入演算子についても同様です。したがって、moveコンストラクタは、ソース引数からメモリをコピーする代わりに、ソースからデスティネーションにメモリを「移動」します。

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

std::vector<foo> get_foos();

関数が戻るときにコピーコンストラクターからオーバーヘッドが発生します(C ++ 0xではそうなります)std :: vectorには、コピーする代わりにmoveコンストラクターがあり、ポインターを設定して'move'を動的に割り当てることができます新しいインスタンスへのメモリ。これは、std::auto_ptrを使用した所有権の譲渡のセマンティクスのようなものです。

于 2010-06-23T22:58:10.103 に答える
9

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

移動セマンティクスは、大きなオブジェクトの不要なコピーを回避するために作成されました。彼の著書「TheC++Programming Language」のBjarneStroustrupは、デフォルトで不要なコピーが発生する2つの例を使用しています。1つは2つの大きなオブジェクトの交換、2つはメソッドからの大きなオブジェクトの返却です。

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

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

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

于 2016-11-18T23:12:12.993 に答える
8

移動セマンティクスの必要性を説明するために、移動セマンティクスを使用しないこの例を考えてみましょう。

Tタイプのオブジェクトを受け取り、同じタイプのオブジェクトを返す関数は次のTとおりです。

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

上記の関数は値による呼び出しを使用します。つまり、この関数が呼び出されると、関数で使用されるオブジェクトを作成する必要があります。
関数もで戻るため、戻り値用に別の新しいオブジェクトが作成されます。

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

2つの新しいオブジェクトが作成されました。そのうちの1つは、関数の期間中のみ使用される一時オブジェクトです。

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


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

最初にオブジェクトを初期化し、次にすべての関連データを古いオブジェクトから新しいオブジェクトにコピーする必要があります。
クラスによっては、データが非常に多いコンテナである可能性があります。これは、多くの時間メモリの使用量を表す可能性があります。

// 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 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参照を使用して実行されます。参照は、1つの重要な違いがある参照と
ほとんどrvalue同じように機能します。つまり 、右辺値参照は移動できますが左辺値は移動できません。lvalue

cppreference.comから:

強力な例外保証を可能にするために、ユーザー定義の移動コンストラクターは例外をスローしないようにする必要があります。実際、標準のコンテナは通常、コンテナ要素を再配置する必要がある場合に、移動とコピーのどちらかを選択するためにstd::move_if_noexceptに依存しています。コピーコンストラクターと移動コンストラクターの両方が提供されている場合、オーバーロード解決は、引数が右辺値(名前のない一時などのprvalueまたはstd :: moveの結果などのxvalue)の場合は移動コンストラクターを選択し、次の場合はコピーコンストラクターを選択します。引数は左辺値(名前付きオブジェクトまたは左辺値参照を返す関数/コンストラクター)です。コピーコンストラクターのみが提供されている場合、すべての引数カテゴリがそれを選択し(右辺値はconst参照にバインドできるため、constへの参照を取得する限り)、移動が利用できない場合に移動のフォールバックをコピーします。多くの場合、移動コンストラクターは、観察可能な副作用が発生する場合でも最適化されます。コピーの省略を参照してください。コンストラクターは、パラメーターとして右辺値参照を受け取る場合、「moveコンストラクター」と呼ばれます。何かを移動する義務はなく、クラスは移動するリソースを持っている必要はなく、「moveコンストラクター」は、パラメーターがconst右辺値参照(const T &&)。

于 2016-02-25T00:00:04.973 に答える
-2

これは、 BjarneStroustrupの著書「TheC++ProgrammingLanguage」からの回答です。ビデオを見たくない場合は、以下のテキストを見ることができます:

このスニペットを検討してください。operator +から戻るには、結果をローカル変数resから呼び出し元がアクセスできる場所にコピーする必要があります。

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

私たちは本当にコピーが欲しくありませんでした。関数から結果を取得したかっただけです。したがって、ベクターをコピーするのではなく、移動する必要があります。移動コンストラクターは次のように定義できます。

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&&は「右辺値参照」を意味し、右辺値をバインドできる参照です。「rvalue」は、「割り当ての左側に表示される可能性のあるもの」を大まかに意味する「lvalue」を補完することを目的としています。resしたがって、右辺値は、関数呼び出しによって返される整数や、 Vectorsのoperator +()のローカル変数など、おおよそ「割り当てることができない値」を意味します。

これで、ステートメントreturn res;はコピーされません!

于 2020-04-25T05:19:17.363 に答える