移動セマンティクスとは何ですか? 質問する

移動セマンティクスとは何ですか? 質問する

ソフトウェアエンジニアリングのラジオを聴き終わったところですスコット・マイヤーズとのポッドキャストインタビューに関してC++11新機能のほとんどは、1 つを除いて私には理解できました。まだ移動セマンティクスが理解できません... それは正確には何ですか?

ベストアンサー1

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

#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このディープ コピーが本当に必要であることに注意してください。後で検査する場合があり、何らかの変更があったx場合は非常に驚くことになるからです。私が3 回 (この文を含めると 4 回) 言ったことと、毎回まったく同じオブジェクトを意味していることに気付きましたか? このような式を「lvalues」と呼びます。xxx

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

C++0x では、「rvalue 参照」と呼ばれる新しいメカニズムが導入され、関数のオーバーロードを介して rvalue 引数を検出できるようになりました。必要なのは、rvalue 参照パラメータを持つコンストラクタを記述することだけです。そのコンストラクタ内では、ソースを有効な状態にしておく限り、ソースに対して何でも行うことできます

    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移動コンストラクタは初期化しますthat(式はx + y右辺値であるため)。そのため、ディープ コピーは行われず、効率的な移動のみが行われます。 はthat依然として引数から独立したオブジェクトですが、ヒープ データをコピーする必要はなく、移動するだけでよいため、その構築は簡単です。 はx + y右辺値であるため、コピーする必要はありませんでした。また、右辺値で示される文字列オブジェクトから移動しても問題ありません。

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

この例で要点が伝われば幸いです。右辺値参照とムーブセマンティクスには他にも多くの機能がありますが、ここでは簡単にするために意図的に省略しました。詳細を知りたい場合は、私の補足回答

おすすめ記事