[C++]右参照:移動の意味と完全な転送

23384 ワード

C++11が導入した新しい特性のうち,メモリモデルと関連施設を併発するほか,最も魅力的で接地ガスの特性は『右値参照』(rvalue reference)である.右の参照を追加する動機は、不要なリソースコピーを減らすことです.次の手順を考慮します.
std::vector v;
v.push_back("string");

vectorに要素を追加するには、string::string(const char*)、string::string(const string&)、string:~string()の3つの関数を前後して呼び出す必要があります.2回のメモリコピーに関連します.1回目は字面定数「string」を使用して一時オブジェクトを構築し、2回目はこの一時オブジェクトを使用してvectorの新しい要素を構築します.「最後に一時的な対象が析出する」.
意味の移動
上記のプログラム操作の問題点は,一時オブジェクトの構造と解析が不要なリソースコピーをもたらすことである.一時オブジェクトを文法的に認識する仕組みがあれば,一時オブジェクトを用いて新しいオブジェクト(コピー構造)を構築する際に,一時オブジェクトが持つリソースを新しいオブジェクトに『移行』することで,このような不要なコピーを排除できる.このような文法的メカニズムが『右引用』であるのに対し、伝統的な引用は『左引用』と呼ばれている.左の値の参照には「&」識別子(string&)、右の値の参照には「&&」識別子(string&&&)が使用されます.ちなみに左(lvalue)とは何か(rvalue):アドレスを取得できる名前付きオブジェクトは左です.値を取得できないオブジェクトは、匿名の一時オブジェクトとすべてのフォント値(literal value)を含む右値です.右値の構文サポートにより、移動の意味を実現するためには、従来のコピーコンストラクタと付与オペレータを右値をパラメータとして再ロードする必要があります.結局、どのリソースが移動できるのか、クラスのみをコピーできるインプリメンテーション者が知っているのか.移動意味のコピー『構造』については、ソースオブジェクトのリソースを目的オブジェクトにバインドし、ソースオブジェクトのリソースへのバインドを解除するのが一般的である.割り当て操作の場合、一般的なプロセスは、まず目的のオブジェクトが持つリソースを破棄し、次にリソースのバインドを変更することです.また、もちろん、従来の構造や付与値と同様に、構造の異常な安全や自己付与も考慮される.プレゼンテーションとして:
class String {
public:
    String(const String &rhs) { ... }
    String(String &&rhs) {
        s_ = rhs.s_;
        rhs.s_ = NULL;
    }
    String& operator=(const String &rhs) { ... }
    String& operator=(String &&rhs) {
        if (this != &rhs) {
            delete [] s_;
            s_ = rhs.s_;
            rhs.s_ = NULL;
        }
        return *this;
    }
private:
    char *s_;
};

右にバインドされた右の値の参照は、名前が付いているため、「左の値」であることに注意してください.考慮:
class B {
public:
    B(const B&) {}
    B(B&&) {}
};
class D : public B {
    D(const D &rhs) : B(rhs) {}
    D(D &&rhs) : B(rhs) {}
};
D getD();
D d(getD());

上のプログラムでは、B::B(B&&&)は呼び出されません.このため、C++11にはstd::move(T&&t)テンプレート関数が導入され、tは右値に変換されます.
class D : public B {
    D(D &&rhs) : B(std::move(rhs)) {}
};

std::moveの可能な実装:
template <typename T>
typename remove_reference<T>::type&&
move(T &&t) {
    return static_cast<remove_reference<T>::type&&>(t);
}

バインド規則
右値参照を導入すると、「参照」から「値」へのバインドルールも拡張されます.
  • 左値参照は左値:int xにバインドできます.int &xr = x;
  • 非常量の左値参照は右値にバインドできません:int&r=0;
  • 定数左値参照は、左値と右値:int xにバインドできます.const int &cxr = x; const int &cr = 0;
  • 右値参照は右値:int&&r=0にバインドできます.
  • 右値参照は左値:int xにバインドできません.int &&xr = x;
  • 定数右値参照には現実的な意味はありません(結局、右値参照の初志は意味を移動することであり、移動は『修正』を意味します).

  • ここで、第5の規則『適用しない』関数テンプレートのパラメータは、例えば、以下の関数は任意のタイプのパラメータを受け入れることができ、右値でも左値でも定数でも非常量でもよい.
    template <typename T>
    void foo(T &&t);
    int x;
    const int xx;
    foo(x); //~ OK
    foo(xx); //~ OK
    foo(10); //~ OK

    T&&パラメータは左の値を受け入れることができ、C++11がこのような特殊な状況に対して行ったルールの改訂であり、「完璧な転送」(perfect forwarding)を実現することを目的としている.
    完全転送
    C++11以前は,パラメータ『転送』の問題があり,完璧な転送が容易に実現できない.転送の目的は、cvプロパティ(const/volatile)や左右値プロパティなど、「参照パラメータ」の追加プロパティを渡すことです.この問題を描くために,左右値属性の伝達を例に(cv属性にも類似の問題がある),以下のクラス定義を参照する.
    class X
    {
    public:
        X(const std::string &s, const std::vector &v) : s_(s), v_(v) {}
    private:
        std::string s_;
        std::vector v_;
    };

    移動の意味をサポートするには、コンストラクション関数を再ロードする必要があります.コンストラクション関数には2つのパラメータがあるため、右参照と左参照の組み合わせも考慮する必要があります.
    class X
    {
    public:
        X(const std::string &s, const std::vector &v) : s_(s), v_(v) {}
        X(std::string &&s, const std::vector &v) : s_(std::move(s)), v_(v) {}
        X(const std::string &s, std::vector &&v) : s_(s), v_(std::move(v)) {}
        X(std::string &&s, std::vector &&v) : s_(std::move(s)), v_(std::move(v)) {}
    private:
        std::string s_;
        std::vector v_;
    };

    コンストラクション関数にn個のパラメータがある場合は、2^n個のリロードが必要です!C++11では,右値参照に基づく関数テンプレートによりこの問題を解決し,本質的には実パラメトリックタイプの推論により,実際の状況に応じてコンパイラによって自動的な『リロード』を完了する.
    class X
    {
    public:
        template <typename T1, typename T2>
        X(T1 &&s, T2 &&v) : s_(std::forward<T1>(s)), v_(std::forward<T2>(v)) {}
    private:
        std::string s_;
        std::vector v_;
    };

    この転送を紹介する前に,右値がパラメータを参照する関数テンプレートの実パラメータ推定規則,すなわち参照折り畳み(reference collapsing)を知る必要がある.BTW.C++11までは、参照の参照タイプ(reference to reference)にバインドすることはできません.Tをテンプレートのタイプパラメータ、Aを実パラメータの基本タイプとすると、次のようになります.
    T
    パラメータ
    折りたたみT
    折りたたみ後の実パラメータタイプ
    A&
    T&
    A
    A&
    A&
    T&&
    A&
    A&
    A&&
    T&
    A&
    A&
    A&&
    T&&
    A
    A&&
    関数のパラメータがT&&&と宣言されている場合、実パラメータが右または右の値で参照されている場合にのみ、折りたたまれた実パラメータタイプが右の値で参照され、そうでない場合は左の値で参照されます.この折りたたみルールにより,左右値参照属性の転送が可能となる.std::forwardは簡単に実現できます.
    template <typename T>
    T&& forward(T &&t)
    {
        return static_cast<T&&>(t);
    }

    まとめ
    C++11には多くの特性が導入されており、目の前が明るくなることが多い.多くの特性は一度閲覧すると明らかになるが,右値参照に関連し,特に完璧な転送は相対的に回り道であり,整理しにくい.右の値の参照には2つのアプリケーションがあり、最も基本的な動機は意味を移動することであり、同時に完璧な転送のサポートにきっかけをもたらす.