std::enable_ifを使ってオーバーロードする時、enablerを使う?

  • 25
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

本の虫:C++0xにおけるenable_ifの新しい使い方

今回のネタ元は言わずと知れたC++erの江添氏のブログからですが、テンプレート引数の型の特性によってオーバーロードする方法として、enablerという変数を使う方法が紹介されています。
実際、以前の記事でもこんな感じで活用しています。

c++
extern void* enabler;
template<bool condition, typename T = void>
using enable_if_type = typename std::enable_if<condition, T>::type;

// 符号なし整数型を受け取るオーバーロード
template<typename T, enable_if_type<std::is_unsigned<T>{}>*& = enabler>
inline constexpr int ntz(T val) noexcept;

// 符号付き整数型を受け取るオーバーロード
template<typename T, enable_if_type<std::is_signed<T>{}>*& = enabler>
inline constexpr int ntz(T val) noexcept;

// 列挙型を受け取るオーバーロード
template<typename T, enable_if_type<std::is_enum<T>{}>*& = enabler>
inline constexpr int ntz(T val) noexcept;

さて、上記のブログでenablerを受け取る型をintなどではなくvoid*&にしなければならない理由が説明されています。

しかし、なぜvoidへのポインターへのリファレンスなのか。以下の形ではだめなのか。

std::enable_if<条件, int>::type = 0

これは動くが、ユーザーが誤ってテンプレート実引数を明示的に渡してしまうかもしれないというおそれがある。その点、void &&という型は、まず現実に使われないので、安全である。およそ、void *&が一体どういう型であるかを理解できるようなユーザーであれば、このenable_ifのテクニックも理解できるほどのC++上級者なので、やはり間違えることはない。

途中void &&になっていますがvoid *&のことだと思います。
要するに、こういうことでしょう。

c++
template<typename T, enable_if_type<std::is_unsigned<T>{}, int> = 0>
inline constexpr int ntz(T val) noexcept;

ntz(10u); //(1)
ntz<unsigned int, 1>(10u); //(2)

//(1)と(2)は別の実体化

まあ、この例であれば、別の実体化をされたところで結果は変わらないのですが、確かにこれはちょっといただけません。場合によっては(関数内部でstatic変数が定義されている場合など)動作もおかしなことになる可能性があります。
ですが、void*&だってenabler以外の何かが渡される可能性は0ではないのです。

c++
extern void* enabler;
extern void* other_enabler;
template<typename T, enable_if_type<std::is_unsigned<T>{}>*& = enabler>
inline constexpr int ntz(T val) noexcept;

ntz(10u); //(1)
ntz<unsigned int, other_enabler>(10u); //(2)

//(1)と(2)は別の実体化

もちろん江添氏が言及している通り、間違える可能性はほぼないとは思いますが。

調べていたら、こんな意見も見つけました
野良C++erの雑記帳: C++0x の SFINAE で気づいたこと
void*&ではなくvoid*を使えばいいじゃない、という記事です。
確かにこれでも問題なさそうに見えるのですが、しかしよく考えたらまだ少しだけ危険です。

c++
template<typename T, enable_if_type<std::is_unsigned<T>{}>* = nullptr>
inline constexpr int ntz(T val) noexcept;

ntz(10u); //(1)
ntz<unsigned int, (void*)1>(10u); //(2)

//(1)と(2)は別の実体化

明示的なテンプレート実引数を渡されてしまう危険性は、完全には排除できていません。
もっとも、ここまでして明示的なテンプレート実引数を渡そうとする人もいないとは思いますが……。

でもよく考えたらこれって、別の値が渡せる型を使っているからそういう事を考えなければならないのです。
つまり、その型の表せる値が一種類であれば、問題はありません。
そしておあつらえ向きに、C++11からコア言語入りした、「値が一種類しかない型」というものが存在します。
要するに、std::nullptr_tを使えばいいじゃない、ということです。

c++
template<typename T, enable_if_type<std::is_unsigned<T>{}, std::nullptr_t> = nullptr>
inline constexpr int ntz(T val) noexcept;

ntz(10u); //(1)
ntz<unsigned int, nullptr>(10u); //(2)

constexpr std::nullptr_t x = nullptr;
ntz<unsigned int, x>(10u); //(3)

//(1)と(2)と(3)は同じ実体化

std::nullptr_tに渡せる値はどうあがいてもnullptrだけなので、例えテンプレート実引数を明示的に渡されても、実体化されたテンプレートは一つだけになります。
こうすればそもそもenablerを用意する必要もありません。

もともと2011年の記事なので、もしかしたらこれって今更感あふれるネタだったりするのかな、とか思いながら書いてみましたが、最近のboost界隈ではどういった設計が主流なのでしょうか。
これはいけないとか、こうするといいとか、何か意見がありましたらぜひ聞かせてください。

ドワンゴは本物のプログラマを募集しているらしいです。