読者です 読者をやめる 読者になる 読者になる

【C++ Advent Calendar 2016 22日目】C++ で enable_if を使うコードのベストプラクティス

C++

[追記]

すみませんすみません、修正しました。

[本編]

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::valuetrue を返し、そうでないなら 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 でも有効なのでデフォルト引数で定義したくない方は使ってみるとよいと思います。