C++テンプレートプログラミングの深層理解

\[本文の構成\] 序論 テンプレートのインスタンス化 暗黙的なインスタンス化 明示的なインスタンス化 テンプレートの特殊化 明示的特殊化 部分特殊化 関数オーバーロードと特殊化 型推論 暗黙的な型変換 対応する型変換 参照とconst 万能参照、参照の折りたたみ、完全転送 万能参照と右辺値参照 型特性 typename 関連型 型特性 イテレータの特性 特性抽出 - SFINAE特性 autoとdecltype auto decltype 後置戻り値型 カンマ演算子と型制限 ラムダ式 可変引数テンプレート パラメータパック展開 関数テンプレート:関数展開を使用 関数テンプレート:初期化リスト展開を使用 関数テンプレート:再帰展開を使用 クラステンプレート:継承による再帰展開 テンプレート関数のオーバーロード解決 オーバーロードマッチングルール ポインタ リストと配列 クラスタイプ 参照とconst 万能参照のオーバーロード サブ関数のオーバーロードマッチング その他の問題 テンプレートと継承 テンプレートとフレンド関数 暗黙的な変換のサポート 関数コールバック

序論

C++は高性能開発環境で広く使用されるプログラミング言語であり、低レベルへの細かな制御を提供するだけでなく、工学開発に必要な機能を十分に備えています。その中の一つがテンプレート機能であり、その意義は以下の通りです:

① 泛用プログラミングの基礎。C++標準ライブラリの汎用コンテナやその他のサードパーティ製汎用ライブラリは、主にC++テンプレートに依存しています。汎用性とは、コードを書く際にオブジェクトの具体的な型を無視し、任意の型であると考え、使用時に型を決定することを意味します。 泛用的な観点からテンプレートを理解すると、一度だけプログラミングすることですべての型に対応できることがわかります。この機能のみであればテンプレートはそれほど複雑ではありませんが、複雑さは以下のような要素によって生まれます。一つは特定の型に対して特別な処理を行う必要があること、もう一つはコンパイラがテンプレートを翻訳する際に起こる問題、例えばテンプレートパラメータが何の型として推論されるか、複数の特殊化が存在する場合にテンプレート型をどのようにマッチさせるかなどです。

② コードの再利用と生成。テンプレートは本質的にコンパイル時にコードを生成するものであり、より高度なマクロ定義と見なすことができます。 コード生成の観点からテンプレートを理解すると、高級言語の機能、例えばリフレクションを実現したり、高級言語のように使いやすいラッパーインターフェースを実装したりできます。さらに、コンパイル時の計算を完了するためにコード生成を利用できます。

* この記事を読むには、右辺値の概念とテンプレートの基本概念についてある程度理解している必要があります。

テンプレートのインスタンス化

テンプレートは純粋なコンパイル時機能であり、実行時動的特性を持たず、テンプレートの生成はテンプレートを使用する際にコンパイラがテンプレート定義に基づいて対応する型のインスタンスを生成します。この特性により以下の状況が発生します:

① 実際に使用されたテンプレートパラメータのみがインスタンス化されます。つまり、あるテンプレートパラメータが一度も呼び出されなければ、その対応するインスタンスは生成されません。この規則はテンプレートクラスの関数にも適用され、テンプレートクラスの関数は部分的にインスタンス化される可能性があります。

② テンプレートは実行時動的推論をサポートしません。最も簡単な例として、非型テンプレートパラメータとして配列サイズを指定するテンプレート配列は静的配列しかできません:

template <typename T, int SIZE> 
class Array { 
    T arr[SIZE];
};


Array<int, 5> arr0; // OK
Array<int, n> arr1; // エラー

③ テンプレートの宣言と定義はヘッダファイルに同時に記述する必要があり、そうでないとリンクエラーが発生する可能性があります。なぜなら、テンプレート関数定義の実装を記述した時点で、コンパイラはテンプレート関数の動作を記述しているだけで、実際にインスタンス化は行われていません。テンプレート関数を実際に呼び出したときにのみ、対応するインスタンス化結果が生成されます。もしテンプレート関数定義をcppファイルに記述した場合、インスタンス化時には関数テンプレート定義を見つけられません。

暗黙的なインスタンス化

テンプレートの宣言と定義を分離してコンパイルできるかどうかを議論するとき、テンプレートは実際に呼び出されたときにインスタンス化されるという事実に基づいています。これは暗黙的なインスタンス化と呼ばれます:

min(1.0f, 4.0f); // min<float>(float, float)のインスタンスを生成
明示的なインスタンス化

当然ながら、直接インスタンス化する型を指定することも可能です。これは明示的なインスタンス化と呼ばれます。これは頻繁に使用する特定のインスタンス化型が必要な場合に、ヘッダファイルで明示的にインスタンス化を行う場合に一般的に使用されます。例えば:

using ObjType = Array<Object, ObjectAllocator>;

これにより、異なるファイルからObjTypeクラスにアクセスでき、それが常に存在することを保証できます。 明示的なインスタンス化は、上記のような宣言のみで、追加の定義は必要ありません。定義はテンプレートの内容に基づいてコンパイラによって生成されます。 限られた数のテンプレートインスタンスのみを参照する必要がある場合は、明示的なインスタンス化を利用して分離コンパイルを実現できます。しかし、これは少数のシナリオでのみ効果的です。

テンプレートの特殊化/具象化

テンプレートの特殊化/具象化(specialization)とは、特殊な型のインスタンス化に対してテンプレートを変更する必要があることを意味します。これには、明示的な特殊化(完全特殊化)と部分的特殊化(偏特殊化)の二つの変更方法が含まれます。

明示的な特殊化

特殊な型の場合、テンプレートの実装は適切ではなく、このようなケースに対して特別な処理が必要になります。この場合、明示的な特殊化(explicit specialization)、または完全特殊化と呼ばれるものを使用できます。

例えば、const char* 文字列型に対して特別な処理が必要です:

template<>
bool Compare<const char*>(const char* a, const char* b)
{
    //...
}

またはクラステンプレートに対して処理を行う場合:

template<typename T1, typename T2>
class A
{
    T1 a;
    T2 b;
};

template<>
class A<float, int>
{
    float a;
    int b;
};

明示的な特殊化の特徴は、テンプレート宣言時に角括弧内の内容が空であることです。

部分的特殊化

部分的なインスタンスに対して特殊なテンプレート変更を行うこともでき、これは部分的特殊化(partial specialization)または偏特殊化と呼ばれます。

template<typename T1>
class A<T1, int>
{
    T1 a;
    int b;
};

部分的特殊化を実装する際には、角括弧内で特定化されたパラメータのみを省略し、特定化されていないパラメータは角括弧内に保持されます。

部分的特殊化はクラステンプレートにのみ適用でき、関数テンプレートには適用できません。

関数オーバーロードと特殊化

特殊化は、特定の型のテンプレート関数インスタンス化を生成する際に、コンパイラに何らかの特別な操作を行うように指示するものです。これは関数オーバーロードとは異なり、選択可能な同名関数オーバーロードを生成しないためです。

型推論

隠しインスタンス化を使用する際、コンパイラは一致する型を推論します。正確な型を提供した場合、型推論は通常問題ありませんが、曖昧な型を提供した場合どうでしょうか?

暗黙的な型変換

普通の関数を呼び出し、一致しない型を提供した場合、関数は暗黙的な変換を通じて型変換を完了することができます。例えば:

double min(double a, double b)
{
    return a < b ? a : b;
}

float x = min(1.0, 2);

この場合、2はint型であり、double型へと暗黙的に変換できます。呼び出しは成立します。

一方テンプレートでは、異なる型に対しては新しい型インスタンスを生成する傾向があり、既存の型インスタンスを介した暗黙的な変換は行いません。例えば、前述の例と同じく、min関数をテンプレート関数として定義した場合:

template<typename T>
T min(T a, T b)
{
    return a < b ? a : b;
}

int x = min(1.0, 2.0); // min<float>
int y = min(1, 2); // min<in>
int z = min(1.0, 2); // エラー

min(1.0, 2.0)を最初に呼び出すと、minのインスタンスが生成され、その後min(1, 2)を呼び出すと、int型の新しいインスタンスminが生成され、int型をdouble型に暗黙的に変換してminのインスタンスを呼び出すことはしません。

min(1,2)がminのインスタンスを使用するようにしたい場合、明示的なインスタンス化を使用できます:

int y = min<double>(1, 2);

入力が一致しない場合、例えばmin(1.0, 2)の場合、テンプレート呼び出しはデフォルトでは暗黙的な変換を行わず、コンパイラはminまたはminのインスタンスを生成するか判断できないため、コンパイルエラーが発生します。

対応する型変換

特に、テンプレートは以下の限られた型変換のみをサポートします:

const/reference

上記のテンプレート関数を例にします。constと非const型を混在させた入力を提供した場合:

const int a = 1;
int b = 2;
int x = min(a, b); // min<int>();

上記の例はコンパイル可能であり、テンプレートは値渡し時にトップレベルのconstを無視するためです。例えば、両方の引数がconst int型の場合、テンプレートは常にintに翻訳されます。

関数/配列

関数または配列を渡すと、関数ポインタまたは配列ポインタにデグレードされ、型変換が発生します:

template<typename T>
void func(T param);

void test(int param);

int arr[2] = { 1, 2 };
func(arr); // int*

func(test); // void(*)(int)

一般的に、テンプレート推論はトップレベルのconst/reference、関数および配列を無視します。ただし、参照を使ってアクセスした場合は型変換は発生しません:

int arr[2] = { 1, 2 };
auto& arr1 = arr; // int(&)[2]

const int ci = 1;
auto& cr = ci;  // const int&

この設計は理解できます。例えば、配列の場合、異なるサイズの配列に対してそれぞれ対応するサイズのインスタンスを生成すると、コードの膨張が発生します。また、サイズが一致しない場合は型として認識されません。

参照とconst

テンプレートの型推論を考える際には、変数の修飾属性、つまりconstおよび左右値属性も考慮する必要があります。テンプレートを宣言する際にconstや&/&&の修飾子を追加した場合、どのような結果になるのでしょうか?以下のようにいくつかのケースについて説明します。

値型
template<typename T>
void func1(T);

前述のように、非参照のテンプレートパラメータについては、テンプレート推論はconstおよび参照属性を無視します。

int i = 1;
const int ci = 1;
int& ri = i;
func1(i); // int
func1(ci); // int
func1(ri); // int
左辺値参照
template<typename T>
void func2(T&);

関数パラメータに&の左辺値制限を追加した場合、右辺値を入力するとエラーになります。

int i = 1;
const int ci = 1; 
func2(i); // int
func2(ci); // const int
func2(1); // エラー

上記の例では、constが含まれる入力に対して、テンプレートはconstを翻訳します。これは、テンプレートパラメータに左右値制限がない場合と全く異なります。

定数左辺値参照
template<typename T>
void func3(const T&);

関数パラメータに&の左辺値制限とconstを追加した場合、理論的にはあらゆるパラメータを入力できます。また、パラメータ自体にconstが含まれているため、推論結果はconstを含まないものになります。これはconstが一時変数の寿命を延長できるためです。

int i = 1;
const int ci = 1; 
func3(i); // int
func3(ci); // int
func3(1); // int
右辺値参照
template<typename T>
void func4(T&&);

関数パラメータに&&の右辺値を追加した場合、右辺値を入力できますが、左辺値も入力できます。ただし、コンパイラは左辺値を左辺値参照型に推論します。

int i = 1;
const int ci = 1;
func4(i); // int&
func4(ci); // const int&
func4(1); // int

これは非常に特殊な参照であり、通常の非テンプレート関数の右辺値参照とは異なり、変数のconst/参照属性を保持します。例えば配列の場合、ポインタにデグレードされません:

template<typename T>
void TestType(T&& param)
{
    cout << typeid(T).name() << endl;
}

Test("abc"); // const char[3]
定数右辺値参照
template<typename T>
void func5(const T&&);

右辺値のみを受け入れます。

上記のケースでは、通常非参照バージョンを使用します。ツール関数として使用されるテンプレート関数の場合、const/参照属性は気にする必要はありません。そうした場合、const/参照の複数のインスタンス化は不要なコードの膨張となります。

右辺値/左辺値のバージョンを使用する際は、主に参照/constに関連するロジックを処理するためです。例えば、元のパラメータのconst/参照属性を保持したい場合、またはconst/参照属性を削除/追加したい場合、左右値変換をしたい場合などが考えられます。

標準ライブラリにはこれらの補助メソッドが提供されています:

remove_reference // 参照属性を削除
add_lvalue_reference // 左辺値参照を追加
add_rvalue_reference // 右辺値参照を追加

これらはオブジェクトではなく型に対して作用することに注意してください。例えば:

remove_reference<int&>::type; // -> int

C++標準ライブラリには、対応するメソッドも封入されています。例えば、std::declvalは右辺値参照の結果を返し、内部ではadd_rvalue_referenceを使用します:

template < class T >
typename std::add_rvalue_reference <T>::type declval ( ) noexcept;
万能参照、参照の折りたたみ、完全転送

上記のすべての参照入力の中で、右辺値参照が広く使われています:

template<typename T>
void func(T&&);

前述のように、右辺値参照はすべての型のパラメータを受け入れることができ、左辺値を入力すると左辺値参照に推論されます。つまり、以下のような結果を得ることができます:

void func(int& &&);

実際には、通常のプログラミング規則では & && のような構文は不正であり、右辺値参照入力のテンプレートでのみ許可されています。対応する規則は参照の折りたたみと呼ばれます:

T&& &  -> T&
T& &&  -> T&
T& &   -> T&
T&& && -> T&&

二重参照の場合、コンパイラはそれを単一の参照に折りたたみ、対応する翻訳規則は上記の通りです。

つまり、左辺値を入力すると左辺値(&& &->&)に翻訳され、右辺値を入力すると右辺値(&& && -> &&)に翻訳されます。そのため、「右辺値参照」のテンプレートはそれほど適切ではなく、字面通り右辺値のみを受け入れるわけではないためです。通常、これを「万能参照」(universal references)と呼びます。

一方、万能参照はパラメータの左右値/const属性をよく保持するため、転送に広く使用されます。パラメータの属性をよく保持するため、「完全転送」とも呼ばれます。

完全転送とは、関数が入力されたパラメータをそのまま別の関数に渡すことを意味します。

① 転送機能を持つ関数パラメータを万能参照テンプレート(&&)に変更すると、入力パラメータが左辺値か右辺値を正しく識別できます;

② 入力パラメータの左右値属性を識別しても、関数内部では実引数自体が左辺値として存在するため、転送時に実引数自体を呼び出すと左右値属性が失われます;

③ 上記問題を解決するために、C++標準ライブラリはforward関数を導入して完全転送の実装を支援します。つまり、転送コードは次のように記述する必要があります:

template<typename T>
void func(T&& param)
{
    otherfunc(std::forward<T>(param));
}

つまり、forwardと万能参照を組み合わせて完全転送を実現する必要があります。

標準ライブラリのforwardの実装は以下の通りです:

// 左辺値参照
template<typename T>
T&& forward(typename remove_reference<T>::type& t) noexcept {
    return static_cast<T&&>(t);
}

// 右辺値参照(非万能参照)
template<typename T>
T&& forward(typename remove_reference<T>::type&& t) noexcept {
    return static_cast<T&&>(t);
}

左辺値と右辺値の二つのオーバーロードを実装しており、左辺値を受け取るバージョンは左辺値を返し、右辺値を受け取るバージョンは右辺値を返し、戻り値の参照の折りたたみで実装されます。

次に、完全転送の応用例を見てみましょう。それはstd::vectorのemplace_back関数です。

struct Point
{
    float x = 0.0f;
    float y = 0.0f;
    Point() { }
    Point(int _x, int _y) : x(_x), y(_y) { }
};

vector<Point> v;
v.push_back(Point(1.0, 2.0));
v.emplace_back(1.0, 2.0);

通常、新しいデータを追加するにはstd::vectorのpush_back関数を呼び出します。上記の例では、Pointのコンストラクタ、コピー構造体、およびデストラクタを呼び出します。コピー構造体自体は移動セマンティクスで最適化できるものの、一時オブジェクトの作成を避けられません。

emplace_backを使用すると、パラメータをvector内部に直接構築するため、一時変数の媒体を必要とせず、C++ vectorのソースコードを参照すると、emplace_back内部は完全転送の構文で実装されていることがわかります。

template <class... _Valty>
decltype(auto) emplace_back(_Valty&&... _Val) {
    // ...
     _Ty& _Result = *_Emplace_reallocate(_Mylast, _STD forward<_Valty>(_Val)...);
    // ...
}

これにより、完全転送には以下の二つの重要な役割があります:

① パラメータの参照/const属性を保持

② 不要なコピーと一時オブジェクトを避ける

万能参照と右辺値参照

形如template<typename> func(T&&)の参照を万能参照と呼びます。なぜなら、ほぼすべての型のパラメータを受け入れることができるからです。しかし、&&のパラメータが必ずしも万能参照であるとは限りません。普通の右辺値のみを受け入れる右辺値参照かもしれません。

普通の関数の場合、右辺値参照です:

void func(int&& param); // 右辺値参照

次にC++ vectorライブラリを見てみましょう。このようにメソッドが提供されています:

void push_back(_Ty&& _Val) {
     emplace_back(_STD move(_Val));
}

これは右辺値入力のpush_back関数のオーバーロードです。ここでもテンプレートが使用されていますが、万能参照とは異なり、これはクラステンプレートの型であり、関数テンプレートの型ではありません。また、moveを使用していることにも注意してください。forwardではなく、これは入力パラメータが右辺値参照ではなく万能参照であるためです。

forwardの実装を見直してみましょう。左値と右値の二つのオーバーロードを提供しており、右値参照入力の定義は以下の通りです:

template<typename T>
T&& forward(typename remove_reference<T>::type&& t) noexcept {
    return static_cast<T&&>(t);
}

ここでremove_referenceが巧妙に参照を削除し、参照の折りたたみを回避し、入力は右辺値のみになります。

型特性

テンプレートの場合、テンプレートの型情報を取得する必要があることがよくあります。コンパイル時に型情報を取得する技術は型特性(type traits)と呼ばれます。

typename

型特性について議論する前に、テンプレート定義でよく見られるキーワードtypenameについて簡単に紹介します。以前にも何度もこのキーワードに遭遇しています。少なくとも以下の二つの用法があります:

① テンプレート型として

template<typename T> void func() { }
template<class T> void func() { }

この例では、typenameとclassの使用は差しません。

② オブジェクトが型であることを示す

前述では、型の参照/const属性を変更する一連の方法を紹介しました。例えばremove_reference<T>::typeは型を返します。

しかし、この表現には曖昧性があります。これはクラスの静的メンバー変数をアクセスする可能性もあります。コンパイラがこれが変数ではなく型であることを認識させるために、typenameを修飾する必要があります。forward関数の実装でtypename remove_reference<T>::typeを使用する理由です。

通常、usingまたはtypedefで上記の記述を簡略化します:

// (1)
template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;

// (2)
typedef typename Iterator::value_type value_type;
// 新しいバージョンではusingを使用

両者の違いはそれほど大きくありません。usingの方がC++プログラミングスタイルに合っており、STLではusingの使用がより広く使われています。

テンプレートパラメータに別名を適用する場合、次のように使用します:

template<typename = XXX>
関連型

テンプレートの場合、template<typename T>でTが対応する型を取得できます。しかし、型特性は本来、その型自体を取得するためのものではなく、現在の型と「関連する」型を取得するためのものです。

前述では、左右値属性を変更する方法を紹介し、これらの方法は型に作用することを示しました。例えば、何度も言及されたremove_referenceは型特性の一つの例です。forward関数の定義のように、remove_reference<T>::typeを通じて型Tの参照を削除した型を取得します。

上記の例を通じて、型特性の一つの応用方法を理解できます。それは、現在の型の関連型を取得することです。つまり、型Tがわかれば、remove_reference<T>::typeでTの参照を削除した型を取得できます。

型特性

ここでは型特性の利用者として、ライブラリ開発者が提供する::typeメソッドを通じて型を取得します。同様に、インターフェース開発時に利用者に必要な型情報を提供することもできます。例えば、この型の関連型情報を提供します。

異なる型の工場があると仮定します。各工場は特定の製品を生産します。この場合、特定の型の工場と対応する型の製品は関連していると考えられます。

例えば、リンゴ工場はリンゴを生産します。まず、これらの二つのクラスを定義します:

class Apple { // ... };

class AppleFactory { // ... };

次に、これらのクラスの関連性を型抽出クラスで定義します。つまり、工場の型を入力すると、工場が生産するオブジェクトの型を取得できます。抽出クラスはテンプレートクラスとして設計されています:

template<typename T>
class FactoryTraits
{
public:
    typedef typename T::ValueType ValueType;
};

次に、クラスの特殊化を通じて具体的な関連性を定義します:

template<>
class FactoryTraits<AppleFactory>
{
public:
    typedef Apple ValueType;
};

これで抽出のすべての定義が完了しました。この例ではAppleFactoryとApple自体はテンプレートクラスではなく、抽出クラスのみがテンプレートクラスです。上記定義の抽出を使用するには、テンプレート内でそれらを使用する必要があります:

template<typename T>
class FactoryContainter
{
public:
	typedef typename FactoryTraits<T>::ValueType ObjType;
	ObjType Produce()
	{
		// ...
	}
};

上記のように、抽出クラスを通じて、FactoryContainerを特定のFactoryにインスタンス化すると、FactoryContainer内で関連クラスにアクセスできます。

FactoryContainter<AppleFactory>::ObjType a; // Type = Apple

異なる型のFactoryを入力すると、::ObjTypeの統一された書き方で対応するオブジェクト型を取得できます。

抽出クラスの実装は難しくなく、いくつかの予設された型を提供するだけです。これはプログラミングの約束と規範に過ぎず、どの具体的な型を提供できるか、そしてそれらの型は明示的なインスタンス化で提供される必要があることを示しています。つまり、抽出クラスの存在により、関連クラスの情報を取得できるようになります。

イテレータ特性

C++標準ライブラリのもう一つの例を見てみましょう。コンテナ要素とイテレータは一定の関連性があります。

イテレータを定義する際、イテレータ特性クラスを使用してコンテナ要素の型を取得できます:

template <class Iterator>
struct iterator_traits {
  typedef typename Iterator::value_type        value_type;
};

以下の呼び出しで型にアクセスできます:

typename iterator_traits<T>::value_type;

ここでvalue_typeは値型を指し、通常すべての修飾子の型を提供する必要があります:

template <class Iterator>
struct iterator_traits {
  typedef typename Iterator::iterator_category iterator_category;
  typedef typename Iterator::value_type        value_type;
  typedef typename Iterator::pointer           pointer;
  typedef typename Iterator::reference         reference;
  typedef typename Iterator::difference_type   difference_type;
};

つまり、イテレータ(Iterator)を実装するには、上記の形式で特性クラスのインスタンスを提供する必要があります。つまり、以下の5つの型を提供する必要があります:iterator_catergory、value_type、pointer、reference、difference_type。すべての型を提供した場合にのみ、イテレータから型にアクセスできます。

そのうちiterator_categoryは現在のコンテナがどのレベルのイテレータアクセスをサポートするかを示します(入力出力イテレータ、前方イテレータ、双方向イテレータ、ランダムアクセスイテレータ)。

特に、標準ライブラリはポインタ型の特殊化バージョンを提供し、ポインタ型がvalue_typeに正常にアクセスできない場合に対応しています。

// partial specialization
template<class T>
struct iterator_traits<T*> {
    typedef T value_type;
};
template<class T>
struct iterator_traits<const T*> {
    typedef T value_type;
};
特性抽出 - SFINAE特性

型特性により、型が「何であるか」を取得できます。さらに、型が「どのようなものであるか」を取得できるようになります。

型特性の取得
template<typename T>
struct is_void {
    static const bool value = false;
};

template<>
struct is_void<void> {
    static const bool value = true;
};

is_void<int> i1; // value = false
is_void<void> i2; // value = true

これは非常にシンプルな例です。まず、テンプレート関数is_voidを定義し、デフォルト値はfalseです。次に、is_void<void>の明示的な特殊化バージョンを提供します。デフォルト値はtrueです。

これにより、is_voidを呼び出すことで、型がvoidかどうかを取得できます。つまり、voidにマッチングできなかった場合、特化されたバージョンは生成されません。

上記のような書き方で、型特性の判定を実現できます。C++標準ライブラリでは、多くの関連する予設メソッドが提供されています:

is_integral
is_enum
is_floating_point
is_arithmetic
is_class  
is_rvalue_reference
is_lvalue_reference
is_scalar
is_compound

上記メソッドについては、通常::valueにアクセスすることで、返される値を取得できます。

さらに、テンプレート関数パラメータのマッチングを使用して実装されたis_classメソッドを見てみましょう。これはis_voidの実装よりも複雑で、入力オブジェクトがクラスかどうかを判断するために使用できます。原理は、クラス型の場合のみ対応する関数を呼び出すことができ、関数の異なる戻り値によって最終的にどのオーバーロードが呼び出されたかを確認します:

template<typename T>
class is_class
{
    typedef char yes[1];
    typedef char no[2];

    template<typename C>
    static yes& test(int C::*); // クラス入力のみ受け付ける

    template<typename C>
    static no& test(...); // ワイルドカード
public:
    static bool const value = sizeof(test<T>(nullptr)) == sizeof(yes);
};

型特性の認識能力を備えると、静的型検査などを行うことができます。前述のforwardの右辺値参照バージョンでは、静的検査が追加されています:

template<typename T>
T&& forward(typename remove_reference<T>::type&& t) noexcept {
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<T&&>(t);
}
広義のテンプレート特殊化

静的型チェックのみであれば、特性抽出はその効果範囲が広くありません。しかし、静的型チェックの特性をテンプレート特殊化に拡張できます。

通常、この特性をSFINAE特性と呼びます。つまり、Substitution failure is not an error。字面の意味は、型マッチング失敗時はその特殊化を破棄し、コンパイルエラーを起こさないことです。

template<class T>

void f(typename T::Y*) { }

上記の例では、型Tに型Yが含まれていない場合、置換失敗とみなされ、そのオーバーロードは選択されません。

前述では、Typeを明確に指定することでテンプレートの明示的な特殊化を実現できます。また、特定の型、例えばポインタに対して部分的特殊化を実装することもできます。しかし、前記では「特定の条件を満たす型」に対して特定の特殊化を実装する方法を説明していませんでした。例えば、enum型に対して特別な処理をしたい場合、期待するコードは以下のようになります:

tempalte<typename T>
class C<T, (if T is enum)>
{
};

この目的を達成するために、SFINAE特性を使用できます。つまり、上記コードの(if T is enum)のプレースホルダーに、Tがenumの場合のみ意味のある文を提供します。Tがenumでない場合、置換を失敗させます。

is_enum<T>::valueは型がenumかどうかを真偽値で返すことができます。しかし、単純な真偽値では無意味な文を取得することはできません。そのため、標準ライブラリではenable_ifがこの目的のために提供されています:

template<bool predicate, typename result = void>
class enable_if;

tempalte<typename result>
class enable_if<true, result>
{
public:
    typedef result type;
};

template<typename result>
class enable_if<false, result>
{
};

このテンプレートクラスでは、式がtrueの場合、enable_if<expression>::typeを介して型を取得でき、falseの場合はenable_if<expression>:::typeは定義されません。

テンプレートにenable_ifを含め、is_enum<T>::valueを第一引数として渡します。is_enum<T>::valueがfalseの場合、enable_if<expression>:::typeは存在しないため、コンパイラはその置換を無視します。

最終的な実装は以下の通りです:

template<typename T, typename Enable = void>
class A
{
};

template<typename T>
class A<T, typename enable_if<is_enum<T>::value>::type>
{
};

enum EName
{
};
int main()
{
    A<float> a1;// primary template
    A<EName> a2; // partial template
}

しかし、上記の記述は少し煩雑です。enable_if_tを使用して記述を簡略化できます:

template<typename T>
class A<T, enable_if_t<is_enum<T>::value>>
{
};

上記の例では、enable_ifの一種の用途を示しました。それはテンプレートの部分特殊化として使用し、追加の型としてです。この追加の型は実際の型演算には関与せず、コンパイラが条件が成立するかを推論するための便利なものです。デフォルト値(void)を提供しているため、利用者はこのパラメータを指定する必要がなく、存在しないかのように扱えます。

総合的に、enable_ifは多くの場所に出現できます。クラスのテンプレートパラメータ、部分特殊化パラメータ、関数テンプレートパラメータ、関数パラメータなどです。以下のように:

クラステンプレートの部分特殊化パラメータ

部分特殊化の使用は前述で紹介されています。複数の部分特殊化バージョンを提供できます:

template<typename T>
class A<T, enable_if_t<is_enum<T>::value>>
{
public:
	A() { cout << "A enum" << endl; }
};

template<typename T>
class A<T, enable_if_t<is_integral<T>::value>>
{
public:
	A() { cout << "A int" << endl; }
};
クラステンプレートのデフォルトテンプレートパラメータ

enable_ifをクラステンプレートのテンプレートパラメータとして使用できます:

template<typename T, typename = enable_if_t<is_enum<T>::value>>
class A
{
public:
	A() { cout << "A enum" << endl; }
};

二つのテンプレートクラスを定義した場合、デフォルトテンプレートパラメータが異なると、コンパイラは同じクラステンプレート定義と認識し、コンパイルできません:

// エラー !
template<typename T, typename = enable_if_t<is_enum<T>::value>>
class A
{
public:
	A() { cout << "A enum" << endl; }
};

template<typename T, typename = enable_if_t<is_integral<T>::value>>
class A
{
public:
	A() { cout << "A int" << endl; }
};
関数テンプレートのデフォルトテンプレートパラメータ

enable_ifを関数テンプレートのテンプレートパラメータとして使用できます。

template<typename T, typename = enable_if_t<is_integral<T>::value>>
void func() { cout << "int" << endl;};

二つの関数テンプレートを定義した場合、デフォルトテンプレートパラメータが異なると、コンパイラは同じ関数定義と認識し、関数オーバーロードと見なさず、コンパイルできません:

// エラー !
template<typename T, typename = enable_if_t<is_integral<T>::value>>
void func() { cout << "int" << endl;};

template<typename T, typename = enable_if_t<is_enum<T>::value>>
void func() { cout << "enum" << endl;};
関数テンプレートの非型テンプレートパラメータ

enable_ifの値を非型テンプレートパラメータとしてクラステンプレートに適用できます。つまり、enable_ifの第二テンプレートパラメータをboolに設定し、true/falseの結果を返します:

template<typename T, enable_if_t<is_integral<T>::value, bool> = true>
void func() { cout << "int" << endl;}

template<typename T, enable_if_t<is_enum<T>::value, bool> = true>
void func() { cout << "enum" << endl;}

このようにして、異なる型の特殊化の「関数テンプレートオーバーロード」を実現し、コンパイルエラーを引き起こさずにできます。

関数テンプレートのデフォルト関数パラメータ

前の例では、enable_ifはテンプレートパラメータリストに適用されていました。実際にはenable_ifは関数パラメータに適用することもでき、指定不要なデフォルトパラメータとして使用できます。以下のように:

template<typename T>
void func(typename enable_if<is_enum<T>::value>::type* = 0) 
{
	cout << "enum" << endl;
}
 
template<typename T>
void func(typename enable_if<is_integral<T>::value>::type* = 0) 
{
	cout << "int" << endl;
}
関数テンプレートの戻り値

enable_ifは関数パラメータに使用できるだけでなく、関数戻り値にも使用できます。std::enable_if<std::is_enum<T>::value>::typeの結果はvoidです。

// 戻り値はvoid
template<typename T>
typename enable_if<is_enum<T>::value>::type func() 
{
	cout << "enum" << endl;
}
 
template<typename T>
typename enable_if<is_integral<T>::value>::type func() 
{
	cout << "int" << endl;
}

// 戻り値はT
template<typename T>
typename enable_if<is_enum<T>::value>::type func1() 
{
	cout << "enum" << endl;
}
 
生成された型の使用

前の例ではenable_ifの特性を利用してテンプレートの生成を制御しましたが、その型自体(第二パラメータ)は使用していません。デフォルトではvoidとして扱いました。これをTに設定すると、型Tとして使用できます。

入力がT:

template<typename T>
void func(typename enable_if<is_integral<T>::value, T>::type t) 
{
	cout << "int" << endl;
}

戻り値がT:

template<typename T>
typename enable_if<is_integral<T>::value, T>::type func() 
{
	cout << "int" << endl;
    return 1;
}
入力型の制御

前節では、enable_ifの適用可能な場所(テンプレートパラメータ、テンプレート非型パラメータ、関数パラメータ、関数戻り値など)と、その使用例である、広義のテンプレート明示的特殊化の実装方法を紹介しました。その成立条件は、型がenable_ifの条件を満たす場合にインスタンス化が生成され、そうでない場合は汎用テンプレートのインスタンス化が使用されることです。この場合、利用可能な「汎用バージョン」が提供されていない場合、型がenable_ifの条件を満たさない場合、適切なオーバーロードが見つからないため、コンパイルエラーが発生します。

この特性を利用して、クラステンプレート/関数テンプレートの型検証と制限を実現できます。つまり、条件を満たす場合のみ対応するクラステンプレートまたは関数テンプレートが生成され、そうでない場合はコンパイルエラーが発生するか、コンパイラは他のオーバーロードを選択します。

例えば、前の関数テンプレートの例では、intの特化バージョンのみを提供した場合:

template<typename T, enable_if_t<is_integral<T>::value, bool> = true>
void func() { cout << "int" << endl;}

この場合、func<float>()を呼び出すとコンパイルエラーになります。

戻り値型の制御

enable_ifを使用して、条件に応じて戻り値型を制御できます。例えば、iが0と0でない場合に異なる型の戻り結果を得たい場合:

template<int i>
typename enable_if<i == 0, int>::type func()
{
	return 0;
}

template<int i>
typename enable_if<i != 0, float>::type func()
{
	return 0;
}

auto t1 = func<0>();
auto t2 = func<2>();
cout << typeid(t1).name() << " " << typeid(t2).name() << endl; // int, float

autoとdecltype

C++には型自動推論に関連する二つのキーワード、autoとdecltypeがあります。これらはコードを書くのをより便利にし、テンプレートの自動型推論に非常に役立ちます。

まず、autoとdecltypeの構文について簡単に紹介します。全体的に、どちらも式/変数の型を推論できますが、autoは式/変数のreference/const属性を削除する傾向があり、decltypeは式/変数のreference/const属性を保持する傾向があります。

auto

コンパイラがautoの型を推論する際、元の型と必ずしも一致しないことがあります。前述のように、以下の違いがあります:

1.参照を無視

参照型の場合、参照オブジェクトの型に翻訳されます。

int i = 0;
int ri = &;
auto a = r; // int

2.トップレベルのconstを無視し、ボトムレベルのconstを保持

const int ci = 1;
const int& cr = ci;
auto b = ci; // int
auto c = cr; // int
auto d = &ci; // const int*

3.autoと&およびconstの組み合わせ

手動で推論対象の&/const属性を追加できます:

const auto e = ci; // const int
auto& f = ci; // const int&

autoの型推論はテンプレート推論のロジックと基本的に一致しており、前述のテンプレート型マッチング規則もautoに適用されます。

decltype

decltypeは変数や式の元の型を基本的には保持します:

1.変数

変数を処理する際、トップレベルのconstと参照を保持します:

const int ci = 0;
const int ri& = ci;
decltype(ci) a = 0; // const int
decltype(ri) b = a; // const int&

変数に括弧を付けると、式として扱われます。代入文の式として現れる変数は参照型を返します:

const int ci = 0;
decltype((ci)) c = ci;
/todo

2.式

コンパイラは式の戻り値型を分析しますが、式自体を実際に計算しません。式は実際の結果を返します:

int a = 0, b = 1;
int* p = &a;
decltype(a + b) x; // int
decltype(*p) y = b; // int&

auto/decltypeはコンパイル時に型を確定するため、コンパイル時にコードを確定するテンプレートに非常に適しており、コンパイル時に既知だがコードを書く段階では未知の型を取得するために役立ちます。

後置戻り値型

auto/decltypeの一般的な用途は、戻り値型の推論を補助することです。

加法の戻り値型をdecltypeで推論したい場合、以下のように記述できます:

decltype(x + y) z = x + y;

この方法をテンプレート関数に拡張する場合、以下のような通常形式では書けません。なぜなら、decltype(t1 + t2)を解析する際、t1とt2の変数がまだ定義されていないからです。

// エラー
template<typename T1, typename T2>
decltype(t1 + t2) add(const T1& t1, const T2& t2) {
    return t1 + t2;
}

この問題を解決するために、C++はラムダ関数のような後置戻り値式をサポートしています:

template<typename T1, typename T2>
auto add(const T1& t1, const T2& t2) ->decltype(t1 + t2) {
    return t1 + t2;
}

C++14では、より簡潔なdecltype(auto)構文が提供され、複雑な後置記法を代替します:

template<typename T1, typename T2>
decltype(auto) add(const T1& t1, const T2& t2) {
    return t1 + t2;
}

さらに、戻り値としてautoを使用することもできます。これはdecltype(auto)とautoの違いは、autoとdecltypeと同じく、変数のconst/reference属性を保持しない可能性があります:

template<typename T1, typename T2>
auto add(const T1& t1, const T2& t2) {
    return t1 + t2;
}

いくつかの状況では、const/reference属性を保持することが非常に重要です。例えば、下記の添字アクセスで、左辺値を返すことを期待しており、これにより直接添字オブジェクトに代入できるようになります:

template<typename T>
decltype(auto) At(T& container, int index)
{
	return container[index];
}

vector<int> vec{1,2,3,4};
At(vec, 0) = 0; // int&
cout << vec[0] << endl; // 出力 : 0
カンマ演算子と型制限

カンマ演算子では、最初のオペランドと二番目のオペランドの結果を計算し、最初のオペランドの結果を破棄し、二番目のオペランドの結果を保持/返します。

後置戻り値型でカンマ演算子をdecltypeに適用すると、式にカンマ演算子を入力し、式は二番目のオペランドの結果を返します。したがって、実際の戻り型を二番目のオペランドの位置に配置し、最初のオペランドには型制限の操作を配置します。

例えば、以下のコードでは型TをClassに制限し、型FをCのメンバ関数ポインタに制限し、戻り値をvoidにします:

​// from cppreference.com
template<class C, class F> auto test(C c, F f) -> decltype((void)(c.*f)(), void()) { }
​
ラムダ式

C++14では、ラムダパラメータでautoキーワードを使用できるようになりました。これはラムダ関数内で関数テンプレートを実装することに相当します:

auto f = [](auto param) { ... };

ただし、template<typename T>は隠されています。これは、Tを介してパラメータ型に直接アクセスできないことを意味し、auto/decltypeの型推論に依存する必要があります:

auto f = [](auto param)
{
    auto param1 = param;
};

auto f1 = [](auto&& param)
{
    remove_reference<decltype(param)>::type param1;
};

ラムダ関数でパラメータの完全転送を実装する場合、次のように実装できます:

auto f = [](auto&& param)
{
    func(std::forward<decltype(param)>(param));
};

可変引数テンプレート

これまでにテンプレートの多くの高度な機能を紹介しました。任意の型にマッチし、型を制御できるようになりました。しかし、汎用性においてはまだ不十分です。なぜなら、パラメータ数は固定されているからです。つまり、汎用転送呼び出しを実装するには、以下のように記述する必要があります:

template<typename T1>
void func_OneParam(T1 t1) { };

template<typename T1, typename T2>
void func_TwoParam(T1 t1, T2 t2) { };

template<typename T1, typename T2, typename T3>
void func_ThreeParam(T1 t1, T2 t2, T3 t3) { };

// ...

これはコーディングプロセスを煩雑にし、拡張性も劣ります。なぜなら、いくつのパラメータのバージョンを予約するか分からないからです。一度だけ記述してすべてのケースを処理できるようにしたいです。言い換えれば、任意の数と型のパラメータを入力できる方法が必要です。

実際、C言語ではすでに似たような方法があります。それは関数のワイルドカードで、...で表されます。最も有名な例は任意の型を印刷できるprintf関数です:

int printf(const char* format, ...)

前述のis_classの実装では、入力型が「クラス」型でない場合に適切なオーバーロードが見つからないことを避けるために、ワイルドカード...の関数をfallbackとして使用しました。

同様に、C++テンプレートもワイルドカードに似た可変引数を提供し、任意の型と数の入力パラメータをサポートします。テンプレートクラスと関数テンプレートに適用できます。その構文は以下の通りです:

template<typename... Args> class C; // 可変クラステンプレートパラメータ
template<typename... Bases> class D : public Bases... { } // 可変引数を基底クラスとして

template<typename... Args> void func(Args... args) {} // 可変関数引数

ここで、typename... ArgsはArgsがテンプレートパラメータパック(parameter pack)であることを示し、0個以上のテンプレートパラメータを受け取ることができます。

Args... argsはargsが関数パラメータパックであることを示し、0個以上の関数パラメータを受け取ることができます。関数パラメータパックでは、sizeof...argsを使用して現在のパラメータパック内のパラメータ数を取得できます。

パラメータパック内のパラメータを読み取るには、パラメータパックを「展開」する必要があります。この過程はパラメータパック展開(parameter pack expansion)と呼ばれます。

パラメータパック展開

省略記号をパラメータパックを含むパターンの右側に置くと、パラメータパック展開と呼ばれます:

Pattern...

例えばargs...は最も簡単なパラメータパック展開であり、arg0, arg1, arg2, arg3...に展開されます。

パターンは少なくとも一つのパラメータパックを含まなければならず、さまざまな形式を持ちます。例えば:

&args... // 展開される: &arg0, &arg1, &arg2
std::forward<Args>(args)... // 展開される: std::forward<Args>(args0), std::forward<Args>(args1)...
++args; // 展開される: ++arg0, ++arg1, ++arg2...
(args, 0)... // 展開される: (arg0,0), (arg1, 0)...

展開された後、パラメータパック内の各パラメータの複数のパターンが得られます。

パラメータパック展開は限定された位置でのみ発生します。一般的には:

① 関数パラメータリスト

func(args...)
auto func = [args...]() { }

② 初期化項目/初期化リスト

Class c(args...)
{ args... }

③ テンプレートパラメータリスト

vector<T, Args...>

パラメータパックから個々のパラメータを解析したい場合、具体的な実装関数が必要です。この関数は通常のテンプレート関数であり、各パラメータの具体的な処理はこの関数に記述します:

template<typename T> 
void funcImpl(T value)
{
    // ...
}

今、展開が以下のような形式になることを望んでいます:funcImpl(args0)、funcImpl(args1)、funcImpl(args2)... これにより、各パラメータに一致するロジックを一度実行できます。したがって、パラメータパック展開は以下のようになります:

funcImpl(args)...

ここでのパターンはfuncImpl(args)であり、結果はvoidですが、これは不適切です。パラメータパック展開を適切にするために、funcImplに任意の戻り値を追加できます:

template<typename T> 
int funcImpl(T value)
{
    // ...
    return 0;
}

または、funcImplに戻り値を追加せずに、カッコ式を使用して結果をvoidにしないようにできます。カッコ式の結果は二番目の値を返します:

(funcImpl(args), 0)...
関数テンプレート:関数展開を使用

すでにパラメータパック展開の例を取得しましたが、前述のようにパラメータパック展開は特定の位置でのみ発生するため、上記のパラメータパックはすべてのロジックを実装できますが、構文的には不適切です。このため、構文的に正しいために「殻」を用意する必要があります。この「殻」には実際の役割はありません。

関数で展開する場合:

template<typename... Args> 
void helper(Args&&...) {}

template<typename... Args> 
void func(Args&&... args) 
{
	helper(funcImpl(args)...); // またはhelper((funcImpl(args),0)...);
}

得られる展開結果は以下のようになります:

helper(funcImpl(arg2), funcImpl(arg1),funcImpl(arg0));

関数パラメータの評価順序は不定であるため、順序が重要な場合は関数は良い選択肢ではありません。

関数テンプレート:初期化リスト展開を使用

前述のように、パラメータパック展開は波括弧内でも発生します。したがって、展開を実現するために、実際の役割のない初期化オブジェクトを殻として構築できます:

template<typename... Args> 
void func(Args&&... args) 
{
	int arr[] { (funcImpl(args),1)...};
}

初期化リストの評価順序は確定されているため、以下の展開結果を得られます:

int arr[] { (funcImpl(arg0),1), (funcImpl(arg1),1), (funcImpl(arg2),1)};
関数テンプレート:再帰展開を使用

さらに一般的な方法は再帰関数展開を使用することです。しかし、再帰を使用するため、パフォーマンスは直接展開ほど良くありませんが、コードの可読性はより直感的です。

一般的な例は再帰展開による出力です:

再帰展開には通常、再帰関数と終了関数の二つの関数が必要です。これらはどちらも不可欠です。パラメータパックが最後のパラメータだけになった場合、終了関数が呼び出されます。

C++17では、畳み込み式を使用して再帰展開の記述を簡略化できます。再帰展開を実装するために二つの関数を使用する必要がなくなります。

前述のテンプレート関数パラメータパック展開の方法以外にも、テンプレートクラスを使用することがあります。例えば、C++では任意数の型を持つtupleクラスをサポートできます:

tuple<int, char, string> t(1,'a',"test");

テンプレートクラスを展開するには、継承+再帰の方法を使用します。

クラステンプレート:継承による再帰展開

クラステンプレートの展開を継承で実現するには、再帰終了クラスと再帰展開実装クラスを含める必要があり、またクラステンプレートの前方宣言が必要です:

// 前方宣言
template <class... _Types>
class tuple;

// 再帰終了クラス
template <>
class tuple<> { // 空のtuple
public:
    // ...
};

// 継承による再帰実装クラス
template <class _This, class... _Rest>
class tuple<_This, _Rest...> : private tuple<_Rest...> { // 再帰tuple定義
public:
    // ...
    constexpr _Mybase& _Get_rest() noexcept { // 残り要素への参照を取得
        return *this;
    }

    _Tuple_val<_This> _Myfirst; // 格納された要素
};

_Myfirstを通じて現在の要素にアクセスし、_Get_rest()を通じて残りの要素にアクセスします(*thisは親クラスを指します)

テンプレート関数のオーバーロード解決

前述で、複数の同名関数を定義できることを学びました。これは通常の関数、テンプレート関数、テンプレート関数の明示的特殊化バージョンを含みます。複数の同名テンプレート関数のバージョンを同時に定義し、入力パラメータも一致する場合、コンパイラはどのマッチング関数を選択するのでしょうか?

全体的に、コンパイラはより「特殊化された」バージョンを優先的に選択します。以下の優先順位に従います:通常関数 -> 明示的特殊化関数(完全特殊化) -> 部分特殊化関数(偏特殊化)-> 一般テンプレート関数。これにより、追加で定義した特殊化関数が効果を持つ可能性があります。

しかし、パラメータがそれほど一致せず、通常関数とテンプレート関数が存在する場合、どのように決定すべきでしょうか?

オーバーロードマッチングルール

テンプレート関数のオーバーロードを実装する際、以下の手順に従います:

1.コンパイラはまず置換明示的特殊化(特殊化)のテンプレートパラメータ

2.コンパイラはテンプレートパラメータの型を決定します

3.コンパイラは関数パラメータ/戻り値、テンプレートパラメータ値/式を置換してその型にします

4.置換が失敗した場合、オーバーロード集合から除外します

5.置換が成功した場合、置換後の関数と他の関数とのマッチングを比較します

6.置換が成功した関数のパラメータが完全に一致し、より特殊化されたバージョンの場合、通常関数-明示的特殊化関数-一般テンプレート関数のルールに従ってより適切なオーバーロードを選択します

7.置換が成功した関数が同名だがパラメータが一致しない場合、よりマッチするパラメータを選択してオーバーロードします

8.よりマッチするパラメータを決定できなかった場合、オーバーロードの曖昧さが発生し、コンパイルエラーになります

● 簡単に言うと、コンパイラはテンプレートパラメータ置換を完了してからオーバーロード解決を行い、パラメータが一致する場合はより特殊化されたバージョンを選択し、パラメータが一致しない場合はよりマッチするパラメータを選択します。

● オーバーロード集合には通常の関数と置換後のテンプレート関数が含まれ、これらはすべてオーバーロード候補として使用できます。

置換の場所は以下の通りです:

1.関数定義内の型/式(戻り値/入力パラメータを含む)

2.テンプレートパラメータ定義内の型/式

3.テンプレート明示的特殊化パラメータ内のパラメータ/式

これは置換の場所が多くあるため、SFINAE特性を使用する際、型判断ロジック(例:enable_if)を複数の場所に配置できます。これらの場所はすべてコンパイル時の置換計算に参加するためです。ただし、関数本体内部は置換プロセスに参加しないことに注意してください。

以下は具体的なテンプレート関数オーバーロードの例です:

ポインタ

テンプレート関数オーバーロードの一般的な方法は、通常のテンプレートを提供しつつ、ポインタと定数ポインタの特殊化バージョンを提供することです。これは通常、ポインタ型は特別な処理が必要なためです。

ポインタ型の特殊化を提供すると、ポインタ型は対応するテンプレート関数に優先的にマッチします:

template<typename T>
void Test(T t)
{
	cout << "Test(T t)" << endl;
}

template<typename T>
void Test(T* t)
{
	cout << "Test(T* t)" << endl;
}

template<typename T>
void Test(const T* t)
{
	cout << "Test(const T* t)" << endl;
}

const int i = 2;
const int* cp = &i;
Test(cp); // Test<int>(const int*)
リストと配列

通常のテンプレートは波括弧初期化リストの型を推論できません。そのため、対応する型を受け取るために、手動でinitializer_listのオーバーロードを提供する必要があります。以下のように:

template<typename T>
void func(T param);

template<typename T>
void func(std::initializer_list<T> initList);

func({ 1, 2, 3}); // match void func(std::initializer_list<int>)

同様に、配列型に対してオーバーロードを提供できます:

template<typename T>
void func(Array<T>& arr) { }
クラスタイプ

入力が「クラス」型であることを保証したい場合、以下のバージョンのオーバーロードを提供できます:

template<typename T>
void func(T param);

template<typename T>
void func(T::* param);

参照とconst

前述で、参照とconstの同名関数のマッチングルールについて紹介しました。すでに参照バージョンのオーバーロード関数を定義した後に、通常のテンプレート関数を定義すると、オーバーロード解決で衝突が発生します。なぜなら、コンパイラがどちらのマッチがより優れているか判断できないからです:

template<typename T>
void Test(T t)
{
	cout << "Test(T t)" << endl;
}

template<typename T>
void Test(const T& t)
{
	cout << "Test(const T& t)" << endl;
}

template<typename T>
void Test(T&& t)
{
	cout << "Test(T&& t)" << endl;
}

int i = 1;
const int ci = 2;
Test(i); // エラー! Test(T t) Test(T&& t)にマッチ
Test(2); // エラー! Test(T t) Test(T&& t)にマッチ
Test(ci); // エラー! Test(T t) Test(const T& t)にマッチ

したがって、参照バージョンの入力後に通常バージョンを提供することは一般的に行いません。

残りのテンプレートオーバーロード関数については、前述のルールに従い、同じ入力でも複数の関数に正しくマッチする可能性がありますが、コンパイラはより優れた結果を決定できるため、曖昧さは発生しません。

左辺値非const変数の場合、左辺値参照に優先的にマッチし、次に万能参照にマッチし、最後に定数左辺値参照にマッチします:

Test(i); // Test(T&) -> Test(T&&) -> Test(const T&)
// iは定数ではないため、constバージョンに優先的にマッチしません
// iは左辺値なので、左辺値参照の方が万能参照よりマッチします

右辺値の場合、万能参照に優先的にマッチし、次に定数左辺値参照にマッチします。左辺値参照にはマッチしません:

Test(1); // Test(T&&) > Test(const T&)

左辺値const変数の場合、定数左辺値参照に優先的にマッチし、次に左辺値参照にマッチし、最後に万能参照にマッチします:

Test(ci); // Test(const T&) -> Test(T&) -> Test(T&&)
万能参照のオーバーロード

前述で参照/constの関数オーバーロード解決ルールについて紹介しました。テンプレート関数で紹介しましたが、このルールは通常の関数にも適用され、テンプレート関数と通常の関数が共存する場合にも適用されます。前述の関数オーバーロード解決ルールでは、テンプレート関数のパラメータ置換を完了し、置換後の関数を「公平に」通常の関数と比較します。

前述で、万能参照を使用して完全転送を実現する方法を紹介しました:

class Test {
public:
    template<typename T>
    Test (T&& t) { ... }
};

上記クラスの場合、ルールに従って、コンパイラはデフォルトでコピー構築関数を生成し、これは完全転送構築関数とオーバーロード関係を形成します。

この場合、コピー構築関数を呼び出そうとすると、前述の関数オーバーロード解決ルールに従い、非常量左辺値に対してはT&&がconst T&より優れたマッチとなるため、実際にはコピー構築関数は呼び出されません:

Test t1;
Test t2(t1); // Test(T&&)を呼び出す
サブ関数オーバーロードマッチング

これにより、万能参照の「マッチ」能力が非常に強いため、関数オーバーロード解決の結果が期待と一致しないことがよくあります。

時には関数オーバーロードを回避することもできます。例えば、名前を変更した関数を提供しますが、場合によっては関数オーバーロードを回避できません。例えば上記の構築関数のように。この場合、SFINAE特性を使用して関数オーバーロードを補助する必要があります。つまり、特定の型のみが万能参照にマッチするように制限します。言い換えれば、コピー構築関数にマッチさせたい場合は、万能参照にマッチさせてはいけません。

いつコピー構築関数にマッチすべきでしょうか?つまり、入力型とクラス型が完全に一致する場合です。したがって、次のように実装できます:

class Test
{
public:
    template<typename T, 
        typename = enable_if_t<!std::is_same<Test, typename std::decay<T>::type>::value>>
    Test(T&& t);
};
// decay : remove const/reference

C++標準ライブラリでの使用例を見てみましょう:

vector(const size_type _Count, const _Ty& _Val, const _Alloc& _Al = _Alloc())
{
}

template <class _Iter, enable_if_t<_Is_iterator_v<_Iter>, int> = 0>
vector(_Iter _First, _Iter _Last, const _Alloc& _Al = _Alloc())
{
}

これはvectorの二つの構築方法を示しています。一つは要素数とデフォルト値を指定し、もう一つはイテレータの先頭と末尾を使用して構築します。第二の構築関数にイテレータ型の制限をかけなかった場合、要素型とsize_typeが一致する場合、最初の構築関数を呼び出そうとしたときに、間違って第二の構築関数にマッチしてしまう可能性があります。

その他の問題

テンプレートと継承

派生クラスで基底クラスの内容にアクセスしたい場合、テンプレートクラスの場合、通常のクラスとは異なり、基底クラスの名前は派生クラスからは直接見えません。そのため、通常のアクセス方法ではアクセスできません。

① thisを使用して基底クラスにアクセス

前述のtupleについて、tupleが残りの要素にアクセスする方法を紹介しました。_Get_rest()が*thisにアクセスしていることに気づくかもしれません。詳しく考えてみると、tupleは再帰定義クラスであり、各tupleは最初の要素のみを含みますが、なぜ*thisが残りの要素の意味を表すのでしょうか?

template <class _This, class... _Rest>
class tuple<_This, _Rest...> : private tuple<_Rest...> { // 再帰tuple定義
public:
    constexpr _Mybase& _Get_rest() noexcept { // 残り要素への参照を取得
        return *this;
    }
};

実際、現在の文脈ではthisは基底オブジェクトを指します。tupleが最初の要素を含む場合、基底は残りの要素を含みます。

同様に、テンプレート基底クラスの関数にアクセスするには、this->を使用できます:

template<typename T>
class Base
{
public:
    void BaseCall() { }
};

template<typename T>
class Derived : public Base<T>
{
public:
    void Call()
    {
        this->BaseCall(); // OK
        BaseCall(); // エラー
    }
};

基底クラス関数を直接呼び出すことはできません。なぜなら、テンプレートTが確定していないと、親クラスはまだインスタンス化されていないため、コンパイラは基底クラスにその関数が含まれているか確認できず、関数定義の検索空間に基底クラスを含めません。this->を使用することで、コンパイラにその関数の定義が基底クラスから来る旨を伝えます。

  1. usingを使用して基底クラスにアクセス

using文を使用することもできます。これはusingが派生クラスで隠された基底クラス名にアクセスするために使用できるからです:

template<typename T>
class Derived : public Base<T>
{
public:
    using Base<T>::BaseCall;
    void Call()
    {
        BaseCall(); // OK
    }
};
テンプレートとフレンド関数

テンプレートクラスの場合、通常のクラスよりもフレンド関数の状況が複雑になります。例えば、このフレンド関数が共有されているか、各インスタンスごとに独立しているか、フレンド型とテンプレート型が関連しているかなどを考慮する必要があります。

上記の状況に応じて、フレンド関数は大体三つの場合に分類できます:

① 非テンプレートフレンド

フレンド関数がテンプレート関数でない場合、フレンド関数は一つだけ存在し、すべてのインスタンスのフレンドです:

void friendfunc()
{
	cout << "フレンド関数を呼び出す" << endl;
}

template<class T>
class C
{
public:
    friend void friendfunc();
};

② 制約付きテンプレートフレンド

フレンド関数がテンプレート関数であり、その型がテンプレートクラスの型に関連している場合、各クラスの特殊化に対応するフレンド関数があります:

template<typename T>
void friendfunc()
{
	cout << "フレンド関数を呼び出す " << endl;
}

template<typename T>
class C
{
public:
	friend void friendfunc<T>();
};

③ 制約なしテンプレートフレンド

フレンド関数がテンプレート関数であり、その型がテンプレートクラスの型に関連していない場合、各フレンド関数の特殊化は各特殊化されたクラスに対応できます:

template<typename T>
void friendfunc()
{
	cout << "フレンド関数を呼び出す " << endl;
}

template<typename T>
class C
{
public:
    template<typename U>
	friend void friendfunc<U>();
};

テンプレートのいくつかの一般的な高度な機能を紹介しました。次はより応用的な内容に焦点を当てます。

暗黙的な変換のサポート

テンプレート型は「新しい型」を生成する傾向があり、暗黙的な変換は行わないことがわかります。実際、テンプレート推論も暗黙的な変換をほとんどサポートしていません。この場合、暗黙的な変換を実現したい場合、メンバ関数テンプレートを使用できます。例えば、スマートポインタがポインタの暗黙的な変換をシミュレートします:

template<typename T>
class SmartPtr {
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other): ptr(other.get()){};
};

このような記述を一般的なコンストラクタと呼びます。この記述により、派生クラスから基底クラスへの変換などを実現できます:

SmartPtr<Base> p = SmartPtr<Derived>(new Derived);
関数コールバック

メッセージ処理メカニズムクラスを実装して関数コールバックを実行したい場合、テンプレートを使用できます。

これを実現するには、ネストされたテンプレートクラスを使用する必要があります。まず、任意の型の関数を生成できるテンプレート関数クラスを実装し、次にユーザー向けの関数クラスを実装し、より関数のようなラッピングを提供します。

定義された関数クラスに対して:

最初に関数ポインタをキャッシュする必要があります。関数はテンプレートとして設計できます。

さらに、この関数を呼び出す必要があります。そのため、パラメータ型と戻り値型を知る必要があります。関数呼び出しには戻り値と任意数のパラメータ入力が含まれるため、二つのテンプレートパラメータを設計します。一つは通常のテンプレートパラメータ、もう一つは可変テンプレートパラメータで単一テンプレートパラメータを置き換えます。パラメータの転送を考慮して、万能参照を使用する必要があります。

通常の関数のような呼び出し方法を提供するために、operator()のオーバーロードを実装する必要があります:

template <typename Ret, typename... Params>
class FuncImpl
{	
public:
	using FuncType = Ret(*)(Params...);
	FuncImpl(FuncType func) 
	{
		funcImpl = func;
	}
	Ret operator()(Params&&... params) 
	{
		return funcImpl(forward<Params>(params)...);
	}
private:
	FuncType funcImpl;
};

以上で基本的な関数ラッピングが実装されましたが、呼び出し側にとっては不便です。ユーザー向けの関数クラスを設計し、FuncImplをそのメンバーとして実装して、具体的な関数の保存と呼び出しを実装します。

型フォーマットを制限できます:class Func<Ret(Params...)>。これにより、Func<void(int)>のような形式で呼び出すことができます。

template <typename Ret>
class Func;

template <typename Ret, typename... Params>
class Func<Ret(Params...)>
{
	using CallableType = FuncImpl<Ret, Params...>;
	CallableType* Callable = nullptr;
public:
	Func(typename CallableType::FuncType func)
		: Callable(new CallableType(func)) { }

	Ret operator()(Params... params)
	{
		return (*Callable)(forward<Params>(params)...);
	}
};

これは実行可能な簡略化されたクラスであり、詳細な内容はstd::functionの実装を参照してください。違いは、STLでは継承ではなくコンポジションを使用して呼び出し可能オブジェクトを含めています。

タグ: cpp template programming Cplusplus type-traits

7月26日 18:16 投稿