C++の高度な機能:右辺値参照とムーブセマンティクス

1、右辺値参照とムーブセマンティクス
  • 以前の学習で右辺値参照と左辺値参照の違いを理解しました。右辺値参照は存在しないアドレスを持つ一時変数を参照するために使用されますが、正確には「右辺値参照は実際の名前を持たない値を参照するために使用されます」
  • 右辺値参照の主な使用目的はムーブコンストラクタにあり、右辺値参照の出現はコピーコンストラクタとムーブコンストラクタを区別するためです。
struct Data{
    Data(){
        puts("Data()");
    }
    Data(const Data& ){
        puts("Data(const Data& )");
    }
    Data(Data&& ) {
        puts("Data(Data&& )");
    }
    ~Data(){
        puts("~Data()");
    }
};
class Container{
private:
    Data* data;
public:
    Container(): data(nullptr){

    }
    Container(Data* _data): data(_data){

    }
    Container(const Container& c){
        data = new Data(*(c.data));
    }
    Container(Container&& c){
        data = c.data;
        c.data = nullptr;
    }
    ~Container(){
        if(data != nullptr){
            delete data;
            data = nullptr;
        }
    }
    Data* getPointer(){
        return data;
    }
    bool isEmpty() const {
        return data == nullptr;
    }
};

まずこのようなコードを与えましょう:

  • 構造体(クラス)Dataが存在します:空のコンストラクタ、コピーコンストラクタ、ムーブコンストラクタ、デストラクタがあります
  • また、Testクラスが存在します:内部にData*のポインタ変数があり、いくつかの空のコンストラクタ、パラメータ付きコンストラクタ、コピーコンストラクタ、ムーブコンストラクタ、デストラクタ、メンバーデータのポインタを取得するメソッド、および現在の変数が空かどうかを判断するメソッドがあります。

ここでコピーコンストラクタとムーブコンストラクタの違いがわかります。コピーコンストラクタのconstは変更を防ぐためです;constを取り除くと、両者は実際には&の違いしかないことがわかります…

2、コピーコンストラクタとムーブコンストラクタの比較
2.1、コピーコンストラクタ
void testCopy()
{
    {
        Container c1(new Data());
        std::cout << "c1.isEmpty: " << c1.isEmpty() << std::endl;
        std::cout << "c1.data = " << c1.getPointer() << std::endl;
        std::cout << "&c1 = " << &c1 << std::endl;
        puts("---------------------");
        Container c2(c1);

        std::cout << "c1.isEmpty: " << c1.isEmpty() << std::endl;
        std::cout << "c1.data = " << c1.getPointer() << std::endl;
        std::cout << "&c1 = " << &c1 << std::endl;

        std::cout << "c2.isEmpty: " << c2.isEmpty() << std::endl;
        std::cout << "c2.data = " << c2.getPointer() << std::endl;
        std::cout << "&c2 = " << &c2 << std::endl;
    }
    std::cout << "Test()" << std::endl;
}
/*		出力
Data()
c1.isEmpty: 0
c1.data = 0x5589714b8e70
&c1 = 0x7ffc29c30858
---------------------
Data(const Data& )
c1.isEmpty: 0
c1.data = 0x5589714b8e70				// c2.dataのアドレスとは異なる
&c1 = 0x7ffc29c30858
c2.isEmpty: 0
c2.data = 0x5589714b92a0				// c1.dataのアドレスとは異なる
&c2 = 0x7ffc29c30860
~Data()
~Data()
testCopy()
*/

  • スコープ内でまずc1変数をスタック領域に割り当て、ヒープ領域にDataを割り当てます。明らかにここではTestのパラメータ付きコンストラクタが呼び出されます
  • c1のdataが空かどうか、およびdataのアドレスを出力し、分割線を出力します
  • 次にc2を作成しますが、c1を使用して作成します。このとき、コピーコンストラクタが呼び出され、c2はc1のdata変数の値を使用して深いコピーを実行します

深いコピーが行われるため、c1.dataとc2.dataのアドレスが異なり、それぞれが独自のdataメンバーを管理していることが自然です。

2.2、ムーブコンストラクタ
void testMove()
{
    {
        Container c1(new Data());
        std::cout << "c1.isEmpty: " << c1.isEmpty() << std::endl;
        std::cout << "c1.data = " << c1.getPointer() << std::endl;
        std::cout << "&c1 = " << &c1 << std::endl;
        puts("---------------------");
        Container c2(std::move(c1));
        std::cout << "c1.isEmpty: " << c1.isEmpty() << std::endl;
        std::cout << "c1.data = " << c1.getPointer() << std::endl;
        std::cout << "c2.isEmpty: " << c2.isEmpty() << std::endl;
        std::cout << "c2.data = " << c2.getPointer() << std::endl;

        std::cout << "&c1 = " << &c1 << std::endl;
        std::cout << "&c2 = " << &c2 << std::endl;
    }
    std::cout << "testMove()" << std::endl;
}
/*		出力
Data()
c1.isEmpty: 0
c1.data = 0x55b5c8274e70
&c1 = 0x7ffeeabea7d8
---------------------
c1.isEmpty: 1
c1.data = 0
c2.isEmpty: 0
c2.data = 0x55b5c8274e70
&c1 = 0x7ffeeabea7d8
&c2 = 0x7ffeeabea7e0
~Data()
testMove()
*/

  • スコープ内でまずc1変数をスタック領域に割り当て、ヒープ領域にDataを割り当てます。明らかにここではTestのパラメータ付きコンストラクタが呼び出されます
  • c1のdataが空かどうか、およびdataのアドレスを出力し、改行を出力します
  • 次に「再作成」するc2ですが、std::move(c1)を使用して作成します。このとき、ムーブコンストラクタが呼び出され、c1はすべてのdataをc2に渡し、自身は空になります。

c1のdataとc2のdataのアドレスが同じであることがわかります。ムーブコンストラクタが発生すると、c1のdataは空になり、c2のdataがそれに取って代わります。そして、c1とc2自身のアドレスも存在しますが、ムーブコンストラクタ時にc1内部のものを右辺値参照に移動させ、最終的にc1は空の殻になるだけです。

2.3、関数呼び出しとムーブコンストラクタ
void testFunction()
{
    std::function<Container()> func = []() -> Container{
        Container c1(new Data());
        std::cout << "c1.isEmpty: " << c1.isEmpty() << std::endl;
        std::cout << "c1.data = " << c1.getPointer() << std::endl;
        return c1;
    };
    {
        Container c2 = func();
        puts("---------------------------------------------------");
        std::cout << "c2.isEmpty: " << c2.isEmpty() << std::endl;
        std::cout << "c2.data = " << c2.getPointer() << std::endl;
    }
    std::cout << "testFunction()" << std::endl;
}
/*
Data()
c1.isEmpty: 0
c1.data = 0x5561f8d91e70
---------------------------------------------------
c2.isEmpty: 0
c2.data = 0x5561f8d91e70
~Data()
testFunction()
*/

この場合、ムーブコンストラクタが呼び出されます

  • ムーブコンストラクタを= delete;に設定すると、コンパイラはエラーを報告してコンストラクタがないことを示します。そして、ムーブコンストラクタを書かないかコメントアウトすると、デフォルトでムーブコンストラクタが追加されます!
3、STLコンテナとムーブコンストラクタ
3.1、戻り値とムーブコンストラクタ
void testVector()
{
    {
        auto func = []() -> std::vector<Data> {
            std::vector<Data> v{Data()};
            puts("-------------------");
            std::cout << "&v[0]" << &v[0] << std::endl;
            return v;
        };
        auto result = func();
        std::cout << "&result[0]" << &result[0] << std::endl;
    }
    std::cout << "testVector()" << std::endl;
}
/*	出力
Data()
Data(const Data& )
~Data()
-------------------
&v[0] = 0x556ce72e2280
&result[0] = 0x556ce72e2280
~Data()
testVector()
*/

  • 出力分析:
    • まず分割線の後を分析します:分割線の後、返されたvとresultが同じdataを保持していることがわかります。つまり、返却時にvector内部ではムーブコンストラクタが呼び出され、実際に格納されている要素に対してコピーコンストラクタが実行されないことを意味します
    • 分割線の前:まずオブジェクトを作成し、vector内部にコピー構築し、newされたオブジェクトを破棄します。Data()のスコープはv{}内にのみ存在します…
  • 結論:returnは所有権を移動するためにムーブコンストラクタのみを呼び出し、ポインタの形式を通じて所有権の引き渡しを完了します
3.2、例の分析
void testContainer()
{
    std::vector<Container> v1;
    {
//        std::cout << (new Data()) << std::endl;              //  1         
//        (new Data())->func();                                //  2
        Container container = Container(new Data());                       //  3
        v1.emplace_back(std::move(container));                   //  4
        std::cout << "&container = " << &container << std::endl;      //  5
    }
    std::cout << "&v1[0] = " << &v1[0] << std::endl;        // 6
    std::vector<Container> v2{std::move(v1)};                    // 7
    std::cout << "&v2[0] = " << &v2[0] << std::endl;        // 8
}
/*		出力
Data()
Container(Data* _data)
Container(Container&& c)
&container = 0x7ffd4f8217a0
~Container()
&v1[0] = 0x55c7369e32a0
&v2[0] = 0x55c7369e32a0
~Data()
~Container()
*/

  • まず(new Data())はヒープ領域にオブジェクトを割り当てるプロセスであり、アドレスを取得できます。したがって、new Data()のような操作は左辺値と見なされます。公開の通常のメソッドを呼び出せる場合。
  • したがって、containerオブジェクトを構築する際に、パラメータ付きコンストラクタ関数が呼び出されてcontainerオブジェクトが構築されます。
  • 4行目のコードはムーブコンストラクタを通じてcontainerオブジェクトのすべてのメンバの内容の所有権をv1コンテナに渡し、この時点でcontainerオブジェクトは空の殻になり、スコープを離れるとデストラクタが呼び出されます
  • 次に7行目でv2コンテナにムーブコンストラクタを通じて渡し、明らかにv1は所有権を移譲しました。
  • 最後にtestContainer関数を離れると、dataとcontainerオブジェクトがデストラクタされます。
4、まとめ
  • 所有権の移転は実装に依存します。ムーブセマンティクスは特別なことをするわけではなく、std::moveはムーブコンストラクタ(ムーブ代入)にマッチするために使用されます
  • 所有権とムーブセマンティクスは切り離せません。ムーブセマンティクスの出現は、これが所有権の移転かコピー複製かを区別するためです。

タグ: C++ 右辺値参照 ムーブセマンティクス コピーコンストラクタ ムーブコンストラクタ

6月19日 22:10 投稿