完全転送

今日、私たちは「 ... これまで C++ で未解決だった問題」を解決しました (Bjarne Stroustrup)。長い話を短くするために、完全転送について書きます。

しかし、完全転送とは?

関数テンプレートが左辺値または右辺値の特性を変更せずに引数を転送する場合、完全転送と呼びます。

偉大な。しかし、左辺値と右辺値とは何ですか?さて、少し寄り道をしなければなりません。

左辺値と右辺値

左辺値と右辺値の詳細については説明せず、したがって glvalues を紹介します。 x値 、および prvalues。 それは必要ありません。興味がある場合は、Anthony Williams の投稿:Core C++ - lvalues and rvalues をお読みください。私は自分の投稿で持続可能な直感を提供します.

右辺値

  • 一時オブジェクト。
  • 名前のないオブジェクト
  • アドレスのないオブジェクト

特性の 1 つがオブジェクトに当てはまる場合、それは右辺値になります。逆に言えば、左辺値には名前とアドレスがあることを意味します。右辺値の例:

int five= 5;
std::string a= std::string("Rvalue");
std::string b= std::string("R") + std::string("value");
std::string c= a + b;
std::string d= std::move(b);

右辺値は割り当ての右側にあります。値 5 とコンストラクター呼び出しは std::string("Rvalue") 右辺値です。これは、値 5 のアドレスを特定することも、作成された文字列オブジェクトに名前を付けることもできないためです。式 std::string("R") + std::string("value") での右辺値の加算についても同じことが当てはまります。

2 つの文字列 a + b の追加は興味深いものです。どちらの文字列も左辺値ですが、追加により一時オブジェクトが作成されます。特別な使用例は std::move(b) です。新しい C++11 関数は、左辺値 b を右辺値参照に変換します。

右辺値は割り当ての右側にあります。左辺値は代入の左側に置くことができます。しかし、それは常に正しいとは限りません:

const int five= 5;
five= 6;

ただし、変数 5 は左辺値です。ただし、5 は定数であり、代入の左辺では使用できません。

しかし、この投稿の課題は次のとおりです。完全な転送.未解決の問題の直感をつかむために、いくつかの完璧な問題を作成します ファクトリ メソッド。

完全なファクトリー メソッド

最初に、短い免責事項。完全なファクトリ メソッドという表現は、正式な用語ではありません。

完璧な工場方式 私にとっては完全に一般的なファクトリーメソッドです。特に、これは関数が次の特性を持つ必要があることを意味します:

  • 任意の数の引数を取ることができます
  • 左辺値と右辺値を引数として受け入れることができます
  • 基礎となるコンストラクターと同じ引数を転送します

あまりフォーマルではないと言いたいです。完全なファクトリ メソッドは、任意のオブジェクトをそれぞれ作成できる必要があります。

最初の反復から始めましょう。

最初の反復

効率上の理由から、関数テンプレートは参照によって引数を取る必要があります。正確に言うと。非定数の左辺値参照として。最初の反復で作成した関数テンプレートを次に示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// perfectForwarding1.cpp

#include <iostream>

template <typename T,typename Arg>
T create(Arg& a){
 return T(a);
}


int main(){
 
 std::cout << std::endl;

 // Lvalues
 int five=5;
 int myFive= create<int>(five);
 std::cout << "myFive: " << myFive << std::endl;

 // Rvalues
 int myFive2= create<int>(5);
 std::cout << "myFive2: " << myFive2 << std::endl;

 std::cout << std::endl;

}

プログラムをコンパイルすると、コンパイラ エラーが発生します。その理由は、右辺値 (21 行目) を非定数の左辺値参照にバインドできないためです。

この問題を解決するには 2 つの方法があります。

<オール>
  • 非定数の左辺値参照を変更する (6 行目) 定数左辺値参照。 右辺値を定数左辺値参照にバインドできます。しかし、関数の引数は定数であり、変更できないため、これは完璧ではありません。
  • 定数左辺値参照の関数テンプレートをオーバーロードする 非 const 左辺値参照。 それは簡単だ。それが正しい道です。
  • 2 回目の繰り返し

    これは、定数の左辺値参照と非定数の左辺値参照に対してオーバーロードされたファクトリ メソッド create です。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // perfectForwarding2.cpp
    
    #include <iostream>
    
    template <typename T,typename Arg>
    T create(Arg& a){
     return T(a);
    }
    
    template <typename T,typename Arg>
    T create(const Arg& a){
     return T(a);
    }
    
    int main(){
     
     std::cout << std::endl;
    
     // Lvalues
     int five=5;
     int myFive= create<int>(five);
     std::cout << "myFive: " << myFive << std::endl;
    
     // Rvalues
     int myFive2= create<int>(5);
     std::cout << "myFive2: " << myFive2 << std::endl;
    
     std::cout << std::endl;
    
    }
    

    プログラムは期待される結果を生成します。

    それは簡単でした。簡単すぎる。このソリューションには、2 つの概念上の問題があります。

    <オール>
  • n 個の異なる引数をサポートするには、関数テンプレート create の 2^n +1 バリエーションをオーバーロードする必要があります。引数なしの関数 create は完全なファクトリ メソッドの一部であるため、2^n +1 です。
  • 関数の引数は名前を持っているため、作成の関数本体で左辺値に変更されます。これは問題ですか?もちろんはい。 a はもう移動できません。したがって、安価な移動の代わりに高価なコピーを実行する必要があります。しかし、さらに悪いことは何ですか。 T のコンストラクター (12 行目) が右辺値を必要とする場合、それは機能しなくなります。
  • これで、C++ 関数 std::forward の形で解決策が得られました。

    3 回目の反復

    std::forward を使用すると、ソリューションは有望に見えます。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // perfectForwarding3.cpp
    
    #include <iostream>
    
    template <typename T,typename Arg>
    T create(Arg&& a){
     return T(std::forward<Arg>(a));
    }
    
    int main(){
     
     std::cout << std::endl;
    
     // Lvalues
     int five=5;
     int myFive= create<int>(five);
     std::cout << "myFive: " << myFive << std::endl;
    
     // Rvalues
     int myFive2= create<int>(5);
     std::cout << "myFive2: " << myFive2 << std::endl;
    
     std::cout << std::endl;
    
    }
    

    cppreference.com から完全な転送を得るためのレシピを紹介する前に、ユニバーサル リファレンスという名前を紹介します。

    名前ユニバーサル リファレンス Scott Meyers によって造られました。

    7 行目のユニバーサル参照 (Arg&&a) は、左辺値または右辺値をバインドできる強力な参照です。派生型 A に対して変数 Arg&&a を宣言すると、自由に使用できます。

    完全な転送を実現するには、ユニバーサル参照を std::forward と組み合わせる必要があります。 a はユニバーサル参照であるため、 std::forward(a) は基になる型を返します。したがって、右辺値は右辺値のままです。

    それではパターンへ

    template<class T>
    void wrapper(T&& a){
     func(std::forward<T>(a)); 
    }
     

    パターンの重要な部分を強調するために赤色を使用しました。関数テンプレートの作成でまさにこのパターンを使用しました。型の名前のみが T から Arg に変更されました。

    関数テンプレートの作成は完璧ですか?申し訳ありませんが、今。 create には、オブジェクトのコンストラクターに完全に転送される引数が 1 つだけ必要です (7 行目)。最後のステップは、関数テンプレートから可変個引数テンプレートを作成することです。

    4 回目の反復 - 完全なファクトリ メソッド

    Variadic Templates は、任意の数の引数を取得できるテンプレートです。これはまさに、完全なファクトリ メソッドに欠けている機能です。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    // perfectForwarding4.cpp
    
    #include <iostream>
    #include <string>
    #include <utility>
    
    template <typename T, typename ... Args>
    T create(Args&& ... args){
     return T(std::forward<Args>(args)...);
    }
    
    struct MyStruct{
     MyStruct(int i,double d,std::string s){}
    };
    
    int main(){
     
     std::cout << std::endl;
    
     // Lvalues
     int five=5;
     int myFive= create<int>(five);
     std::cout << "myFive: " << myFive << std::endl;
    
     std::string str{"Lvalue"};
     std::string str2= create<std::string>(str);
     std::cout << "str2: " << str2 << std::endl;
    
     // Rvalues
     int myFive2= create<int>(5);
     std::cout << "myFive2: " << myFive2 << std::endl;
    
     std::string str3= create<std::string>(std::string("Rvalue"));
     std::cout << "str3: " << str3 << std::endl;
    
     std::string str4= create<std::string>(std::move(str3));
     std::cout << "str4: " << str4 << std::endl;
     
     // Arbitrary number of arguments
     double doub= create<double>();
     std::cout << "doub: " << doub << std::endl;
     
     MyStruct myStr= create<MyStruct>(2011,3.14,str4);
    
    
     std::cout << std::endl;
    
    }
    

    7 ~ 9 行目の 3 つのドットは、いわゆるパラメーター パックです。 3 つのドット (楕円とも呼ばれます) が Args の左側にある場合、パラメーター パックはパックされます。正しい場合、パラメーター パックはアンパックされます。特に、9 行目の std std::forward(args)... の 3 つのドットにより、各コンストラクター呼び出しで完全な転送が実行されます。結果は印象的です。これで、完全なファクトリ メソッドを引数なし (40 行目) または 3 つの引数あり (43 行目) で呼び出すことができます。

    次は?

    RAII (Resource Acquisition Is Initialization の略) は、C++ の非常に重要なイディオムです。なんで?次の投稿で読んでください。