Edited at

C++メタ関数のまとめ

More than 1 year has passed since last update.


メタ関数のまとめ


メタ関数とは?

メタ関数とは、コンパイル時に型情報を取得できる関数のようなもの

実際には、関数の形を取らず、クラスになっていることがほとんどだが

関数にすることもできる

例えば型Tと型Uが同一かどうかを確かめたい場合は

std::is_same<T,U>::value

とすれば

is_sameはT,Uが同じ型ならネストされたメンバ変数valueはtrue

T,Uが違う型ならfalseを返す

他には、どのようなことができるのか?

標準ライブラリを見ていこう!


標準ライブラリ<type_traits>のメタ関数一覧

メタ関数はC++11から追加された標準ライブラリ<type_traits>に定義されている

C++14ではis_nullptris_finalが仲間入りした

C++17からはis_literal_type、が非推奨になる

また、

has_unique_object_representations

is_swappable_withis_swappableis_nothrow_swappable_withis_nothrow_swappable

is_callableis_nothrow_callable

conjunctiondisjunctionnegation

bool_constantvoid_tが仲間入りする


一覧の見方

テンプレート宣言は省略。

かわりにテンプレート仮引数にclassなどの修飾をする。

例えば、

template < class T, T value >

class integral_constant;

integral_constant<class T, T value>

と表記する。


ヘルパークラス



integer_constant<class T, T value>

T型の整数定数を表す型

true_type

trueを表す型

false_type



falseを表す型

bool_constant<bool B>(Since C++17)

bool型の整数定数を表す型


基本的な型



is_void<class T>

T型が void であるかどうかをテストします

is_nullptr<class T>(since C++14)

T型が nullptr_t であるかどうかをテストします

is_integral<class T>

T型が整数型であるかどうかをテストします

is_floating_point<class T>

T型が浮動小数点型であるかどうかをテストします/



is_array<class T>

T型が配列型であるかどうかをテストします

is_pointer<class T>

T型がポインターであるかどうかをテストします

is_lvalue_reference<class T>

T型が lvalue 参照であるかどうかをテストします

is_rvalue_reference<class T>

T型が rvalue 参照であるかどうかをテストします

is_member_function_pointer<class T>

T型がメンバー関数へのポインターであるかどうかをテストします

is_member_object_pointer<class T>

T型がメンバー オブジェクトへのポインターであるかどうかをテストします

is_enum<class T>

T型が列挙型であるかどうかをテストします

is_union<class T>

T型が共用体であるかどうかをテストします

is_class<class T>

T型がクラスであるかどうかをテストします

is_function<class T>

T型が関数型であるかどうかをテストします


複合型



is_scalar<class T>

T型がスカラーであるかどうかをテストします

is_arithmetic<class T>

T型が演算型であるかどうかをテストします

is_reference<class T>

T型が参照であるかどうかをテストします

is_fundamental<class T>

T型が void または演算型であるかどうかをテストします

is_member_pointer<class T>

T型がメンバーへのポインターであるかどうかをテストします

is_object<class T>

T型がオブジェクト型であるかどうかをテストします

is_compound<class T>

T型が非スカラー(複合型)であるかどうかをテストします


型の特性



is_abstract<class T>

T型が抽象クラスであるかどうかをテストします

is_const<class T>

T型が定数型であるかどうかをテストします

is_empty<class T>

T型が空のクラスであるかどうかをテストします

is_final<class T>(since C++14)

T型にfinalが付いているかどうかをテストします

is_pod<class T>

T型が POD であるかどうかをテストします

is_literal_type<class T>(deprecated in C++17)

T型がリテラル型かどうかをテストします

is_polymorphic<class T>

T型に仮想関数が存在するかどうかをテストします

is_signed<class T>

T型が符号付き整数であるかどうかをテストします

is_standard_layout<class T>

T型が標準レイアウト型であるかどうかをテストします

is_unsigned<class T>

T型が符号なし整数であるかどうかをテストします

Tis_volatile<class T>

型が volatile であるかどうかをテストします


型のサポートするオペレーション


型が特定のオペレーションをサポートしているかをテストする

特にコンストラクト、コピー、ムーブ、デストラクトの4種類のオペレーションについては

通常版

トリビアル版

無例外保証版

の3種類がある


is_constructible<class T, class... Args>

is_trivially_constructible<class T, class... Args>

is_nothrow_constructible<class T, class... Args>

指定された初期化子型リスト(Args...)からT型がコンストラクトできるかをテストする

is_default_constructible<class T>

is_trivially_default_constructible<class T>

is_nothrow_default_constructible<class T>

T型がデフォルトコンストラクトできるかをテストする

is_copy_constructible<class T>

is_trivially_copy_constructible<class T>

is_nothrow_copy_constructible<class T>

T型がコピーコンストラクトできるかをテストする

is_move_constructible<class T>

is_trivially_move_constructible<class T>

is_nothrow_move_constructible<class T>

T型がムーブコンストラクトできるかをテストする

is_assignable<class T, class U>

is_trivially_assignable<class T, class U>

is_nothrow_assignable<class T, class U>

T型にU型が代入演可能かどうかをテストする

is_copy_assignable<class T>

is_trivially_copy_assignable<class T>

is_nothrow_copy_assignable<class T>

T型がコピー代入演可能かどうかをテストする

is_destructible<class T>

is_trivially_destructible<class T>

is_nothrow_destructible<class T>

T型が明示的にdeleted宣言されていないデストラクタをもつかテストする

has_virtual_destructor<class T>

T型がvirtualなデストラクタを持つかテストする

is_swappable_with<class T, class U>(since C++17)

T型がU型とスワップできるかをテストします

is_swappable<class T>(since C++17)

T型が同じ型とスワップできるかをテストします

is_nothrow_swappable_with<class T, class U>(since C++17)

T型がU型と無例外保証付きでスワップできるかをテストします

is_nothrow_swappable<class T>(since C++17)

T型が同じ型と無例外保証付きでスワップできるかをテストします

has_unique_object_representations<class T>(since C++17)

オブジェクトから自動的にハッシュを計算できるかをテストする(C++17では自動ハッシュ計算をサポートしないが、その前段階として入った機能である)


型の特性についての問い合わせ



alignment_of<class T>

T型のアライメントを取得します

rank<class T>

配列の次元数を取得します

extent<class T, size_t I = 0>

配列のI番目の次元を取得します


型の関係



is_same<class T, class U>

2 つの型が等しいかどうかをテストします

is_base_of<class Base, class Derrived>

Base型がDerrived型の基本クラスであるかどうかをテストします

is_convertible<class From, class To>

From型をTo型に変換できるかどうかをテストします

is_callable<F(Args...)>(since C++17)

関数が呼び出せるかどうかをテストします

is_nothrow_callable<F(Args...)>(since C++17)

関数が無例外保証付きで呼び出せるかどうかテストします


const/volatileの変更



add_const<class T>

T型から const T型を作成します

add_volatile<class T>

T型から volatile T型を作成します

add_cv<class T>

型から const volatile 型を作成します

remove_const<class T>

型から非 const 型を作成します

remove_volatile<class T>

型から非 volatile 型を作成します

remove_cv<class T>

型から非 const volatile 型を作成します


referenceの変更



add_lvalue_reference<class T>

T型からT型への左辺値参照(T&)を作成します

add_rvalue_reference<class T>

T型からT型への右辺値参照(T&&)を作成します

remove_reference<class T>

型から非参照型を作成します


配列の変更



remove_extent<class T>

配列型から次元を取り除き要素型を作成します

remove_all_extents<class T>

配列型からすべての次元を取り除き非配列型を作成します


ポインタの変更



add_pointer<class T>

T型から型へのポインタ(T*)を作成します

remove_pointer<class T>

型へのポインタから型を作成します


符号の変更



make_signed<class T>

符号付きの型を作成します

make_unsigned<class T>

符号なしの型を作成します


その他の変更



aligned_storage<size_t Len, size_t Align = -default-align>

アライメント調整された領域を作成します

aligned_union<size_t Len, class... Types>

アライメント調整された共用体領域を作成します

common_type<class... Typesgt;

2つ以上の型の型変換可能な共通型のインスタンスを作成します

conditional<bool B, class T, class F>

コンパイル時条件式, BがtrueならT, false ならFをtypeに持つ

decay<class T>

非参照、非定数、非揮発の型、または型へのポインターを作成します
(配列と関数を通常のテンプレート型推論と同様に推論する場合にも用いる)

enable_if<bool B, class T = void>

条件が真の場合有効な型Tに実体化します

underlying_type<class E>

enumの基底の型を作成します

result_of<f(Args...)>

関数の戻り値の型を作成します

void_t<class... Dummy>(since C++17)

テンプレート引数に何を与えてもvoidに実体化します


コンパイル時条件の論理演算(since C++17)



conjunction<class... B>(since C++17)

コンパイル時条件の可変長AND演算を行います

disjunction<class... B>(since C++17)

コンパイル時条件の可変長OR演算を行います

negation<class B>(since C++17)

コンパイル時条件のNOT演算を行います


エイリアステンプレート

型から型を作成するメタ関数は

typename add_const<T>::type

のようにしてネストされた型を取り出すが

面倒なのでエイリアステンプレートが定義されている

以下のようになっている

template < typename T >

using add_const_t = typename add_const<T>::type ;

typename::type が両方とも省ける!

add_const_t<T>などと書ける

やったぜ!


変数テンプレート

constexprVariable template 導入によっては型情報をbool値で返す

メタ関数を

template < typename T, typename U >

constexpr bool is_same_v = is_same<T,U>::value ;

と記述できるようになった

これで

is_same_v<T,U>などと書ける

ちょい便利です。

この機能はC++17から標準ライブラリに入ることになっている


神でもわかるメタ関数の作り方

C++11以前ではsizeofを悪用した一見意味の分からないコードで

メタ関数を記述していた

しかし、C++11からdecltypeが入り

declvalが入り

とにかく簡単にかけるようになった


C++11以前のメタ関数

boost::is_convertibleの実装である

基本的にsizeofとオーバーロード解決の順序を使う

template <typename From, typename To>

struct is_convertible_basic_impl
{
// 2 つの同名関数を作って
static no_type _m_check(...);
static yes_type _m_check(To);

// 関数の戻り値の型を見る(どっちの関数が使われるかを見る)
static bool value = sizeof( _m_check(From) ) == sizeof(yes_type);
};


C++11以降のメタ関数

SFINAE という強力な武器を得た

これは、Substitution Failure Is Not An Errorの略語である

(テンプレートの)実体化の失敗はエラーではない、という意味である

と言ってもわからないだろうから、例を出して説明する

template < typename T >

typename T::value_type func(T);

上の関数の戻り値にtypename T::value_typeという型が用いられている

引数はTであるから、あらゆる型を推論できるがvalue_typeというメンバ型名を持っていない場合にはどうなるのだろう?

ここで SFINAE が発動する

テンプレートの実体化に失敗した場合は即座にエラーにせず、他の関数が一致しないかを探すのである

このSFIANEはdecltypeの文脈でも適用される

template < typename L, typename R >

auto plus(L&& l, R&& r) -> decltype(l+r)
{
return l + r;
}

戻り値の後置宣言を用いて、decltype(任意の式)とすることで、任意の式が有効な場合に実体化に成功し、無効の場合実体化に成功するという関数になる

ただし重要なことは、実体化に成功する関数が同時に複数あるとオーバーロードの解決に失敗するため、適切に実体化を失敗させるテクニックが必要であり、どう考えても玄人しか使えないという点である。

C++11以降のメタ関数ではこれらを駆使する。


特定のクラスかどうかを判定する

クラスの完全な特殊化を用いる

template < typename T >

struct is_int {
static constexpr bool value = false ;
}
template < >
struct is_int<int> {
static constexpr bool value = true ;
}


複合型の判定(ポインタ型の判定など)

ポインタ型の判定をしたいときはクラスの部分的特殊化を利用する

template < typename T >

struct is_pointer {
static constexpr bool value = false ;
}
template < typename T >
struct is_int<T*> {
static constexpr bool value = true ;
}

これだけならstd::is_pointer<T>を使えばいい

部分的特殊化を使う場合は判定したいクラスがテンプレートになっているときだ

std::vector<T>かどうかを判定するメタ関数を書きたいとする

このときTはなんでもかまわないので部分的特殊化が使える

template < typename T >

struct is_vector : std::false_type{};

template < typename T >
struct is_vector<std::vector<T>> : std::true_type {};

template < typename T >
constexpr bool is_vector_v = is_vector<T>::value;

また、std::tupleかどうかを判定したい場合はtupleが可変長テンプレートなので、可変長テンプレートで部分的特殊化を用いる

template < typename T >

struct is_tuple : std::false_type{};

template < typename ...Types >
struct is_tuple<std::tuple<Types...>> : std::true_type {};

template < typename T >
constexpr bool is_tuple_v = is_tuple<T>::value;


メンバ型名を持っているか判定する

関数のオーバーロード解決(SFINAE)を利用する

クラスがiteratorを持っているかどうかを判定したいときは

template <class T>

class has_iterator {
template <class U>
static constexpr bool check(typename U::iterator*)
{ return true; }

template <class U>
static constexpr bool check(...)
{ return false; }
public:
static constexpr bool value = check<T>(nullptr);
};

上の関数ではSFINAEのトリックを使っている

Uiteratorメンバを持っていない場合

実体化に失敗しオーバーロード解決の候補から外されて

下の関数が呼ばれることになる

関数に定義はもはや必要ではない

われわれにはdecltypeがある

そして、ヘルパークラスの

std::true_typestd::false_typeがある

template <class T>

class has_iterator {
template <class U>
static constexpr std::true_type check(typename U::iterator*);

template <class U>
static constexpr std::false_type check(...);

public:
static constexpr bool value = decltype(check<T>(nullptr))::value;
};

ちなみにこのSFINAEのトリックをテンプレートのデフォルト引数にすることもできる

オーバーロードが優先されれば引数はなんでも良いので

例えばintlongでもよい

template <class T>

class has_iterator {
template <class U, typename O = typename U::iterator>
static constexpr std::true_type check(int);

template <class U>
static constexpr std::false_type check(long);

public:
static constexpr bool value = decltype(check<T>(0))::value;
};



ちょっと一捻りして継承を使う


struct has_iterator_impl {
template <class T>
static std::true_type check(typename T::iterator*);

template <class T>
static std::false_type check(...);
};

template <class T>
class has_iterator :
public decltype(has_iterator_impl::check<T>(nullptr)) {};


型名以外のメンバを持っているかを判定する

型名以外のメンバとは

変数と関数である


メンバ変数を持っているかを確かめたい場合

struct has_value_impl {

template <class T>
static std::true_type check(decltype(T::value)*);

template <class T>
static std::false_type check(...);
};

template <class T>
class has_value :
public decltype(has_value_impl::check<T>(nullptr)) {};



メンバ関数f()を持っているかを確かめたい場合

struct has_f_impl {

template <class T>
static auto check(T&& x)->decltype(x.f(),std::true_type{});

template <class T>
static auto check(...)->std::false_type;
};

template <class T>
class has_f :
public decltype(has_f_impl::check<T>(std::declval<T>())) {};

これはdecltypeとカンマ演算子operator,を利用したSFINAEトリックである

decltypeが引数を2つとるわけではない!

decltypeの中の式はカンマ演算子によって左から右に順次評価され

返る値は一番右の結果になります

つまり

x.f()の呼び出しに失敗すれば

SFINAEによってオーバーロード解決の候補から外され

成功すれば

std::true_type{}を評価しdecltypeの表す型はstd::true_typeになります



カンマ演算子

コンマ演算子の結合規則は、左から右方向です。コンマで区切られた 2 つの式は左から右に評価されます。左オペランドは常に評価され、右オペランドが評価される前にすべての副作用が完了します。

コンマは、関数の引数リストなどの一部のコンテキストで、区切り記号として使用できます。区切り記号としてのコンマの使用と演算子としての使用を混同しないでください。この 2 つの用途は、まったく別のものです。

次の式を考えます。

e1 , e2

この式の型と値は、e2 の型と値です。e1 を評価した結果は破棄されます。結果は、右オペランドが左辺値の場合は左辺値です。

通常、コンマが区切り記号として使用される場所 (たとえば、関数の実引数や集約の初期化子) では、コンマ演算子とそのオペランドをかっこで囲む必要があります。 たとえば、

func_1( x, y + 2, z );

func_2( (x--, y + 2), z );

上の func_1 の関数呼び出しでは、x、y + 2、z という 3 つの引数がコンマで区切られて渡されます。 func_2 の関数呼び出しでは、かっこにより、コンパイラは順次評価演算子として最初のコンマを解釈します。この関数呼び出しは、func_2 に 2 つの引数を渡します。最初の引数は、順次評価演算 (x--, y + 2) の結果です。この演算は、式 y + 2 の値と型を持ち、第 2 の引数は z です。



関数value()と変数valueのどちらかを持っているかを確かめたい場合

クラスが持つメンバは型、関数、変数である

この内で型にはなく、関数と変数に共通してもつ性質を利用する

すなわち、ポインタを持つという性質を利用する

struct has_value_impl {

template <class T>
static std::true_type check(decltype(&T::value));

template <class T>
static std::false_type check(...);
};

template <class T>
class has_value :
public decltype(has_value_impl::check<T>(nullptr)) {};


型の特性

Tが代入可能かどうかを確かめたい場合

struct is_assignable_impl {

template <class T>
static auto check(T&& x, T const& y) -> decltype(x=y,std::true_type{});

static auto check(...) -> std::false_type;
};

template <class T>
struct is_assignable
: decltype(is_assignable_impl::check(std::declval<T>(),std::declval<T>())) {};


declval

ここでstd::declval<T>()なるものが出てきました

式が有効になるかどうかはdecltypeな中に式を記述する

SFINAEトリックを用いれば良いのでした

ただ、式を記述するには変数が必要です

型の特性を調べるのはコンパイル時なので変数を作ることはできません

しかし、decltypeのなかでは本当の変数は必要ないのです

template < typename T >

std::add_rvalue_reference<T> value() ;

のような関数を作れば返り値をdecltypeの中で変数として用いることができます

これこそdeclvalのやっていることです

declvaldecltypeの中でのみ用いることができる変数を作る関数です


C++1z時代のメタ関数 [ 追記(2016-9-17)]

C++も順調に進化して、メタ関数の書き方も変遷している。

C++1zに採択されたvoid_tについて追記する。


始まりはここから(適当

template<typename T, typename = void>

struct is_equality_comparable : std::false_type
{};

template<typename T>
struct is_equality_comparable<T,
typename std::enable_if<
true,
decltype(std::declval<T&>() == std::declval<T&>(), (void)0)
>::type
> : std::true_type
{};

Toperator==で比較できるかを調べるメタ関数である。

特殊化された下のクラスはstd::enable_ifの条件を常に真にしてdecltypeの中で条件を記述する。そういておいて、条件が恙無く評価できればカンマ演算子でvoidに推論させている。

明らかにstd::enable_ifの使い方を間違っている。

実体化に成功したら常にvoidになる型がアレばいいのではないか?

そう、そこでvoid_tの爆誕である。

template < typename ... >

using void_t = void;

void_tにどのような仮引数を渡してもvoidに実体化する。

これを用いて、先のis_equality_comparableを書き直そう。

template< class, class=void >

struct is_equality_comparable : std::false_type
{};

template< class T >
struct is_equality_comparable<T,
void_t<decltype(std::declval<T&>() == std::declval<T&>() )>
> : std::true_type
{};

基底となるクラスのテンプレート宣言は

template <class,class=void>

とし、std::false_typeを継承する。

次に特殊化されたクラスを作る。

テンプレート宣言を

template <class T>

とする。

ここでvoid_tの出番である。

特殊化の引数の1つめはTにしておき、2つめをvoid_tにする。

void_tのテンプレート引数にdecltypeを指定し、その中に確かめたいことを書く。

struct is_equality_comparable<T,

void_t<decltype(std::declval<T&>() == std::declval<T&>() )>

今回はoperator==で比較できるかを書いたが、この手法で特定のメンバを持っているかや、特定の操作が可能かどうかをコンパイル時に確かめることができる。

Detection Idiomと呼ばれるテクニックだ。

もっと詳しく詳しく知りたいなら以下を参考にすれば良いだろう。

Faith and Brave

yohhoyの日記

N3911 TransformationTrait Alias void_t

以下はC++1zには採択されなかったvoid_tを用いたツールキットの提案である。

なかなか便利なのだが、やり過ぎ感が漂っている。

N4502 Proposing Standard Library Support for the C++ Detection Idiom, v2


コンパイル時の条件演算


bool_constant

・条件Aか条件Bを満たす場合に真となるメタ関数

std::integral_constant<bool,value>を継承してしまいましょう。std::integral_constant<bool,value>のエイリアスstd::bool_constant<value>がつかえます(since C++17)。

例えば、Rangeであるかどうかを雑に判定するとして、T型のオブジェクトaに対して

条件A

std::begin(a), std::end(a)

が可能

もしくは

条件B

begin(a), end(a)

が可能であれば良い。

そこで、この片方が可能であるかどうかを判定するメタ関数をそれぞれ書く。

最後に

template < typename T >

struct is_range : std::bool_constant<
A<T>::value || B<T>::value
>{};

のようにすれば良い。

以下、実装例。

#include <type_traits>

#include <utility>

namespace cranberries_magic{

template < class, class=void >
struct enable_std_begin_end : std::false_type {};

template < typename T >
struct enable_std_begin_end<T,
std::void_t<decltype( std::begin(std::declval<const T&>()),std::end(std::declval<const T&>()) )>>
: std::true_type {};

template < class, class=void >
struct enable_adl_begin_end : std::false_type {};

template < typename T >
struct enable_adl_begin_end<T,
std::void_t<decltype( begin(std::declval<const T&>()),end(std::declval<const T&>()) )>>
: std::true_type {};

} // ! namespace cranberries_magic

template < typename T >
struct is_range
: std::bool_constant<
cranberries_magic::enable_std_begin_end<T>::value
|| cranberries_magic::enable_adl_begin_end<T>::value>
{};

template < typename T >
constexpr bool is_range_v = is_range<T>::value;


enable_if_tを使った場合

is_iteratorを書いてみた例。

iterator_tagとしてinput_iterator_tagもしくは、output_iterator_tagを継承していればiteratorであるはずという、雑な判定である。

bool_constantとの違いはクラスが2つに別れているところだ。

  template < class, class=void >

struct is_iterator : std::false_type {};

template < typename T >
struct is_iterator<T,
std::enable_if_t<
std::is_base_of<std::input_iterator_tag, typename std::iterator_traits<T>::iterator_category>::value
|| std::is_base_of<std::output_iterator_tag, typename std::iterator_traits<T>::iterator_category>::value
>
> : std::true_type
{};


コンパイル時条件の論理演算 [ 追記(2017-2-23)]

C++17から、コンパイル時条件(メタ関数の真偽)を演算した結果を返してくれるメタ関数が登場!

AND演算する可変長テンプレート conjunction

OR演算する可変長テンプレート disjunction

NOT演算する negation

の3種類がある

ナンデこんなにややこしい名前なのかと思われたかもしれないが、

C++ではand, or, notは予約語であり、使えないのだ

もともとboost MPLにあるand_, or_, not_と同じ名前で提案されていたが、こうなってしまったのだ

conjunction, disjunction は渡したメタ関数のvalueをAND(OR)した結果を返してくれる可変長テンプレートになっている

negation は渡したメタ関数のvalueを否定した結果を返してくれる

// 全ての条件がtrueなら結果がtrueとなる

constexpr bool result1 = std::conjunction_v<
std::true_type,
std::is_void<void>,
std::is_same<int,int>
>;

// 条件を否定する
constexpr bool result2 = std::negation_v<std::true_type>; // false

これを使えば、上例のis_rangeを以下のように書ける

直接disjunctionを継承して、条件を羅列すれば良い

かなり楽になってきた感じ

template < typename T >

struct is_range
: std::disjunction<
detail::enable_std_begin_end<T>,
detail::enable_adl_begin_end<T>>
{};

is_iterator も書き換えてみよう

template < typename T >

struct is_iterator : std::disjunction<
std::is_base_of<std::input_iterator_tag, typename std::iterator_traits<T>::iterator_category>,
std::is_base_of<std::output_iterator_tag, typename std::iterator_traits<T>::iterator_category>>
{};

複数のコンパイル時条件から新しいコンパイル時条件をつくるならこれだなって感じですね


C++14 Generic Lambdaパワーで汎用的 has_xxx [ 追記(2017-9-03)]

特定のメンバ名ではなく、あとからいくらでも名前を指定できたら便利ですよね。

実は、Boostにあるんだが、ちょっとちがった方法を紹介する。

言うまでもないが、マクロである。

まず、全貌は以下。

#include <type_traits>

#include <utility>
namespace cranberries {
struct protean_bool {
constexpr operator std::true_type() const { return{}; }
constexpr operator std::false_type() const { return{}; }
};

template <class T, class... Ts>
struct Overload : T, Overload<Ts...> {
Overload(T a, Ts... xs): T{a}, Overload<Ts...>{xs...} {}
using T::operator();
using Overload<Ts...>::operator();
};

template <class T> struct Overload<T> : T {
Overload(T a): T{a} {}
using T::operator();
};

template <class... F>
inline constexpr Overload<F...>
make_overload(F&&... f)
{
return {{std::forward<F>(f)}...};
}
template <class T> struct type_placeholder{ using type = T; };
}
#define CRANBERRIES_HAS_TYPE(XXX, ...) \
bool(false \
? ::cranberries::make_overload( \
[](auto x)->decltype(std::declval<typename decltype(x)::type::XXX>(), std::true_type{}) {return{}; }, \
[](...)->std::false_type {return{}; } \
)(::cranberries::type_placeholder<__VA_ARGS__>{}) \
: ::cranberries::protean_bool{})
int main()
{
static_assert(CRANBERRIES_HAS_TYPE(value_type, std::true_type),"");
}

これは、以下のように直接使える。


static_assert(
CRANBERRIES_HAS_TYPE(value_type, std::true_type), // true
"");

仕組みを解説する。

コンパイル時に型特性を調べるにはSFINAEを使ってオーバーロードが成功するかを調べる。

まず、オーバーロードされた都合のいい関数をその場で作りたい。

そこで、以下のようなoperator()をusingしたクラスを用意する。

コンストラクタがないとgccに怒られる。

  template <class T, class... Ts>

struct Overload : T, Overload<Ts...> {
Overload(T a, Ts... xs): T{a}, Overload<Ts...>{xs...} {}
using T::operator();
using Overload<Ts...>::operator();
};

template <class T> struct Overload<T> : T {
Overload(T a): T{a} {}
using T::operator();
};

make_overload()に関数を渡すと、オーバーロードされた関数のように振る舞う。

これにジェネリックラムダを渡してその場でオーバーロードされた関数を作ってしまう。

以下のようにすると、引数から型をdecltype(x)で取得しXXXという型メンバがあるかを確かめられる。

__VA_ARGS__で展開しているのはテンプレートの場合クラスにカンマが使われる可能性があるため。

また、クラスがdefault constructibleかどうかも定かではないため、

type_placeholderというdefault constructibleな型にクラスを埋め込みラムダ式に引数として渡して推論させる。

引数xをdecltypeして依存型名typeから目的の型を取り出す。

cranberries::make_overload(

[](auto x)->decltype(std::declval<typename decltype(x)::type::XXX>(), std::true_type{}) {return{}; },
[](...)->std::false_type {return{}; }
)(::cranberries::type_placeholder<__VA_ARGS__>{})

ただし、この方法は重大な欠点があって、ジェネリックラムダを使っているせいで未評価オペランドの文脈で使用できない。

decltypeで型を取得できないので以下ができない。

// ラムダ式は未評価式で使えないのでこれはダメ!

decltype(
cranberries::make_overload(
[](auto x)->decltype(std::declval<typename decltype(x)::type::XXX>(), std::true_type{}) {return{}; },
[](...)->std::false_type {return{}; }
)(::cranberries::type_placeholder<SOME_TYPE>())
)::value

decltypeが封じられたのでもう少し工夫する必要がある。

僕の大好きな条件演算子を使うことにする。

下準備としてprotean_boolクラスを作っておく(ポケモンを英語プレイしていたら分かるだろうが、proteanとは変幻自在という意味)。

  struct protean_bool {

constexpr operator std::true_type() const { return{}; }
constexpr operator std::false_type() const { return{}; }
};

次のように第3オペランドを常に戻り値とする条件演算子を書く。

false ? make_overload(...)(x) : cranberries::protean_bool{};

この条件演算子の戻り値型はmake_overloadの呼び出しの結果によって変わる。

ラムダ式のdecltypeがメンバの引数xに対してdetectionに成功するとstd::true_typeを返すラムダ式がオーバーロードに成功し呼ばれる。

失敗すれば、std::false_typeを返すラムダ式がオーバーロードに成功し呼ばれる。

条件式はfalseなので、protean_boolが戻り値になる。

もし、make_overloadの戻り値型がstd::false_typeならprotean_boolはstd::false_typeに暗黙に変換される。

もし、make_overloadの戻り値型がstd::true_typeならprotean_boolはstd::true_typeに暗黙に変換される。

最終的にstd::true_typeかstd::false_typeのoperator bool()によってtrueかfalseに変換されbool値が手に入る。


跋文

参考になりましたでしょうか?

メタ関数で有意義なTMPライフを満喫しましょう!

間違いや質問、ご意見等は

コメントしていただくか

いなむ先生 | Twitter

までご一報ください

できるだけすみやかに対応させていただきます


All text is available under the CC0 1.0 Universal license.