Variadic Templates を SFINAE 用のダミー引数として使う

C++ では、ある関数に渡された引数の種類によって、実際に行う処理を切り替えたい時には、

#include <type_traits>

template< class T,
  class = typename std::enable_if<
    std::is_integral<T>::value
  >::type
>
void f( T const& x ) {
  // T が整数型の場合
}

template< class T,
  class = typename std::enable_if<
    std::is_floating_point<T>::value
  >::type
>
void f( T const& x ) {
  // T が浮動小数点数型の場合
}

template< class T,
  class = typename std::enable_if<
    std::is_class<T>::value
  >::type
>
void f( T const& x ) {
  // T がクラスの場合
}

のように SFINAE を使って書くことが出来ますが、
実際にコンパイルしてみると分かりますが、上のコードはコンパイル通りません。


これは、上記の3つの関数が全て同じシグネチャを持つためであり、
実際に SFINAE を使って上記のようなオーバーロードを用意する場合には、ダミー引数を用意して

#include <type_traits>

class dummy1_ {};
class dummy2_ {};
class dummy3_ {};

template< class T,
  class = typename std::enable_if<
    std::is_integral<T>::value
  >::type
>
void f( T const& x, dummy1_ = {} ) {
  // T が整数型の場合
}

template< class T,
  class = typename std::enable_if<
    std::is_floating_point<T>::value
  >::type
>
void f( T const& x, dummy2_ = {} ) {
  // T が浮動小数点数型の場合
}

template< class T,
  class = typename std::enable_if<
    std::is_class<T>::value
  >::type
>
void f( T const& x, dummy3_ = {} ) {
  // T がクラスの場合
}

のように書く必要があります*1が、これは正直あまり美しくないですし、

f( 1, {} ); // well-formed

のようなコードがコンパイル通ってしまうという弱点があります。


で、この弱点を何とかして解決できないか、と考えた末に、
「 Variadic Templates は長さが 0 の場合にもマッチする」という性質を活かし、
長さ 0 の Variadic Templates をダミー引数として使う、
という方法を思いついたので、ここで紹介したいと思います。


ポイントは、 Variadic Templates が余分な引数を吸収しないよう、
SFINAE を使って、 Variadic Templates の長さが 0 であることを要求することだけです。
具体的には、このように書きます:

#include <type_traits>

template< class T, class... Args,
  class = typename std::enable_if<
    ( sizeof...(Args) == 0 ) &&
    std::is_integral<T>::value
  >::type
>
void f( T const& x, Args... ) {
  // T が整数型の場合
}

template< class T, class... Args,
  class = typename std::enable_if<
    ( sizeof...(Args) == 0 ) &&
    std::is_floating_point<T>::value
  >::type
>
void f( T const& x, Args*... ) {
  // T が浮動小数点数型の場合
}

template< class T, class... Args,
  class = typename std::enable_if<
    ( sizeof...(Args) == 0 ) &&
    std::is_class<T>::value
  >::type
>
void f( T const& x, Args**... ) {
  // T がクラスの場合
}

ダミー引数の部分は、他の関数と区別出来れば何でもよいので、
Args...Args*... だけでなく、 Args&...Args&&...Args const&... でも問題ないですが、
* は何回でも付けられるという利点があるので、特に理由がないなら * でいいでしょう。


というわけで、このテク、気に入ったら使ってみるといいんじゃないでしょうか。
ちょっとトリッキーですが、そもそもダミー引数自体が最初からトリッキーですしね。

追記( 22:50, 03/05 )

ダミー引数を使った SFINAE は、実はダミー引数を三種類用意しなくても、

#include <type_traits>

class dummy_ {};

template<class T>
void f( T const& x,
  typename std::enable_if<
    std::is_integral<T>::value, dummy_
  >::type = {}
){
  // T が整数型の場合
}

template<class T>
void f( T const& x,
  typename std::enable_if<
    std::is_floating_point<T>::value, dummy_
  >::type = {}
){
  // T が浮動小数点数型の場合
}

template<class T>
void f( T const& x,
  typename std::enable_if<
    std::is_class<T>::value, dummy_
  >::type = {}
){
  // T がクラスの場合
}

と書くことができ、この場合は( = {};= dummy_() に書き換え、 Boost.TypeTraits を使えば)
C++98/03 でも使える手法です。


そうしていないのは、単純に僕の好みの問題で、
class TT を定義した、その直後に T に対する制約を書いた方が、
T の制約を引数に紛れ込ませるよりも読みやすい気がするからです。

さらに追記( 23:10, 03/05 )

ダミー引数を取る場合、
class dummy_ {}; のデフォルトコンストラクタを explicit にすれば、

f( 1, {} );

という記述であっても、コンパイルエラーにすることが出来ます。


つまり、

#include <type_traits>

struct dummy_ {
  explicit dummy_() {}
};

template<class T>
void f( T const& x,
  typename std::enable_if<
    std::is_integral<T>::value, dummy_
  >::type = dummy_()
){
  // T が整数型の場合
}

// 以下同じなので省略

と書き、 dummy_ の名前をもっとユニークなものにするなり、
クラスメンバの場合には private なクラス内クラスにするなりすれば、
ダミー引数の誤爆は、ほとんど完全に回避できるようになります。


Variadic Templates を使いたくない(あるいは使えない)場合には、そう書くのもいいかもしれません。


なお、ダミー引数を使わない方法としては、名前空間と using 宣言を使い、

#include <type_traits>

namespace impl1_ {
  template< class T,
    class = typename std::enable_if<
      std::is_integral<T>::value
    >::type
  >
  void f( T const& x ) {
    // T が整数型の場合
  }
}
using impl1_::f;

namespace impl2_ {
  template< class T,
    class = typename std::enable_if<
      std::is_floating_point<T>::value
    >::type
  >
  void f( T const& x ) {
    // T が浮動小数点数型の場合
  }
}
using impl2_::f;

namespace impl3_ {
  template< class T,
    class = typename std::enable_if<
      std::is_class<T>::value
    >::type
  >
  void f( T const& x ) {
    // T がクラスの場合
  }
}
using impl3_::f;

と書く方法もありますので、お好みに合わせて使い分けるといいんじゃないでしょうか。

*1:実際には、ダミー引数のうち1つは書く必要ないのですが、今回は対称性を考えて全てダミーを導入しています