std::move C++11標準新特性:右値参照と転送の意味

9253 ワード

新しい特性の目的
右参照(Rvalue Reference)は、C++の新しい規格(C++11、11は2011年を表す)に導入された新しい特性であり、移動の意味(Move Sementics)と正確な伝達(Perfect Forwarding)を実現している.主な目的は2つあります.
  • は、2つのオブジェクトが相互作用するときに不要なオブジェクトコピーを除去し、演算ストレージリソースを節約し、効率を向上させる.
  • は、汎用関数をより簡潔かつ明確に定義することができる.

  • トップに戻る
    左と右の定義
    C++(Cを含む)のすべての式と変数は、左または右のいずれかです.一般的な左値の定義は、複数の文で使用できる非一時オブジェクトです.すべての変数はこの定義を満たし、複数のコードで使用できます.左です.右の値は、現在の文でのみ有効な一時的なオブジェクトです.次の例を参照してください.
  • 単純な付与文
     :int i = 0;

    この文では、iは左、0は一時、右です.次のコードでは、iは参照できますが、0はできません.即時数はすべて右です.
  • の右の値は、付与式の左側にも表示されますが、付与のオブジェクトとして使用できません.右の値は現在の文でのみ有効であり、付与には意味がありません.例:((i>0) ? i : j) = 1;この例では、0は右の値として"="の左側に現れる.ただし、付与対象はiまたはjであり、いずれも左値である.C++11の前に、右の値は参照できません.最大は、次のような定数参照で右の値をバインドします.
     const int &a = 1;

    この場合、右の値は変更できません.しかし、実際には右の値は次のように変更できます.
     T().set().get();

    Tはクラスで、setはTの変数に関数を割り当て、getはこの変数の値を取り出すために使用されます.この文では,T()は一時オブジェクト,すなわち右値を生成し,set()は変数の値を修正し,この右値を修正する.右の値を変更できる以上、右の値参照を実現できます.右の値の参照は、実際のエンジニアリングの問題を容易に解決し、非常に魅力的なソリューションを実現します.

  • トップに戻る
    左と右の構文記号
    左の宣言記号は「&」で、左の値と区別するために右の値の宣言記号は「&&」です.
    サンプル・プログラム:
     void process_value(int& i) { 
      std::cout << "LValue processed: " << i << std::endl; 
     } 
    
     void process_value(int&& i) { 
      std::cout << "RValue processed: " << i << std::endl; 
     } 
    
     int main() { 
      int a = 0; 
      process_value(a); 
      process_value(1); 
     }

    実行結果:
     LValue processed: 0 
     RValue processed: 1

    Process_value関数はリロードされ、左値と右値がそれぞれ受け入れられます.出力結果から,一時オブジェクトは右値として処理されていることが分かる.
    しかし、一時オブジェクトが右の値を受け入れる関数を介して別の関数に渡されると、この一時オブジェクトは伝達中に名前付きオブジェクトになったため、左の値になります.
    サンプル・プログラム:
     void process_value(int& i) { 
      std::cout << "LValue processed: " << i << std::endl; 
     } 
    
     void process_value(int&& i) { 
      std::cout << "RValue processed: " << i << std::endl; 
     } 
    
     void forward_value(int&& i) { 
      process_value(i); 
     } 
    
     int main() { 
      int a = 0; 
      process_value(a); 
      process_value(1); 
      forward_value(2); 
     }

    実行結果:
     LValue processed: 0 
     RValue processed: 1 
     LValue processed: 2

    2この即時数は関数forward_にあるがvalue受信時は右ですがprocess_value受信時、左値になりました.
    トップに戻る
    意味の定義を移す
    右の値参照は、転送の意味をサポートするために使用されます.転送の意味は、リソース(スタック、システムオブジェクトなど)を1つのオブジェクトから別のオブジェクトに転送することで、不要な一時オブジェクトの作成、コピー、破棄を低減し、C++アプリケーションのパフォーマンスを大幅に向上させることができます.一時オブジェクトのメンテナンス(作成と破棄)は、パフォーマンスに深刻な影響を及ぼします.
    転送の意味はコピーの意味とは対照的で、ファイルの切り取りとコピーをクラスすることができ、あるディレクトリから別のディレクトリにファイルをコピーすると、切り取りよりも速度が遅くなります.
    意味を移すことで、一時オブジェクトのリソースは他のオブジェクトに移行することができます.
    既存のC++メカニズムでは,コピー構造関数と付与関数を定義できる.転送の意味を実現するには、転送構造関数を定義する必要があります.また、転送付与オペレータを定義することもできます.右の値のコピーと割り当てについては、転送コンストラクタと転送割り当てオペレータが呼び出されます.トランスファコンストラクタとトランスファコピーオペレータが定義されていない場合は、既存のメカニズムに従い、コピーコンストラクタと付与オペレータが呼び出されます.
    通常の関数とオペレータは、右の値参照オペレータを使用して転送の意味を実現することもできます.
    トップに戻る
    転送構造関数と転送付与関数の実装
    簡単なstringクラスを例に、コピーコンストラクタとコピー付与オペレータを実装します.
    サンプル・プログラム:
     class MyString { 
     private: 
      char* _data; 
      size_t   _len; 
      void _init_data(const char *s) { 
        _data = new char[_len+1]; 
        memcpy(_data, s, _len); 
        _data[_len] = '\0'; 
      } 
     public: 
      MyString() { 
        _data = NULL; 
        _len = 0; 
      } 
    
      MyString(const char* p) { 
        _len = strlen (p); 
        _init_data(p); 
      } 
    
      MyString(const MyString& str) { 
        _len = str._len; 
        _init_data(str._data); 
        std::cout << "Copy Constructor is called! source: " << str._data << std::endl; 
      } 
    
      MyString& operator=(const MyString& str) { 
        if (this != &str) { 
          _len = str._len; 
          _init_data(str._data); 
        } 
        std::cout << "Copy Assignment is called! source: " << str._data << std::endl; 
        return *this; 
      } 
    
      virtual ~MyString() { 
        if (_data) free(_data); 
      } 
     }; 
    
     int main() { 
      MyString a; 
      a = MyString("Hello"); 
      std::vector<MyString> vec; 
      vec.push_back(MyString("World")); 
     }

    実行結果:
     Copy Assignment is called! source: Hello 
     Copy Constructor is called! source: World

    このstringクラスは、私たちのプレゼンテーションのニーズを基本的に満たしています.main関数では,コピーコンストラクタを呼び出す操作とコピー付与オペレータの操作を実現した.MyString(「Hello」)もMyString(「World」)も一時的なオブジェクト、つまり右の値です.これらは一時的であるが、プログラムはコピー構造とコピー付与を呼び出し、意味のないリソース申請と解放の操作をもたらした.一時オブジェクトが既に申請したリソースを直接使用できれば、リソースを節約でき、リソース申請と解放の時間を節約できます.これが転送の意味を定義する目的です.
    まず、転送構造関数を定義します.
      MyString(MyString&& str) { 
        std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
        _len = str._len; 
        _data = str._data; 
        str._len = 0; 
        str._data = NULL; 
     }

    コピーコンストラクション関数と同様に、次の点に注意してください.
    1.パラメータ(右)の記号は、右参照記号(&&)である必要があります.
    2.パラメータ(右)は定数ではありません.右の値を変更する必要があるからです.
    3.パラメータ(右)のリソースリンクとタグは変更する必要があります.そうでない場合、右の値の構造関数はリソースを解放します.新しいオブジェクトに移行したリソースも無効です.
    次に、転送割り当てオペレータを定義します.
      MyString& operator=(MyString&& str) { 
        std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
        if (this != &str) { 
          _len = str._len; 
          _data = str._data; 
          str._len = 0; 
          str._data = NULL; 
        } 
        return *this; 
     }

    ここで注意すべき問題は,遷移構造関数と同様である.
    転送コンストラクタと転送レプリケーションオペレータを追加すると、プログラムの実行結果は次のとおりです.
     Move Assignment is called! source: Hello 
     Move Constructor is called! source: World

    これにより、コンパイラは左値と右値を区別し、右値に対して転送コンストラクタと転送付与オペレータを呼び出します.リソースを節約し、プログラムの実行効率を向上させます.
    右の値の参照と転送の意味があれば、クラスを設計して実装する際に、大量のリソースを動的に申請する必要があるクラスに対して、アプリケーションの効率を向上させるために、転送構造関数と転送付与関数を設計する必要があります.
    トップに戻る
    標準ライブラリ関数std::move
    コンパイラが右の値のみを参照している以上、転送コンストラクタと転送付与関数を呼び出すことができ、すべてのネーミングオブジェクトは左の値のみを参照することができます.ネーミングオブジェクトが使用されなくなったことが知られている場合、転送コンストラクタと転送付与関数を呼び出したい場合は、左の値参照を右の値参照として使用します.どうすればいいですか?標準ライブラリには、左の参照を右の参照に変換する非常に簡単な方法で関数std::moveが用意されています.
    サンプル・プログラム:
     void ProcessValue(int& i) { 
      std::cout << "LValue processed: " << i << std::endl; 
     } 
    
     void ProcessValue(int&& i) { 
      std::cout << "RValue processed: " << i << std::endl; 
     } 
    
     int main() { 
      int a = 0; 
      ProcessValue(a); 
      ProcessValue(std::move(a)); 
     }

    実行結果:
     LValue processed: 0 
     RValue processed: 0
    std::moveはswap関数の性能を向上させる上で非常に役立ち、一般的にswap関数の一般的な定義は以下の通りである.
        template <class T> swap(T& a, T& b) 
        { 
            T tmp(a);   // copy a to tmp 
            a = b;      // copy b to a 
            b = tmp;    // copy tmp to b 
     }

    std::move,swap関数の定義があります.
        template <class T> swap(T& a, T& b) 
        { 
            T tmp(std::move(a)); // move a to tmp 
            a = std::move(b);    // move b to a 
            b = std::move(tmp);  // move tmp to b 
     }

    std::moveにより,簡単なswap関数で3回の不要なコピー操作を回避した.
    トップに戻る
    正確な伝達(Perfect Forwarding)
    本文はこの意味を正確に伝えることを採用している」と述べた.Perfect Forwardingも完璧に転送され、正確に転送されるなど、同じ意味で使われています.
    正確な転送は、パラメータのセットをそのまま別の関数に渡す必要があるシーンに適用されます.
    「そのまま」はパラメータの値が変わらないだけでなく、C++にはパラメータの値のほかに、2つの属性があります.
    左/右およびconst/non-const.正確な伝達とは、パラメータ伝達中にこれらの属性とパラメータ値を変更できないことです.汎用関数では,このような需要は非常に普遍的である.
    次に例を挙げて説明する.関数forward_valueは、あるパラメータを別の関数process_に渡す汎用関数です.value.
    forward_valueの定義は次のとおりです.
     template <typename T> void forward_value(const T& val) { 
      process_value(val); 
     } 
     template <typename T> void forward_value(T& val) { 
      process_value(val); 
     }

    関数forward_valueは、各パラメータに対して2つのタイプ、T&とconst T&を再ロードする必要があります.そうしないと、次の4つの異なるタイプのパラメータの呼び出しで同時に満たすことはできません.
      int a = 0; 
      const int &b = 1; 
      forward_value(a); // int& 
      forward_value(b); // const int& 
     forward_value(2); // int&

    1つのパラメータに対して2回再ロードされます.すなわち、関数の再ロード回数とパラメータの個数は比例します.この関数の定義回数はプログラマーにとって非常に非効率である.右の値の参照がこの問題を解決する方法を見てみましょう.
     template <typename T> void forward_value(T&& val) { 
      process_value(val); 
     }

    一度定義して、右の値で参照されるパラメータを1つ受け入れるだけで、すべてのパラメータタイプをそのままターゲット関数に渡すことができます.4種類のタイプパラメータを使わない呼び出しはすべて満たすことができて、パラメータの左右の値の属性とconst/non-cosntの属性は完全に目標関数process_に伝わりますvalue.この解決策は簡潔で優雅ではないでしょうか.
      int a = 0; 
      const int &b = 1; 
      forward_value(a); // int& 
      forward_value(b); // const int& 
      forward_value(2); // int&&

    C++11で定義されたT&&の導出規則は、
    右の実パラメータは右の参照で、左の実パラメータは左の参照です.
    一言で言えば、パラメータの属性は変わらない.これにより,パラメータの完全な伝達も完璧に実現された.
    右の値の参照は、表面的には参照記号が1つ追加されただけですが、C++ソフトウェア設計やクラスライブラリの設計に非常に大きな影響を及ぼします.コードを簡素化し、プログラムの実行効率を向上させることができます.各C++ソフトウェアデザイナーとプログラマーは、それを理解し、応用することができるはずです.クラスを設計する際に動的に申請するリソースがあれば,転送構造関数と転送コピー関数も設計すべきである.クラスライブラリを設計する際にはstd::moveの使用シーンも考慮し、積極的に使用する必要があります.
    トップに戻る
    まとめ
    右の値の参照と転送の意味はC++の新しい標準の重要な特性です.各専門のC++開発者は、実際のプロジェクトに把握し、応用しなければならない.コードを再構築する機会がある場合も,新しいものを適用できるかどうかを考えるべきである.使用する前に、コンパイラのサポート状況を確認する必要があります.