C++
C++11

【SFINAE】可変リストによるテンプレート関数の優先実体化

More than 1 year has passed since last update.


標準にメタ関数がある、あるいは簡単にメタ関数が作れる場合

メタプログラミングでは、型に関する検査を日常的に行う。

それは多くの場合、特殊化によって実装を分岐する等の分かりやすいメタ関数か、標準ライブラリに存在するメタ関数で行われる。

例えば、ある型がconst修飾されているかどうかを検査するメタ関数は以下のような実装が考えられる。

template<class T> struct is_const : public std::false_type {};

template<class T> struct is_const<T const> : public std::true_type {};

このように、型に関する特殊化で事足りる場合、プログラムは非常に簡潔なものとなる。

このような、ある特性を評価しboolを返すようなメタ関数を量産していけば、型の検査を用いたSFINAEによる実装の分岐は、脳死していてもコピペで量産することができる。


#include <type_traits>
#include <iostream>

template<class T>
auto printBasicType()
-> typename std::enable_if<std::is_null_pointer<T>{}>::type
{
std::cout << "null pointer" << std::endl;
}

template<class T>
auto printBasicType()
-> typename std::enable_if<std::is_array<T>{}>::type
{
std::cout << "array" << std::endl;
}

template<class T>
auto printBasicType()
-> typename std::enable_if<std::is_integral<T>{}>::type
{
std::cout << "integral" << std::endl;
}

template<class T>
auto printBasicType()
-> typename std::enable_if<std::is_class<T>{}>::type
{
std::cout << "class" << std::endl;
}

int main() {
class Hoge {};
printBasicType<std::nullptr_t>();
printBasicType<int>();
printBasicType<int[]>();
printBasicType<Hoge>();
return 0;
}

あるいは、Enablerテクニックを使う。


extern void* enabler;

template<class T, typename std::enable_if<std::is_null_pointer<T>{}>::type*& = enabler>
void printBasicType()
{
std::cout << "null pointer" << std::endl;
}

//...同様

---- 追記 ----

Enablerテクニックにおいて,staticストレージのvoidポインタ型の変数を参照するよりも安全な手法がコメントより提案されました.

値を一つしか持たないstd;::nullptr_t型を使用するというものです.

template<class T, typename std::enable_if<std::is_null_pointer<T>{}, std::nullptr_t>::type = nullptr>

void printBasicType()
{
std::cout << "null pointer" << std::endl;
}

---- 追記ここまで ----

しかし、このEnablerテクニックはあくまで型特性をboolで評価するメタ関数があることを前提としている。

メタ関数がなくては始まらないのだ.

冒頭にあるように,特殊化を用いて簡単に場合分けできる場合は,ここで解説する必要などないだろう.

この記事を読んでいるみなさんは私などより優秀であるからだ.


ちょっといやらしいメタ関数のケース

ここで,単純な特殊化では実装が難しいケース,例えばあるメンバ関数を持つかどうかの検査を行うメタ関数を実装してみる.

以下はダメな例.当然ながらテンプレート引数の数が違うのでアウト.

template<typename T, typename = decltype(std::declval<T>().hoge())>

struct has_hoge : std::true_type {};

template<typename T>
struct has_hoge : std::false_type {};

関数でやるとどうか.

template<class T>

struct has_hoge {
template<typename U = T, typename = decltype(std::declval<U>().hoge()) >
static std::true_type test();
static std::false_type test();
};

これはコンパイルが通る.

通るが...どんな型を渡そうが...常にfalse_typeになる.

なぜなら,テンプレート関数は非テンプレート関数よりも呼び出す関数の解決の優先順位が低いからである.

普通の関数-特殊化関数-テンプレート関数

の順で優先的に解決される.解決できなかった際は,暗黙変換を試みて,再度解決を行う.

つまり,非テンプレートのtestがマッチした時点で呼び出す関数は確定する.

これはもうだめだ...やっぱりSFINAEなんて無理だったんや...

と思うのはまだ早い.この世には最も優先順位が低いかわいそうな関数がある.

void test(...);

template<typename Args...>
void test(Args... args);

何をかくそう,可変引数を持つ関数.

こいつらは,あらゆる関数の呼び出し解決を試みて,最終手段として最後に呼び出される.

こいつを使えばok


#include <type_traits>

template<class T>
struct has_hoge {
template<typename U = T, typename = decltype(std::declval<U>().hoge()) >
static std::true_type test(int);
static std::false_type test(...);
};

int main() {
struct piyo {
bool hoge(){ return true; }
};

static_assert(decltype(has_hoge<piyo>::test(0)){}, "!!");
static_assert(!decltype(has_hoge<int>::test(0)){}, "!!");

return 0;
}

コンパイルが通るよ! やったねたえちゃん!

ちなみにこのテク,結構そこら中で使われている.

STLのtype_traitsでは特に.libc++のstd::is_convertibleとか.

かなり応用範囲が広いので是非使っていこう.

どうでもいいが,コンパイラによってはdeltype(exp)::valueは許可されていないので,

decltype(has_hoge<piyo>::test(0))::value

とは書けない.

identityと呼ばれるメタ関数をかませるのが一般的だ.

template<class T>

struct identity {
typedef T type;
};

//...
static_assert(identity<decltype(has_hoge<piyo>::test(0))>::type::value, "!!");

gcc4.6くらいまではdecltype(exp)::valueは無理みたいだ.

std::identityはconceptのように闇へと葬り去られたらしい.