C++ コア ガイドライン:可変個引数テンプレートのルール

可変個引数テンプレートは C++ の典型的な機能です。ユーザーの観点からは使いやすいですが、実装者の観点からは非常に恐ろしく見えます。今日の投稿は、主に実装者の視点に関するものです。

ヴァリアディック寺院の詳細について書く前に、この記事の紹介について簡単に述べたいと思います。私が C++ を教えるときは、2 つの頭を抱えていることがよくあります。1 つはユーザーに対するもので、もう 1 つは実装者に対するものです。テンプレートなどの機能は使いやすいですが、実装するのは困難です。この重大なギャップは通常、C++ にあり、Python、Java、さらには C などの他の主流のプログラミング言語よりも深いと思います。正直なところ、このギャップに問題はありません。私はこれをギャップ抽象化と呼んでいますが、これは C++ の力の本質的な部分です。ライブラリまたはフレームワークの実装者の技術は、使いやすく (誤用しにくい) 安定したインターフェイスを提供することです。要点がわからない場合は、std::make_unique を開発する次のセクションを待ってください。

今日の投稿は 3 つのルールに基づいています:

  • T.100:さまざまな型の可変数の引数を取る関数が必要な場合は、可変個引数テンプレートを使用してください
  • T.101:???可変引数テンプレートに引数を渡す方法 ???
  • T.102:???可変個引数テンプレートへの引数を処理する方法 ???

あなたはすでにそれを推測することができます. 3 つのルールはタイトルのみです。したがって、最初の 3 つのルールから 1 つのストーリーを作成します。

お約束通り、std::make_unique を開発したいと思います。 std::make_unique は、std::unique_ptr によって保護された、動的に割り当てられたオブジェクトを返す関数テンプレートです。いくつかのユースケースをお見せしましょう.

// makeUnique.cpp

#include <memory>

struct MyType{
 MyType(int, double, bool){};
};

int main(){
 
 int lvalue{2020};
 
 std::unique_ptr<int> uniqZero = std::make_unique<int>(); // (1)
 auto uniqEleven = std::make_unique<int>(2011); // (2)
 auto uniqTwenty = std::make_unique<int>(lvalue); // (3)
 auto uniqType = std::make_unique<MyType>(lvalue, 3.14, true); // (4)
 
}

このユースケースに基づいて、std::make_unique の要件は何ですか?

<オール>
  • 任意の数の引数を処理する必要があります。 std::make_unique 呼び出しは、0、1、および 3 つの引数を取得します。
  • 左辺値と右辺値を処理する必要があります。行 (2) の std::make_unique 呼び出しは右辺値を取得し、行 (3) では左辺値を取得します。最後のものは、右辺値と左辺値も取得します。
  • 引数を変更せずに、基礎となるコンストラクターに転送する必要があります。つまり、std::make_unique が左辺値/右辺値を取得する場合、std::unique_ptr のコンストラクターは左辺値/右辺値を取得する必要があります。
  • この要件は通常、std::make_unique、std::make_shared、std::make_tuple などのファクトリ関数だけでなく、std::thread にも当てはまります。どちらも C++11 の 2 つの強力な機能に依存しています:

    <オール>
  • 可変個引数テンプレート
  • 完璧な転送
  • ここで、ファクトリ関数 createT を作成します。完全転送から始めましょう。

    完全転送

    まず、完全転送とは

    • 完璧な転送 引数の値カテゴリ (左辺値/右辺値) と const を保持できます /volatile 修飾語。

    完全転送は、ユニバーサル参照と std::forward で構成される典型的なパターンに従います。

    template<typename T> // (1)
    void create(T&& t){ // (2)
     std::forward<T>(t); // (3)
    }
    

    完全な転送を行うためのパターンの 3 つの部分は次のとおりです。

    <オール>
  • テンプレート パラメータ T:typename T が必要です
  • 完全転送参照とも呼ばれるユニバーサル参照で T をバインドします:T&&t
  • 引数で std::forward を呼び出します:std::forward(t)
  • 重要な観察事項は、T&&(2 行目) が左辺値または右辺値をバインドできること、および std::forward (3 行目) が完全な転送を行うことです。

    makeUnique.cpp など、最後に動作する createT ファクトリ関数のプロトタイプを作成します。 std::make_unique を createT 呼び出しに置き換え、createT ファクトリ関数を追加し、(1) と (4) の行をコメントアウトしました。さらに、ヘッダー (std::make_unique) を削除し、ヘッダー (std::foward) を追加しました。

    // createT1.cpp
    
    #include <utility>
    
    struct MyType{
     MyType(int, double, bool){};
    };
    
    template <typename T, typename Arg>
    T createT(Arg&& arg){
     return T(std::forward<Arg>(arg));
    }
     
    int main(){
     
     int lvalue{2020};
     
     //std::unique_ptr<int> uniqZero = std::make_unique<int>(); // (1)
     auto uniqEleven = createT<int>(2011); // (2)
     auto uniqTwenty = createT<int>(lvalue); // (3)
     //auto uniqType = std::make_unique<MyType>(lvalue, 3.14, true); // (4)
     
    }
    

    罰金。右辺値 (2 行目) と左辺値 (3 行目) がテストに合格しました。

    Variadic テンプレート

    ドットが重要な場合もあります。正確に 9 つの点を正しい場所に配置すると、行 (1) と行 (4) が機能します。

    // createT2.cpp
    
    #include <utility>
    
    struct MyType{
     MyType(int, double, bool){};
    };
    
    template <typename T, typename ... Args>
    T createT(Args&& ... args){
     return T(std::forward<Args>(args) ... );
    }
     
    int main(){
     
     int lvalue{2020};
     
     int uniqZero = createT<int>(); // (1)
     auto uniqEleven = createT<int>(2011); // (2)
     auto uniqTwenty = createT<int>(lvalue); // (3)
     auto uniqType = createT<MyType>(lvalue, 3.14, true); // (4)
     
    }
    

    魔法はどのように機能しますか? 3 つの点は楕円を表します。それらを使用すると、Args、または args がパラメーター パックになります。より正確には、Args はテンプレート パラメーター パックであり、args は関数パラメーター パックです。パラメーター パックに適用できる操作は 2 つだけです。それは、パックまたはアンパックです。楕円が Args の左側にある場合、パラメーター パックはパックされます。楕円が Args の右側にある場合、パラメーター パックはアンパックされます。式 (std::forward(args)...) の場合、これは、パラメーター パックが消費されるまで式がアンパックされ、アンパックされたコンポーネントの間にカンマが配置されることを意味します。これですべてです。

    CppInsight は、カーテンの下を見るのに役立ちます。

    これで、ほぼ完了です。これが私の createT ファクトリ関数です。

    template <typename T, typename ... Args>
    T createT(Args&& ... args){
     return T(std::forward<Args>(args) ... );
    }
    

    欠けている 2 つのステップは次のとおりです。

    <オール>
  • プレーンな T の代わりに std::unique_ptr を作成します
  • 関数の名前を make_unique に変更します。
  • 終わりました。

    std::make_unique

    template <typename T, typename ... Args>
    std::unique_ptr<T> make_unique(Args&& ... args){
     return std::unique_ptr<T>(new T(std::forward<Args>(args) ... ));
    }
    

    私はあなたを怖がらせるのを忘れていました。ここが私の投稿の恐ろしい部分です。

    printf

    もちろん、C 関数の printf は知っています。これはその署名です:int printf( const char* format, ... );. printf は、任意の数の引数を取得できる関数です。その能力はマクロ va_arg に基づいているため、タイプセーフではありません。

    可変個引数テンプレートのおかげで、printf はタイプセーフな方法で書き直すことができます。

    // myPrintf.cpp
    
    #include <iostream>
     
    void myPrintf(const char* format){ // (3)
     std::cout << format;
    }
     
    template<typename T, typename ... Args>
    void myPrintf(const char* format, T value, Args ... args){ // (4)
     for ( ; *format != '\0'; format++ ) { // (5)
     if ( *format == '%' ) { // (6) 
     std::cout << value;
     myPrintf(format + 1, args ... ); // (7)
     return;
     }
     std::cout << *format; // (8)
     }
    }
     
    int main(){
     
     myPrintf("\n"); // (1)
     
     myPrintf("% world% %\n", "Hello", '!', 2011); // (2)
     
     myPrintf("\n"); 
     
    }
    

    コードはどのように機能しますか? myPrintf がフォーマット文字列のみで呼び出された場合 (1 行目)、(3 行目) が使用されます。 (2)行の場合、関数テンプレート(4行目)が適用されます。関数テンプレートは、書式記号が `\0` と等しくない限りループします (5 行目)。フォーマット記号が `\0` と等しくない場合、2 つの制御フローが可能です。最初に、フォーマットが '%' で始まる場合 (6 行目)、最初の引数値が表示され、myPrintf がもう一度呼び出されますが、今回は新しいフォーマット記号と引数が少ない (7 行目)。次に、フォーマット文字列が「%」で始まらない場合、フォーマット記号が表示されます (8 行目)。関数 myPrintf (3 行目) は、再帰呼び出しの終了条件です。

    プログラムの出力は期待どおりです。

    次は?

    可変個引数テンプレートに対するルールが 1 つ残されています。その後、ガイドラインはテンプレートのメタプログラミングに続きます。次回の投稿では、テンプレート メタプログラミングについてどの程度深く掘り下げるべきかわかりません。