【C++ Advent Calendar 2016 22日目】C++ で enable_if を使うコードのベストプラクティス
[追記]
@pink_bangbi typename std::enable_if<..., std::nullptr_t>::type* = nullptr の * は余計ではありませんか。これだと nullptr_t にする意味が
— akinomyoga (@akinomyoga) 2016年12月22日
すみませんすみません、修正しました。
[本編]
C++ Advent Calendar 201622日目の記事です。
『ベストプラクティス』と書いていますが、釣りタイトルですです。誰かもっといい書き方教えてください。
最近さっぱり C++ を書いてないのでいろいろと厳しい。
あ、あと最初に書いておくと本記事はあくまでも『enable_if
の使い方』に関する記事なので SFINAE 自体にはあまり言及しません。
[enable_if とは]
enable_if
は一般的にSFINAE を利用したコンパイル条件分岐で使用するためのメタ関数になります。
SFINAE とは cpprefjp から引用すると
「SFINAE (Substitution Failure Is Not An Errorの略称、スフィネェと読む)」は、テンプレートの置き換えに失敗した際に、即時にコンパイルエラーとはせず、置き換えに失敗した関数をオーバーロード解決の候補から除外するという言語機能である。
たとえば、関数のシグニチャの一部として「typename T::value_type」が書いてあり、型Tがvalue_typeという型を持っていない場合、その関数がオーバーロード解決から除外される。これによって型が任意の機能を持っているかを、コンパイル時に判定できた。
参照:任意の式によるSFINAE - cpprefjp C++日本語リファレンス
とのこと。
例えば、関数テンプレートの呼び出しの際にテンプレート引数の型が『整数型かそうでないか』で呼び出す関数を切り分けたい場合などで SFINAE と enable_if
を組み合わせて使用します。
まあ C++ を書いてる人であれば enable_if
や SFINAE などは知っていて当然ですよね。
それでは実際に enable_if
を使ったコードを書いていきます。
[C++03 時代の enable_if
]
まず、C++03 時代の enable_if
の利用方法を書いてみましょう。
C++03 ではまだ enable_if
は標準ライブラリに入っておらず、Boost を利用します。
#include <boost/core/enable_if.hpp> #include <boost/type_traits/is_integral.hpp> template<typename T> void func(T t, typename boost::enable_if<boost::is_integral<T> >::type* = 0){ std::cout << t << "は整数だよ" << std::endl; } void func(...){ std::cout << "引数は整数じゃないよ" << std::endl; } func(10); // 10は整数だよ func('c'); // cは整数だよ func(3.14f); // 引数は整数じゃないよ
http://melpon.org/wandbox/permlink/w3oZ2lSl1aS8bbTa
C++03 時代では割とオーソドックな書き方になるかと思います。
C++03 では『関数のデフォルト引数として enable_if::type
を定義してる』のが肝になります。
また『テンプレート型の条件』には is_integral
というメタ関数を使用しています。
このメタ関数は『整数型であれば is_integral::value
が true
を返し、そうでないなら false
』という関数になります。
このメタ関数と enable_if
を組み合わせることで関数呼び出しの条件分岐を行っています。
ちなみに boost::enable_if
の場合は内部で ::value
が呼び出されるので、呼び出し側では ::value
をつける必要はないので『生の型』をそのまま enable_if
へと渡します。
[C++11 時代の enable_if
]
それでは C++03 のコードをベースに C++11 に対応していきましょう。
[std::enable_if
を使う]
C++11 では標準ライブラリに enable_if
が入ったため、Boost に依存することなく enable_if
が使用できるようになりました。
ついでに boost::is_integral
も標準ライブラリ入りしたのでそれも使用するように変更します。
typename boost::enable_if<boost::is_integral<T> >::type* = 0
↓
typename std::enable_if<std::is_integral<T>::value>::type* = 0
ここで注意なのは boost::enable_if
の場合は内部で ::value
が展開されていたんですが、std::enable_if
の場合は型ではなくて値(::value
)を渡す必要があります。
boost::enable_if
から std::enable_if
にコードを変更する場合にはこのことに注意してください。
[関数テンプレートのデフォルト引数で enable_if
を定義]
C++11 では関数テンプレートでも『テンプレート型のデフォルト引数』を定義できるようになりました。
ですので、関数の引数ではなくて『関数テンプレートのデフォルト引数』で enable_if
を定義する事が出来ます。
template< typename T, // nullptr を受け取るのがポイント typename std::enable_if<std::is_integral<T>::value, std::nullptr_t>::type = nullptr > void func(T t){ std::cout << t << "は整数だよ" << std::endl; }
基本的に関数の引数で定義してた時と同じような感じですが、0
ではなくて nullptr
をデフォルト値として渡しています。
また、enable_if
の第二引数は ::type
で返す方を指定できるため std::nullptr_t
を渡してします。
これがポイントなんですが nullptr
で受け取る理由は以下の記事を参照してください。
この時に enable_if
の第二引数に std::nullptr_t
型を渡しているのもポイントなんですが、個人的には
typename std::enable_if<std::is_integral<T>::value>::type* = nullptr
みたいに省略しても基本的には問題ないと思います。
また、他には enabler
というイディオムを利用することもあります。
// enable_if で使用するためのダミーの変数を『宣言』しておく extern void* enabler; template< typename T, // nullptr の変わりに enabler を渡す typename std::enable_if<std::is_integral<T>::value>::type*& = enabler > void func(T t){ std::cout << t << "は整数だよ" << std::endl; }
[C++11 のまとめ]
#include <utility> #include <iostream> template< typename T, typename std::enable_if<std::is_integral<T>::value, std::nullptr_t>::type = nullptr > void func(T t){ std::cout << t << "は整数だよ" << std::endl; } void func(...){ std::cout << "引数は整数じゃないよ" << std::endl; } int main(){ func(10); // 10は整数だよ func('c'); // cは整数だよ func(3.14f); // 引数は整数じゃないよ return 0; }
http://melpon.org/wandbox/permlink/gTyeZk1qGdyYogV5
[C++14 時代の enable_if
]
C++14 ではさらに enable_if
の alias templates である enable_if_t
というメタ関数が標準ライブラリに追加されました。
この enable_if_t
を使用することで typename
と ::type
を省略する事ができるようになります。
typename std::enable_if<std::is_integral<T>::value, std::nullptr_t>::type = nullptr
↓
std::enable_if_t<std::is_integral<T>::value, std::nullptr_t> = nullptr
また、alias templates 自体は C++11 で実装された機能なので、以下のように定義しておけば C++11 でも使用することが出来ます。
template< bool B, class T = void > using enable_if_t = typename enable_if<B,T>::type;
参照:std::enable_if - cppreference.com
[C++17 時代の enable_if
]
よくわからんので誰か書いて。
[まとめ]
まとめると最新版の enable_if
を使ったコードはこうじゃ。
#include <type_traits> #include <iostream> template< typename T, std::enable_if_t<std::is_integral<T>::value, std::nullptr_t> = nullptr > void func(T t){ std::cout << t << "は整数だよ" << std::endl; } void func(...){ std::cout << "引数は整数じゃないよ" << std::endl; } int main(){ func(10); // 10は整数だよ func('c'); // cは整数だよ func(3.14f); // 引数は整数じゃないよ return 0; }
http://melpon.org/wandbox/permlink/7WmFlkGJQ94yVRAU
と、言う感じで各時代の enable_if
の使い方をまとめてみました。
enable_if
を使った SFINAE はメタプログラミングをする C++er に取っては一般的なテクニックだと思いますが、結構時代によって書き方が変わってきていますね。
今回は enable_if
の記事だったのであまり SFINAE には突っ込みませんでしたが、SFINAE を使ったテクニックも時代によって変わってきているので、気になる方は調べてみるとよいと思います。
また、他に『enable_if
はもっとこうしたほうが簡単に書けるよ!』みたいなツッコミがあればぜひ教えてもらえると助かります。
あと C++17 はよくわからないので誰か書いてください。 C++17 だと constexpr if があるのでそもそも enable_if
が要らなくなるという話もある
[おまけ]
ちなみに enable_if
の第二引数には ::type
が返す型を指定することが出来ます(デフォルトでは void
型を返す。
ですので関数定義時に戻り値型が決まっているなら次のように『戻り値型』に enable_if
を定義することもできます。
template< typename T > // 戻り値型が void ならそのまま enable_if::type を戻り値型に typename std::enable_if<std::is_integral<T>::value>::type func(T t){ std::cout << t << "は整数だよ" << std::endl; } template< typename T > // 任意の型を返す場合は enable_if の第二引数に渡す typename std::enable_if<std::is_integral<T>::value, int>::type plus10(T t){ return t + 10; } template< typename T > // もちろん enable_if_t を使用して typename と ::type を省略することも std::enable_if_t<std::is_integral<T>::value, T> twice(T t){ return t + t; }
これは C++03 でも有効なのでデフォルト引数で定義したくない方は使ってみるとよいと思います。