#C++関数テンプレートと半順序とオーバーロード
この記事は「プロ生ちゃん Advent Calendar 2015」の19日目の記事です
はじめまして、いなむ先生と申します
C++とテンプレートが大好きです
この記事では関数テンプレートに関して解説します
##関数テンプレート
関数テンプレートとは変数の型をテンプレート宣言したもの
C++98から導入された新機能である
template < typename T >
T max(T a, T b) ;
この場合には呼び出しの実引数から型推論が行われる
max(1,2); // T is int
max(1.0, 3.0); // T is double
##オーバーロード
オーバーロードとは同じ名前を持つ引数の型が異なる関数を呼び分ける事ができる機能
double max(double a, double b); // #1
int max(int a, int b); // #2
max(1.0, 2.0); // calls #1
max(1,2) // calls #2
##半順序その前に
テンプレートは更に複雑な宣言が可能である
テンプレートの呼び分けを可能にする機能を紹介する
###明示的特殊化と部分的特殊化
引数が特定の型の場合に呼び出すテンプレートを指定することができる
これを関数テンプレートの明示的特殊化という
template < typename T >
T f(T a, T b)
{
return a + b ;
}
template < >
int f<int>(int a, int b)
{
return a * b ;
}
テンプレートであることを示すためにtemplate宣言は省略できない
< >
の中身をカラで宣言する
さらに、関数名に続いて<>
を書きその中に特別扱いする型を記述する
これを利用して特定の型で関数テンプレートが呼ばれた場合にコンパイルエラーとすることができる
template < typename T >
T f(T a, T b)
{
return a + b ;
}
template < >
int f<int>(int a, int b) = delete ;
上の例では、intの場合には明示的特殊化が優先的に呼ばれるが__deleted宣言__されているためにコンパイルエラーとなる
次のような特殊化を行うことはできない
template < typename L, typename R >
void f(L lhs, R rhs) ;
template < typename L >
void f<L,int>(L lhs, int rhs) ; // Error !
template < >
void f<int,int>(int lhs, int rhs) ; // OK !
2つ目の特殊化を部分的特殊化という
これはクラステンプレートではできるが、関数テンプレートでは許可されていない
具体的にテンプレート仮引数を指定する特殊化の場合、テンプレート仮引数をすべて指定しなければならない
次のようにオーバーロードは行うことができる
template < typename T >
void f(T* p) ; // Tのポインタのみを引数に取る
多くの場合には部分的特殊化ではなく、オーバーロードで代用できます
N4140 14.8.2.5 Deducing template arguments from a type
より引用すれば
推論されるテンプレートの宣言は以下のとおり
- T
- cv-list T
- T*
- T&
- T&&
- T[integer-constant ]
- template-name (where template-name refers to a class template)
- type (T)
- T()
- T(T)
- T type ::*
- type T::*
- T T::*
- T (type ::*)()
- type (T::*)()
- type (type ::*)(T)
- type (T::*)(T)
- T (type ::*)(T)
- T (T::*)()
- T (T::*)(T)
- type [i]
- template-name (where template-name refers to a class template)
- TT
- TT
- TT<>
Tはテンプレート引数
TTはテンプレートテンプレート引数
iは非型テンプレート引数
(T)は少なくともひとつ以上の引数を持つ関数の引数リスト
()は引数のない関数の引数リスト
は少なくともひとつ以上のテンプレート引数を持つテンプレート引数リスト
<>はテンプレート引数を持たないテンプレート引数リストである
対して、テンプレートが決して推論されない場合がある
- ネストした名前にテンプレート引数が使われてい
::の左側にテンプレート仮引数がある場合と思っていい
template < typename T >
void f( typename hoge<T>::type ){} // non-deduced contexts
-
decltype式(C++14以降)
decltype
式は推論の対象にならない -
非型テンプレート引数または配列の要素数がテンプレート引数を参照する部分式である
template< std::size_t N >
void foo( int(&)[2*N] ){} // error
template< std::size_t N >
void bar( int(&)[N] ){} // OK
[N]はOKで
[2*N]は部分式なのでアウト
- 推論に使用される引数において、デフォルト引数が適用になる
デフォルト引数から推論することができない
template < typename T >
void f( T = 0 ) ;
f() // error!
- 初期化リストが指定された場合
初期化子リストをinitializer_listと推論できない
template < typename T >
void f(T) ;
f({1,2,3}); // error!
##半順序
通常の関数、テンプレートが混在した場合はどれが呼ばれるのか?
おおまかに次のようになる
f( [int] )
を実行したとき([int]
はintがcvr修飾されたもの)
-
f(int)
,f(int const)
,f(int&)
等、intがcvr修飾されたもの -
f<int>(int)
などの特殊化されたテンプレート -
f(T)
,などtemplate -
f(double)
,f(long)
など型変換可能な引数を持つもの - f(...)可変長引数
という順序で呼ばれる
この順序大まかにしか決まっておらず、
推論に一致するものがないまたは、推論が複数のテンプレートに一致する場合においてはコンパイルエラーとなる
1.の場合においては、実引数がcv修飾されている場合にはcv修飾された関数が優先的に呼ばれる
テンプレートでも同じことができる
##関数の戻り値推論など
###関数の戻り値を推測する事ができる
template < typename L, typename R >
auto f(L&& a, R&& b)
{
return a + b ;
}
この場合はa + b
から型が推測されるがvc修飾は型に含まれない
関数の戻り値をcv修飾を含めた完全な型で推論するためにはdecltype(auto)
を用いる
template < typename L, typename R >
decltype(auto) f(L&& a, R&& b)
{
return a + b ;
}
ただし、decltype(auto)
には落とし穴があり
戻り値を()
で囲んだ場合に起こる
C++では 変数x
は左辺値であり、(x)
も左辺値である
このことによってx
がint
の場合
decltype(x) = int
であり
decltype( (x) ) = int&
である
template < typename L, typename R >
decltype(auto) f(L&& a, R&& b)
{
return (a + b) ;
}
このようなコードを書いた場合a + b
の結果というローカルオブジェクトを参照してしまう
###関数の戻り値を後置する
template < typename L, typename R >
auto f(L&& a, R&& b)->decltype(a + b)
{
return a + b;
}
このように書くことで、戻り値を後置することができる
単にreturn文から推論する場合はdecltype(auto)
を用いれば良い
同じ式を二度も書くのはカッコ悪い
##SFINAE
SFINAEとは
Substitution Failure Is Not A Error
の略語である
日本語にすると
「テンプレートのインスタンス化の失敗はエラーではない」
となる
テンプレートのインスタンス化に失敗しても、即座にエラーにならず
オーバーロードの候補から外れる
そうしておいて、更に一致するテンプレートを探すことができる
これを悪用 利用すると任意の場合にオーバーロードを分岐させる事ができます
std::enable_if<>
を用いて特定の条件をメタ関数などで記述することで
条件に合致した場合にテンプレートのインスタンス化を失敗させることで
オーバーロードの候補から外すことができる
テンプレート仮引数にSFINAE
template <typename T, std::enable_if_t<pred>*& =nullptr_t>
void f(T t){}
戻り値にSFINAE
template <typename T>
std::enable_if_t<pred,ReturnType> f(T t){}
引数にSFINAE
template <typename T>
void f(T t,std::enable_if_t<pred>*=nullptr){}
更に戻り値の後置宣言で用いるdecltype
と合わせる事ができる
今回はメンバ関数にfindを持つmap
と、持たないコンテナにおいて
統一して要素を検索できるようにmy_find
を書いてみた
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <map>
template < typename Container >
auto my_find(Container& c, typename Container::key_type key)
->decltype((c.find(key)))
{
std::cout << "member" << std::endl;
return (c.find(key));
}
template < typename Container >
decltype(auto) my_find(Container& c, typename Container::value_type v)
{
std::cout << "free" << std::endl;
return (std::find(c.begin(),c.end(), v)) ;
}
int main(){
std::vector<int> a{1,2,3};
std::map<int,int> b;
b[0] = 1;
b[2] = 3;
b[4] = 5;
auto&& iter1 = my_find(a,2);
std::cout << ( iter1 == a.end() ? "not found" : "found" ) << std::endl;
std::cout << std::endl;
auto&& iter2 = my_find(b,2);
std::cout << ( iter2 == b.end() ? "not found" : "found" ) << std::endl;
return 0;
}
実行結果
free
found
member
found
同じ呼び出しで範囲から見つかった場合は要素を指すイテレータ
見つからなかった場合はend
を指すイテレータが返る
こんなややこしいことをして何が楽しいのかは放っておいてください
密かに、第二引数はテンプレートの推論ルールでは推論されません
依存型です
これを推論可能なテンプレートにした場合にはkey_type や value_type
との整合性が取れなかった場合にはコンパイルできなくなるため
ちょっとしたテクニックですかね?
テンプレートは便利ですが、SFINAEなど
本来なら条件分岐の文法ではないものをオーバーロードの条件分岐に用いたり
メタプログラミング方面で進化を遂げすぎたため
永遠に玄人のテクニックである気がします
static_if
が渇望される!
##跋文
最後まで読んでくださった方ありがとうございました
突貫で執筆したためチェックが行き届いておりません
間違いがありましたらコメントをよろしくお願いします