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 T
で T
を定義した、その直後に 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つは書く必要ないのですが、今回は対称性を考えて全てダミーを導入しています