C++左値右値と右値参照

39646 ワード

本稿では,C++左右値の定義と,C++11における右値参照の用途をまとめる.
転載:C++11新特性3-左右値と右値参照C++11:Rvalue Reference–Move Sematics C++11:Rvalue Reference–Fowarding
C++11は,右値参照を導入することで性能を最適化し,意味を移動することで無駄なコピーを低減し,完全な転送によってパラメータの実際のタイプに従うことができないことを解決する.
要点:
  • 右値とは、文字値と一時値/死値を右値、またはアドレスを取得できない値を右値
  • と呼ぶ.
  • 右値参照特徴1:参照関数が返すアポトーシス値は、アポトーシス値のライフサイクルを延長し、オブジェクトコピー回数を減らす(const定数参照も同様の効果を実現し、コンパイラ戻り値最適化RVOはより良い効果を実現する(g++-fno-elide-constructorsオフ最適化))
  • 右値参照特徴2:右値も左値も参照可能、
  • 右値参照特徴3:T&&tは未定の参照タイプ(汎用参照)であり、左値か右値かはその初期化に依存し、
  • を完全に転送することができる.
    1.左と右
    C++では、左右の値の簡略定義:
  • 左:一定のメモリを占有し、認識可能なアドレスを有するオブジェクト
  • .
  • 右:左以外のすべてのオブジェクトの典型的な左値、C++のほとんどの変数は左
  • です.
    int i = 1;   // i    
    int *p = &i; // i         
    i = 2; // i           
    
    class A;
    A a; //                 
    

    標準の右
    int i = 2; // 2    
    int x = i + 2; // (i + 2)    
    int *p = &(i + 2);//  ,        
    i + 2 = 4; //   ,       
    
    
    class A;
    A a = A(); // A()    
    
    int sum(int a, int b) {
         return a + b;}
    int i = sum(1, 2) // sum(1, 2)    
    

    参照(左参照)
    int i;
    int &r = i;
    int &r = 5; //   ,          
    
    // const      
    //                ,   5
    const int &r = 5;
    

    関数の場合、同様に
    int square(int& a) {
         return a * a;}
    
    int i = 2;
    square(i); //   
    square(2); //   ,        
    
    //     
    int square(const int& a) {
         
      return a * a;
    } 
    //      square(2)
    

    左は右、右は左
    int i = 2;
    int x = i + 2; // i + 2    
    
    int v[3];
    //                
    *(v + 1) = 2;
    

    いくつかの注意事項
    // 1.          
    int myglobal ;
    int& foo() {
         return myglobal;}
    foo() = 50;
    
    //        ,[]           
    array[3] = 50; 
    
    // 2.           
    // i   ,      
    const int i = 1;  
    
    // 3.          
    class dog;
    // bark()       dog()   
    dog().bark();
    

    2.右参照
    右の値を参照する2つの大きな用途:
  • 一時的なライフサイクルを延長
  • モバイル構造、浅いコピー
  • 完全転送、左値と右値を区別
  • 2.1右の値は一時値を参照
    次のコードは、Aのコンストラクション関数を何回か呼び出します.
    A getA()
    {
         
    	return A();
    }
    int main()
    {
         
    	//          A     (1     ,2     )
    	A a1 = getA();
    	//          A     (1     ,1     )
    	const A& a2 = getA();
    	//          A     (1     ,1     )
    	A& a3 = getA();
    }
    

    右の値の参照は、一時変数の宣言サイクルを延長し、定数の左の値の参照と同様の効果を発揮します.ただし、定数左値参照は定数であり、右値参照は定数ではなく、オブジェクトのconst関数と非const関数を呼び出すことができます.
    2.2移動語意と移動構造
    クラスには通常、コピーコンストラクション関数がありますが、浅いコピーと深いコピーの2つのコンストラクション関数が必要になる場合があります.特に、コピー構造のパラメータが一時的な値である場合、メンバー変数の所有権を新しいオブジェクトに移動するだけで、右の値参照の重要な役割は、移動の意味をサポートすることです.
    一時オブジェクトからコピーして構築する場合は、浅いコピーを呼び出します.右のオブジェクトからコピーして構築する場合は、次のようにします.
    class A
    {
         
    public:
    	A()
    	{
         
    	}
    	A(const A&a)	//     
    	{
         
    		size_t len = strlen(a->str);
    		str = (char*)malloc(len+1);
    		memset(str,0,len+1);
    		memcpy(str,a->str,len);
    	}
    	A(A&&a)			//     
    	{
         
    		str = a->str;
    		a->str = NULL;
    	}
    	~A()
    	{
         
    		if(nullptr != str)
    		{
         
    			free(str);
    			str = NULL;
    		}
    	}
    
    	char * str =NULL;
    }
    
    A GetA()
    {
         
        return A();
    }
    
    int main()
    {
         
    	A a = GetA();	//       
    	A a2 = a;	//       	
    }
    

    2.2.1 std::move右値参照タイプ変換
    上の移動コンストラクション関数では、パラメータは右の値で参照する必要があります.左の値も移動コンストラクション関数を使用して新しいオブジェクトを作成したい場合は、どうすればいいですか?
    移動語意(std::move)は、左値を右値参照に変換できます.ソースコードを表示します.std::moveはタイプ変換と同じです.static_cast(lvalue).
    または、上記のコードテストを使用します.
    
    int main()
    {
         
    	A a = GetA();	//       
    	A a2 = a;		//       
    	A a3 = std::move(a);	//       ,1. std::move(a)   a     ;2.  a         a3
    }
    
    

    その他のテスト:
    int a = 1; //   
    int &b = a; //     
    
    //     :          
    int &&c = std::move(a); 
    
    void printInt(int& i) {
         
      cout << "lval ref: " << i << endl; 
    }
    void printInt(int&& i) {
         
      cout << "rval ref: " << i << endl; 
    } 
    
    int main() {
         
      int i = 1;
      
      //    printInt(int&), i   
      printInt(i);
      
      //    printInt(int&&), 6   
      printInt(6);
      
      //    printInt(int&&),    
      printInt(std::move(i));   
    }
    

    コンパイラ呼び出し時に区別できないため
  • printInt(int)とprintInt(int&)
  • printInt(int)とprintInt(int&)
  • printInt(int)関数を再定義すると、エラーが発生します.
    なぜ意味を動かす必要があるのか
    class myVector {
         
      int size;
      double* array;
    public:
      //       
      myVector(const myVector& rhs) {
           
        std::cout << "Copy Construct
    "
    ; size = rhs.size; array = new double[size]; for (int i=0; i<size; i++) { array[i] = rhs.array[i]; } } myVector(int n) { size = n; array = new double[n]; } }; void foo(myVector v) { /* Do something */ } // , MyVector myVector createMyVector(); int main() { // Case 1: myVector reusable=createMyVector(); // myVector // foo reusable // ok foo(reusable); /* Do something with reusable */ // Case 2: // createMyVector // // // foo(createMyVector()); }

    解決策、移動構造関数の追加
    //       
    myVector(myVector&& rhs) {
           
        std::cout << "Move Constructor
    "
    ; size = rhs.size; array = rhs.array; rhs.size = 0; rhs.array = nullptr; }

    ではfoo(createMyVector()はコピーコンストラクタを呼び出すのではなく、モバイルコンストラクタを呼び出す(もちろん、コンパイラが直接このステップを最適化する可能性が高い)
    しかしながら、C++03では、この問題を解決するために、2つのfoo関数を定義する必要がある場合があり、比較的面倒である.
  • foo_by_value(myVector)
  • foo_by_ref(myVector&)

  • また、fooを呼び出すとreusableは使用されなくなり、移動語の意味を使用することができます.
    int main () {
         
     myVector reusable = createMyVector();
     //       myVector        
     foo(std::move(reusable));
     /* No use of reusable anymore */
    }
    

    もう1つの利点は、割り当て演算子を再ロードすることです.
    X& X::operator=(X const & rhs); 
    X& X::operator=(X&& rhs);
    

    C++11からすべてのSTLが移動構造を実現した
    2.3完全な転送と参照の折りたたみ
    関数テンプレートでは、テンプレートのパラメータのタイプ(すなわち、パラメータの左値、右値の特徴を保持する)に完全に従って、関数テンプレートで呼び出された別の関数にパラメータを渡します.完璧な転送にはstd::forward関数が必要です.
    パーフェクト転送とは、実際に参照された値のタイプを右に、左を左に、右を右に、右を参照された関数に転送することです.モバイル構造も完璧な転送によって実現されることが分かった.
  • 左値は左値に転送する、右値は右値
  • に転送される.
    void show_log(string& str) {
          cout << "string & show " << str.data() << endl; }
    void show_log(string &&str) {
          cout << "string && show " << str.data() << endl; }
    
    //     
    template <typename T>
    void show(T&& arg)
    {
         
        // show_log(arg); // arg     ,   ,          
        show_log(std::forward<T>(arg));	// std::forward        
    }
    
    int main()
    {
         
        string msg = "hello";
        show(msg);
        show(std::move(msg));
    }
    
    void foo(myVector& v) {
         }
    
    //     
    template<typename T>
    void relay(T arg) {
         
        foo(arg);
    }
    
    int main() {
         
      myVector reusable= reateMyVector();
      
      //       
      relay(reusable); 
    
      //       
      relay(createMyVector()); 
    }
    

    この実装には2つのバージョンのfooが定義されている場合に問題があります.
    void foo(myVector& v) {
         }
    void foo(myVector&& v) {
         }
    

    常にfoo(myVector&v)だけが呼び出されます(右の値はmyVector&&vを参照して左の値で、ここのvは名前があります)
    したがって、std::forwardを使用して、上記のrelay関数を書き換える必要があります.
    template<typename T>
    void relay(T&& arg) {
         
        foo(std::forward<T>(arg));
    }
    

    そしてある
  • relay(reusable)呼び出しfoo(myVector&)
  • relay(createMyVector()呼び出しfoo(myVector&)
  • 完璧な転送の原理を説明するには,まずC++11の引用折り畳み原則を導入する.
  • T& & => T&
  • T& && => T&
  • T&& & => T&
  • T&& && => T&&

  • だからrelay関数では
  • relay(9); => T = int&& => T&& = int&& && = int&&
  • relay(x); => T = int& => T&& = int& && = int &

  • 従って、この場合、T&&はuniversal referenceと呼ばれ、すなわち満足する
  • Tはテンプレートタイプ
  • である.
  • Tは、クラステンプレートタイプ
  • ではなく、関数テンプレートタイプである参照折り畳みが適用されるように導出される.
    そして、C++11はremove_を定義するreference、参照指向のタイプを返す
    template<class T>
    struct remove_reference; 
    
    remove_reference<int&>::type == int
    remove_reference<int>::type  == int
    

    そこでstd::forwordの実現は以下の通りである.
    template<class T>
    T&& forward(
    typename remove_reference<T>::type& arg
    ) {
         
      return static_cast<T&&>(arg);
    }
    

    右の値の参照(右の値の参照自体が左の値)を右の値に変更したのと同じで、左の値は変更されません.
    3.まとめ
    std::move比較std::forward:
  • std::move(arg)argを右の
  • に変換
  • std::forward(arg)argをT&&
  • に変換