C++右値参照の浅い分析

10839 ワード

自分が理解して学んだ右の値を引用して関連する技術の詳細を整理して共有してみたいと思っています.興味のある友达に助けてほしいと思っています.
右参照は、C++11規格に追加された機能です.右の値の参照により、プログラマは論理的に不要なコピーを無視できます.また、完璧な転送を実現する関数をサポートするためにも使用できます.これらは、より効率的で堅牢なライブラリです.
moveの意味
特定の右値参照定義は展開されません.まずmoveの意味を話します.右値参照はmoveの意味をサポートするために使用されます.moveの意味は、同じタイプのオブジェクトAのリソース(スタックに割り当てられているか、ファイルハンドルまたは他のシステムリソースであるかのいずれか)を別の同じタイプのオブジェクトBに移動し、オブジェクトAのリソースの所有権を解除することを意味する.これにより、不要な一時オブジェクトの構造、コピー、およびプロファイルなどの動作を低減することができる.たとえば、よく使用するstd::vectorのように、同じstd::vectorタイプが2つ割り当てられている場合、一般的な手順は次のとおりです.
  • 内部の付与構造関数は、通常、指定されたサイズのメモリを先に割り当てる、
  • である.
  • ソースstd::vectorから新規申請のメモリにコピー、
  • 以降は元のオブジェクトインスタンスを解析し,
  • .
  • は最後に新しい申請のデータを引き継いだ.

  • これがC++11以前に使用されていたコピーの意味、つまりよく言われる深いコピーです.moveの意味はコピーの意味とは対照的で、浅いコピーに似ていますが、リソースの所有権が移行しました.moveの意味の実現はコピー動作を低減し,プログラムの性能を大幅に向上させることができる.
    moveの意味の構造を実現するためには,対応する文法が必要である.従来のコピーコンストラクタなどはそのニーズを満たすことができない.最も典型的な例はC++11廃棄std::auto_ptrは,その構造関数が不明確な所有権関係を生じ,BUGを生じやすい.これもstd::auto_が好きではない人が多いです.ptrの原因.C++11はこれに対応する構造関数を追加した.
    class Foo {
    public:
        Foo(Foo&& f) {}
        Foo& operator=(Foo&& f) { 
            return *this;
        }
    };

    ここでは,2つの関数のパラメータタイプがFoo&&&であることが明らかになった.これが右値参照の基本構文です.このような目的は,関数リロードにより異なる機能処理を実現することである.
    強制moveの意味
    C++11は,右値にmove語義を用いてもよいし,左値にmove語義を用いてもよいことを規定している.すなわち,左値を右値参照に変換しmove意味を用いることができる.例えばC++の古典関数swapでは:
    template<class T>
    void swap(T& a, T& b) 
    { 
      T tmp(a);
      a = b; 
      b = tmp; 
    } 
    
    X a, b;
    swap(a, b);

    上のコードには右値はありませんが、tmp変数は本関数の役割ドメインにのみ作用し、データの転送動作を担うために使用されます.C++11で定められた上記の規則は,ここでは逆に非常によく適用できる.C++11はこのルールを達成するためにstd::move関数を実現し,この関数は伝達されたパラメータを右値参照に変換して返す.すなわち,C++11では,swapの実現は以下の通りである.
    template<class T> 
    void swap(T& a, T& b) 
    { 
      T tmp(std::move(a));
      a = std::move(b); 
      b = std::move(tmp);
    } 
    
    X a, b;
    swap(a, b);

    実際の使用ではstd::moveをできるだけ多く使用することもできます.移行構造関数を実装するには、カスタムタイプのみが必要です.
    右参照
    右の値が何を参照しているのかを明らかにするためには、左と右の値を言わざるを得ません.簡単に言えば、左値はメモリ空間を指す式であり、&オペレータでメモリ空間のアドレスを取得することができます.右の値は左以外の値の式です.この《Lvalues and Rvalues》を読んで深く理解することができます.
    右の値の参照はC++の一般的な参照と非常に似ており、複合タイプでもあります.区別を容易にするために、通常の参照は左の参照です.左の参照は、タイプの後に&オペレータを追加することです.右の値の参照は、上の転送構造関数のパラメータのように、タイプの後に&&オペレータを追加します.
    右値参照の動作は左値参照と似ていますが、右値参照は一時オブジェクトのみをバインドし、左値参照はバインドできません.右の値参照の出現は、関数の再ロード決定にも影響します.左は左参照パラメータの関数を優先し、右は右参照パラメータの関数を優先します.
    void foo(X& x); // lvalue reference overload
    void foo(X&& x); // rvalue reference overload
    
    X x;
    X foobar();
    
    foo(x); // argument is lvalue: calls foo(X&)
    foo(foobar()); // argument is rvalue: calls foo(X&&)

    理論的には、このような方法で任意の関数を再ロードすることができますが、ほとんどの場合、このような再ロードはコピー構造関数と付与演算子、すなわちmoveの意味を実現するためにのみ表示されます.
    もしあなたがvoid foo(X&)を実現したら、しかしvoid foo(X&&&);は実現せず、では、以前と同じfooのパラメータは左の値しかありません.void foo(X const&);が実現したら、しかしvoid foo(X&&&);は実現せず、従来と同様にfooのパラメータは左でも右でもよい.左と右を区別できる唯一の方法はvoid foo(X&&);を実現することである.最後に、void foo(X&&);、しかしvoid foo(X&);とvoid foo(X const&);ではfooのパラメータは右の値しかありません.
    右参照は右ですか?
    void foo(X&& x)
    {
      X anotherX = x;
      // ...
    }

    上の関数foo内で、Xのどの構造関数が呼び出されますか?コピー構造ですか、それとも転送構造ですか.私たちが前に言ったように、これは右の値の参照で、呼び出されたX(X&&);で行ないます.しかし実際には、ここで呼び出されたのはX(const X&);ここで迷った点は、右値参照タイプは左値としても右値としてもよく、その右値参照に名前があるかどうかを判断する基準です.名前があれば左、そうでなければ右です.名前付きの右値参照を右値に変更するには、std::move関数を使用します.
    void foo(X&& x)
    {
      X anotherX = std::move(x);
      // ...
    }

    独自の転送コンストラクション関数を実装する際、一部の人はそれを理解していないため、独自の転送コンストラクション関数内部の実装で実際にコピーコンストラクション関数が実行される.
    moveの意味と戻り値の最適化
    moveの意味と強制moveと右値参照の概念を理解すると、関数を実装するときに戻った場所で強制moveを行う友人もいます.これにより、コピーを1回減らすことができると考えられています.例:
    X foo()
    {
      X x;
      // perhaps do something to x
      return std::move(x); // making it worse!
    }

    実はこれは必要ありません.コンパイラは戻り値最適化(Return Value Optimization)を行うためです.C++11規格には以下の規定がある.
    When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.
    直接return x;NRVOがサポートする使用例シーンで、余分なコピー構造を実現できます.コンパイラは、コピー構造を使用するかmove構造関数を使用するかを自分で選択します.
    しかしstd::move(x);NRVOを阻止する可能性がありますすなわち、moveの意味を実行するために追加のオーバーヘッドが必要になる可能性があります.
    Stackoverflowの2つの質問を読むことをお勧めします.
    《When should std::move be used on a function return value?》
    《Why does std::move prevent RVO?》
    完全転送
    右値参照はmoveの意味を実現するために用いられるほか,完全な転送の問題を解決するために用いられる.工場関数を書くことがあります.たとえば、次のコードがあります.
    template<typename T, typename Arg> 
    shared_ptr<T> factory(Arg arg)
    { 
      return shared_ptr<T>(new T(arg));
    }

    この実装は非常に簡単で,パラメータargをクラスTに渡して構築する.しかし、ここでは、参照をパラメータとするコンストラクション関数には使用されない追加のパス値の関数呼び出しが導入されています.
    では、この問題を解決するために、引用を考えている人がいます.例えば、
    template<typename T, typename Arg> 
    shared_ptr<T> factory(Arg& arg)
    { 
      return shared_ptr<T>(new T(arg));
    }

    しかし、ここではまた問題があり、パラメータとして右の値を受信できません.
    factory<X>(hoo()); // error if hoo returns by value
    factory<X>(41); // error

    対応する解決策はconstリファレンスの導入を継続することである.複数のパラメータがある場合、この関数のパラメータリストは気持ち悪くなります.同時にmoveの意味が実現できないという問題もある.
    右値参照はこの問題を解決することができ,リロード関数を用いずに真の完璧な転送を実現することができる.ただし、右の2つの参照ルールを組み合わせる必要があります.
  • は、重畳規則
  • を参照する.
    A& & => A&
    A& && => A&
    A&& & => A&
    A&& && => A&&
  • テンプレートパラメータ導出規則
  • template<typename T>
    void foo(T&&);

    関数fooの実パラメータがAタイプの左値である場合,TのタイプはA&である.さらに参照重ね合わせ規則に基づいて判断し,最後のパラメータの実際のタイプはA&である.fooの実パラメータがAタイプの右値である場合,TのタイプはAである.引用重ね合わせルールから判断できますが、最後のタイプはA&&.
     
    上記のルールがあれば、前の完璧な転送問題を右値参照で解決することができます.次は解決策です.
    template<typename T, typename Arg> 
    shared_ptr<T> factory(Arg&& arg)
    { 
      return shared_ptr<T>(new T(std::forward<Arg>(arg)));
    }

    std::forwardの実現は以下の通りです.
    template<class S>
    S&& forward(typename remove_reference<S>::type& a) noexcept
    {
      return static_cast<S&&>(a);
    }

    ここでは具体的な例を展開して説明しないが,上記の2つのルールが分かれば分かる.Scott Meyersの《Universal References in C++11》を読むことをお勧めします.
     
    リファレンスドキュメント
  • 《C++ Rvalue References Explain》
  • 《Universal References in C++11》
  • 《A Brief Introduction to Rvalue References》

  •  
    まとめ
    右値参照の出現は追加の複雑さを増したように見えますが、moveの意味を実現し、プログラムの性能を向上させるのに役立つ収益は非常に明らかです.また、完璧な転送を実現し、ライブラリの設計を便利にします.
    C++はこのようにして、あなたに新しい特性を追加した後、追加の学習の難しさをもたらします.しかし、これも多くの人がC++が好きな理由で、プログラマーに多くの可能性を与えています.オブジェクトのライフサイクルを正確に制御できるのは、高性能プログラムに欠かせないツールです.