C++メタ関数のまとめ

  • 86
    いいね
  • 7
    コメント

メタ関数のまとめ

メタ関数とは?

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

例えば型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が仲間入りする

ヘルパークラス


integer_constant
整数の定数を表す型
true_type
trueを表す型(typedef)
false_type
falseを表す型(typedef)

基本的な型


is_void
型が void であるかどうかをテストします
is_nullptr(since C++14)
型が nullptr_t であるかどうかをテストします
is_integral
型が整数型であるかどうかをテストします
is_floating_point
型が浮動小数点型であるかどうかをテストします/
is_array
型が配列型であるかどうかをテストします
is_pointer
型がポインターであるかどうかをテストします
is_lvalue_reference
型が lvalue 参照であるかどうかをテストします
is_rvalue_reference
型が rvalue 参照であるかどうかをテストします
is_member_function_pointer
型がメンバー関数へのポインターであるかどうかをテストします
is_member_object_pointer
型がメンバー オブジェクトへのポインターであるかどうかをテストします
is_enum
型が列挙型であるかどうかをテストします
is_union
型が共用体であるかどうかをテストします
is_class
型がクラスであるかどうかをテストします
is_function
型が関数型であるかどうかをテストします

複合型


is_scalar
型がスカラーであるかどうかをテストします
is_arithmetic
型が演算型であるかどうかをテストします
is_reference
型が参照であるかどうかをテストします
is_fundamental
型が void または演算型であるかどうかをテストします
is_member_pointer
型がメンバーへのポインターであるかどうかをテストします
is_object
型がオブジェクト型であるかどうかをテストします
is_compound
型が非スカラー(複合型)であるかどうかをテストします

型の特性


is_abstract
型が抽象クラスであるかどうかをテストします
is_const
型が定数型であるかどうかをテストします
is_empty
型が空のクラスであるかどうかをテストします
is_final(since C++14)
型にfinalが付いているかどうかをテストします
is_pod
型が POD であるかどうかをテストします
is_literal_type(deprecated in C++17)
型がリテラル型かどうかをテストします
is_polymorphic
型に仮想関数が存在するかどうかをテストします
is_signed
型が符号付き整数であるかどうかをテストします
is_standard_layout
型が標準レイアウト型であるかどうかをテストします
is_unsigned
型が符号なし整数であるかどうかをテストします
is_volatile
型が volatile であるかどうかをテストします

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


型が特定のオペレーションをサポートしているかをテストする
特にコンストラクト、コピー、ムーブ、デストラクトの4種類のオペレーションについては

通常版
トリビアル版
無例外保証版

の3種類がある

is_constructible
is_trivially_constructible
is_nothrow_constructible
指定された初期化子型リストから型がコンストラクトできるかをテストする
is_default_constructible
is_trivially_default_constructible
is_nothrow_default_constructible
デフォルトコンストラクトできるかをテストする
is_copy_constructible
is_trivially_copy_constructible
is_nothrow_copy_constructible
コピーコンストラクトできるかをテストする
is_move_constructible
is_trivially_move_constructible
is_nothrow_move_constructible
ムーブコンストラクトできるかをテストする
is_assignable
is_trivially_assignable
is_nothrow_assignable
指定された型が代入演可能かどうかをテストする
is_copy_assignable
is_trivially_copy_assignable
is_nothrow_copy_assignable
あるが型コピー代入演可能かどうかをテストする
is_destructible
is_trivially_destructible
is_nothrow_destructible
明示的にdeleted宣言されていないデストラクタをもつかテストする
has_virtual_destructor
virtualなデストラクタを持つかテストする
is_swappable_with(since C++17)
型が他の型とスワップできるかをテストします
is_swappable(since C++17)
型が同じ型とスワップできるかをテストします
is_nothrow_swappable_with(since C++17)
型が他の型と無例外保証付きでスワップできるかをテストします
is_nothrow_swappable(since C++17)
型が同じ型と無例外保証付きでスワップできるかをテストします
has_unique_object_representations(since C++17)
オブジェクトから自動的にハッシュを計算できるかをテストする(C++17では自動ハッシュ計算をサポートしないが、その前段階として入った機能です)

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


alignment_of
型のアライメントを取得します
rank
配列の次元数を取得します
extent
配列の次元を取得します

型の関係


is_same
2 つの型が等しいかどうかをテストします
is_base_of
一方の型がもう一方の型の基本クラスであるかどうかをテストします
is_convertible
一方の型をもう一方の型に変換できるかどうかをテストします
is_callable(since C++17)
関数が呼び出せるかどうかをテストします
is_nothrow_callable(since C++17)
関数が無例外保証付きで呼び出せるかどうかテストします

const/volatileの変更


add_const
型から const 型を作成します
add_volatile
型から volatile 型を作成します
add_cv
型から const volatile 型を作成します
remove_const
型から非 const 型を作成します
remove_volatile
型から非 volatile 型を作成します
remove_cv
型から非 const volatile 型を作成します

referenceの変更


add_lvalue_reference
型から型への左辺値参照を作成します
add_rvalue_reference
型から型への右辺値参照を作成します
remove_reference
型から非参照型を作成します

配列の変更


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

ポインタの変更


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

符号の変更


make_signed
サイズが型以上の型または最小の符号付きの型を作成します
make_unsigned
サイズが型以上の型または最小の符号なしの型を作成します

その他の変更


aligned_storage
アライメント調整された領域を作成します
aligned_union
アライメント調整された共用体領域を作成します
common_type
2 つの型の型変換可能な共通型のインスタンスを作成します
conditional
コンパイル時条件式
decay
非参照、非定数、非揮発の型、または型へのポインターを作成します
(配列と関数を通常のテンプレート型推論と同様に推論する場合に用いる)
enable_if
条件が真の場合有効な型を作成します
underlying_type
enumの基底の型を作成します
result_of
関数の戻り値の型を作成します
void_t(since C++17)
voidの可変長エイリアステンプレートです

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


conjunction(since C++17)
コンパイル時条件の可変長AND演算を行います
disjunction(since C++17)
コンパイル時条件の可変長OR演算を行います
negation(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 A 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>>
{};

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

跋文

参考になりましたでしょうか?
メタ関数で有意義なTMPライフを満喫しましょう!
間違いや質問、ご意見等は
コメントしていただくか
いなむ先生 | Twitter
までご一報ください
できるだけすみやかに対応させていただきます


All text is available under the CC0 1.0 Universal license.