#メタ関数のまとめ
##メタ関数とは?
メタ関数とは、コンパイル時に型情報を取得できる関数のようなもの
実際には、関数の形を取らず、クラスになっていることがほとんどだが
関数にすることもできる
例えば型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_nullptr とis_finalが仲間入りした
C++17からはis_literal_type、が非推奨になる
また、
has_unique_object_representations、
is_swappable_with、is_swappable、is_nothrow_swappable_with、is_nothrow_swappable、
is_callable、is_nothrow_callable、
conjunction、disjunction、negation、
bool_constant、void_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>
などと書ける
やったぜ!
##変数テンプレート
constexpr
と Variable 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のトリックを使っている
U
がiterator
メンバを持っていない場合
実体化に失敗しオーバーロード解決の候補から外されて
下の関数が呼ばれることになる
関数に定義はもはや必要ではない
われわれにはdecltype
がある
そして、ヘルパークラスの
std::true_type
とstd::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のトリックをテンプレートのデフォルト引数にすることもできる
オーバーロードが優先されれば引数はなんでも良いので
例えばint
とlong
でもよい
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
のやっていることです
declval
はdecltype
の中でのみ用いることができる変数を作る関数です
##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
{};
型T
がoperator==
で比較できるかを調べるメタ関数である。
特殊化された下のクラスは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.