現代C++の理解テンプレートタイプ推定(template type deduction)

7867 ワード

テンプレートタイプ推定の理解(template type deduction)
複雑なシステムがどのように動作しているのか理解できないことが多いが、このシステムが何ができるか知っている.C++のテンプレートタイプ推定は,パラメータをテンプレート関数に渡すことでプログラマに満足のいく結果をもたらすことが多いが,その推定過程をより明確に記述することはできない.テンプレートタイプ推定は,現代C++で広く用いられているキーワードautoの基礎である.autoコンテキストでテンプレートタイプ推定を使用する場合、テンプレートに適用されるほど直感的ではないので、テンプレートタイプ推定がautoでどのように動作するかを理解することが重要です.
以下、詳細に説明する.次の偽コードを見てください.
template
void f(ParamType param);

次のコードで呼び出します.
f(expr); //call  f with some expression

コンパイル中にコンパイラはexprを使用して2つのタイプを推定します.1つはTのタイプで、1つはParamTypeです.この2つのタイプは、ParamTypeには通常constや参照などの修飾子が含まれているため、異なることが多い.テンプレートが次のように宣言された場合:
template
void f(const T& param);//ParamType is const T&

次のコードで呼び出します.
int x = 0;
f(x); //call f with an int

Tはintと推定されますが、ParamTypeはconst int&と推定されます.
Tの推定タイプと関数に伝達されるパラメータタイプは同じであると自然に考えられるが,上記の例ではパラメータxのタイプがint,Tもintタイプと推定される.しかし、往々にして状況はそうではない.Tのタイプ推定はパラメータexprのタイプだけでなくParamTypeの形式にも依存する.
3つのケースがあります.
  • ParamTypeはポインタまたは参照タイプですが、universal referenceではありません(このタイプは後述しますが、このタイプは左値参照と右値参照とは異なることを理解する必要があります).
  • ParamTypeはuniversal referenceです.
  • ParamTypeは、ポインタでも参照でもありません.

  • 次に、各例を例に挙げます.各例は、次のテンプレート宣言と関数呼び出しの擬似コードから進化します.
    template
    void f(ParamType param);
    f(expr);

    ParamTypeはポインタまたは参照タイプです
    この場合のタイプ推定は、次のようになります.
  • exprのタイプが参照である場合、参照セクションは無視されます.
  • は、次いでexprのタイプをParamTypeとパターンマッチングしてTを最終的に決定する.

  • 次の例を見てください.
    template 
    void f(T &param);

    次の変数を宣言します.
    int x = 27; //x  int
    const int cx = x;//cx const int
    const int& rx = x;//rx   const int   

    paramとTの推定は以下の通りである.
    f(x); //T    int,param        int &
    f(cx);//T    const int,param       const int &
    f(rx);//T    const int(        ),param       const int &

    2番目と3番目の関数呼び出しでは、cxとrxがconst値を渡すため、Tはconst intと推定され、生成されるパラメータタイプはconst int&であり、1つの参照パラメータにconstオブジェクトを渡すと、この値が変更されることを望んでいないため、パラメータはconstへの参照と推定されるべきである.テンプレートタイプ推定もそうですが、推定タイプTの場合constはタイプの一部になります.
    第3の例では、rxのタイプは参照タイプであるが、Tは非参照タイプと推定される.タイプ推定中にrxの参照タイプが無視されるためです.
    上記の例では、左参照パラメータについて説明しただけで、右参照パラメータについても同様に試用します.
    関数fのパラメータタイプをcont&に変更すると、実パラメータcxとrxのconst属性は変わらないに違いありませんが、パラメータ宣言をconstへの参照として宣言するので、constをTの一部と推定する必要はありません.
    template 
    void f(const T &param);

    宣言された変数は変更されません:
    int x = 27; //  
    const int cx = x;//  
    const int& rx = x;//  

    paramとTの推定は以下の通りである.
    f(x); //T    int,param       const int &
    f(cx);//T    int,param       const int &
    f(rx);//T    int(       ) ,param       const int &

    paramがポインタまたはconstを指すポインタである場合、本質的には参照の推定プロセスと同じである.
    ポインタと参照はテンプレートパラメータとして推定中の結果として明らかであり,以下の例ではいくつか暗黙的である.
    ParamTypeはUniversal Referenceです
    このタイプのパラメータは、宣言時の形式で右値参照と類似しています(関数テンプレートのタイプパラメータがTの場合、Universal Referenceとして宣言してTT&&&&&)が、渡された実パラメータが左の値であれば、結果は右値参照とは異なります(後で説明します).
    Universal Referenceのテンプレートタイプ推定は、次のようになります.
  • exprが左である場合、TおよびParamTypeはいずれも左参照と推定される.ちょっと不思議ですが、まず、これはテンプレートタイプ推定でTを参照と推定する唯一の場合です.次に,ParamTypeの宣言は右値参照構文を用いるが,最終的には左値参照と推定される.
  • exprが右の場合は、前節(ParamTypeはポインタまたは参照タイプ)を参照してください.
  • 例を挙げます.
    template 
    void f(T &&param);
    
    int x = 27; //  
    const int cx = x;//  
    const int& rx = x;//  

    paramとTの推定は以下の通りである.
    f(x); //x   ,  T int&,ParamType  int&
    f(cx);//cx   ,  T const int&,ParamType  const int&
    f(rx);//rx   ,  T const int&,ParamType  const int&
    f(27);//27   ,T int ,ParamType int&&

    ここでのポイントは,テンプレートパラメータがUniversal Referenceタイプの場合,左値と右値の推定が異なることである.このことはテンプレートパラメータが非Universal Referenceタイプの場合には起こりません.
    ParamTypeはポインタでもリファレンスでもない
    この場合、いわゆる値別伝達です.
    template 
    void f(T param);//    

    関数fに渡される実パラメータ値は、元のオブジェクトのコピーとなる.これはexprからTを推定する方法を決定した.
  • も同様であり、exprのタイプが参照である場合、参照部分は無視される.
  • exprがconstである場合、同様に無視されます.volatileの場合も無視します.

  • 例を見てみましょう.
    int x = 27; //  
    const int cx = x;//  
    const int& rx = x;//  

    paramとTの推定は以下の通りである.
    f(x); // T int ParamType  int
    f(cx);//  
    f(rx);//  

    cxとrxがconstであってもparamはconstではないことがわかる.paramはcxとrxのコピーにすぎないため、paramのタイプにかかわらず元の値に影響を与えません.exprを変更できないということは、exprのコピーを変更できないという意味ではありません.
    注意paramがby-valueの場合、constまたはvolatileは無視されます.前の例では、パラメータタイプがconstを指す参照またはポインタである場合、タイプ推定中にexprのconst属性が保持されることを説明した.しかし、次の場合、exprがconstオブジェクトを指すconstポインタであり、paramのタイプがby-valueであれば、結果はどうなるのでしょうか.
    template 
    void f(T param);//    
    
    const char * const ptr = "Fun with pointers";
    f(ptr);

    まずconstポインタを思い出します.アスタリスクの左側のconst(ポインタに最も近い)はポインタがconstであり、ポインタの指向を変更することはできません.アスタリスクの右側のconstはポインタが指す文字列がconstであり、文字列の内容を変更することはできません.ptrがfに伝達されると、ポインタ自体が値で伝達される.by-valueパラメータのタイプ推定ではconst属性は無視されるため、ポインタのconst、すなわち星番号の右側のconstは無視され、最後に推定されるパラメータタイプはconst char*ptr、すなわちポインタの指向を修正することができ、ポインタの指す内容を修正することはできない.
    配列パラメータ
    上記の3つのケースはテンプレートタイプ推定の大部分をカバーしているが,もう1つのケースは配列と言わざるを得ない.配列とポインタは互換性があるように見えますが、このような幻覚をもたらす主な原因の一つは、多くの場合、配列が最初の配列要素を指すポインタに劣化することができ、この劣化の下のコードこそがコンパイルされることができます.
    const char name[]="HarlanC";//name    const char[8]
    const char*ptrToName = name;//       

    ポインタと配列のタイプは異なるが,配列がポインタに劣化する規則のため,上のコードはコンパイル可能である.
    配列をby-valueパラメータ付きテンプレートに渡すと、何が起こるのでしょうか.
    template 
    void f(T param);//    
    f(name);

    配列を関数パラメータとする構文は合法的です.
    void myFunc(int param[]);

    しかし、ここでの配列パラメータはポインタパラメータとして処理されます.つまり、次の宣言は上記の宣言と等価です.
    void myFunc(int* param); // same function as above

    配列パラメータはポインタパラメータとして扱われるため、値で渡されるテンプレート関数に1つの配列を渡すとポインタタイプと推定されます.テンプレート関数fが呼び出されると、タイプパラメータTはconst char*と推定される.
    f(name); // name is array, but T deduced as const char*

    関数は真の配列パラメータを宣言することはできませんが(このように宣言してもポインタとして処理されます)、パラメータを配列への参照として宣言できます.テンプレート関数を次のように変更します.
    template 
    void f(T& param);//     

    配列の実パラメータを渡します.
    f(name);

    このときTは真の配列タイプと推定される.このタイプは配列の大きさも同時に含み、上記の例ではTはconst charと推定される[8].fのパラメータタイプはconst char(&)[8].
    この声明を使うのは役に立つ.配列に含まれる要素の数を推定するテンプレートを作成できます.
    //           ,
    //               
    //              
    template 
    constexpr std::size_t arraySize(T (&)[N]) noexcept 
    { 
        return N; 
    } 

    関数戻り値をconstexprタイプとして宣言することは、この値がコンパイル期間中に得られることを意味する.これにより、コンパイル中に配列のサイズを取得し、同じサイズの配列を宣言できます.
    int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
    int mappedVals[arraySize(keyVals)];

    std::arrayはあなたが現代のC++プログラマーであることをより体現することができます.
    std::array mappedVals;

    関数パラメータ
    配列はポインタに劣化できる唯一のタイプではありません.関数タイプもポインタに劣化することができ、我々が議論した配列に関するタイプ推定プロセスは関数にも適用される.
    void someFunc(int, double); // someFunc     ,   void(int, double)
    
    template
    void f1(T param); //passed by value
    template
    void f2(T& param); // passed by ref
    f1(someFunc); // param      ptr-to-func void (*)(int, double)
    f2(someFunc); // param     ref-to-func void (&)(int, double)

    要点まとめ
  • テンプレートタイプ推定では、参照は非引用として処理されます.すなわち、パラメータの参照属性は無視されます.
  • テンプレートパラメータタイプがuniversal referenceの場合、タイプ推定を行うと左値が特殊に処理されます.
  • テンプレートタイプパラメータがby-valueの場合、constまたはvolatileは非constまたは非volatileとして処理される.
  • テンプレートタイプパラメータがby-valueの場合、関数または配列にパラメータを入れるとポインタに劣化します.