249
199

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C++メタ関数のまとめ

Last updated at Posted at 2015-12-29

#メタ関数のまとめ

##メタ関数とは?
メタ関数とは、コンパイル時に型情報を取得できる関数のようなもの
実際には、関数の形を取らず、クラスになっていることがほとんどだが
関数にすることもできる

例えば型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.

249
199
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
249
199

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?