SFINAEでオーバーロードされた関数を検出する方法

C++ のSFINAE (Substitution Failure Is Not An Error)を使って,クラス T に foo(int a, int b)があるか否かを調べる方法です.

簡単な例(class Tに オーバーロードされた関数がない場合)

まずはシンプルに,クラス T に対してT::foo が存在するかどうか調べるコードを用意します.

SFINAEの実装は,c++11で導入された type_traits の std::false_type と std::true_type を使うと簡単に実装できます.

(めんどくさいのでクラスは classじゃなくてstruct で定義しています)

#include <iostream>
#include <type_traits>

template <typename... Ts> using void_t = void;  // std::void_t が使えない処理系もあるので自前で void_t を定義

// デフォルトのテンプレートクラス
// その他のテンプレートが展開できなかった場合は,このデフォルトのテンプレートが採用される
// 
// このテンプレートクラスは std::false_type を継承しているので,::value は false となる.
template <typename T, typename = void>
struct has_foo : std::false_type {};

// T::foo が存在する場合は,こちらのテンプレートが展開される
//
// このテンプレートクラスは std::true_type を継承しているので, ::valueはtrueとなる
template <typename T>
struct has_foo<T, void_t<decltype(&T::foo)>> : std::true_type {};


int main() {
    struct A {
	void foo();
    };

    struct B {
	void another();
    };

    std::cout << std::boolalpha;
    
    std::cout << has_foo<A>::value << std::endl;  
    // true が出力される.つまり struct Aにはfooがある
    
    std::cout << has_foo<B>::value << std::endl; 
    // false に出力される.つまり struct  Bにはfooが無い

    return 0; 
}

オーバーロードされた関数がある場合の挙動 (+ SFINAEの仕組み)

上記のコードは,オーバロードされた関数がなければ期待通りに動作します.

一方で overload された関数がある場合, has_foo<T> は T::foo を検出できなくなります.例えば

    struct C {
        void foo();
        void foo(int x);
    };
    std::cout << has_foo<C>::value << std::endl;  

は true(つまり C::foo がある) ではなくて,false (つまりC::foo がない)を出力します

理由は has_foo の decltype(&T::foo) で T::foo のポインタが解決できないためです.
例えば g++ で

    struct C {
        void foo();
        void foo(int x);
    };
     decltype(C::foo);

というコードをコンパイルすると以下のエラーが出ます

error: 'decltype' cannot resolve address of overloaded function
         decltype(C::Foo);

つまりT::foo にはオーバーロードされたものが二つあるので,コンパイラはどちらのアドレスを選べば良いか判断できずコンパイル・エラーを出します.一方でSFINAEを使うとこのエラーを回避するために,デフォルトの has_foo<T> が選ばれてコンパイルが進むということになります.

オーバーロードされた関数がある場合のSFINAE

少々面倒なコードが必要になります

#include <type_traits>

// TestType<A,int> で A::foo(int) のdecltypeを返す
template<typename T, typename... Ts>
using TestType = auto(Ts...) -> decltype(T::foo(std::declval<Ts>()...));


//  T::foo()を検出するテンプレート
template<typename T, typename = void>
struct has_foo0 : std::false_type { };
template<typename T>
struct has_foo0<T, std::void_t<TestType<T>> > : std::true_type { };

//  T::foo(int)を検出するテンプレート
template<typename T, typename = void>
struct has_foo1 : std::false_type { };
template<typename T>
struct has_foo1<T, std::void_t< std::is_same<TestType<T,int>,void(int)>> > : std::true_type { };

TestType<T, ...Ts> というテンプレートクラスを新たに用意します.これは例えば TestType<A,int > とすると A::foo(int)のdecltypeを返します.これを使って has_foo0<T> と has_foo1<T>を定義しています

テストしてみます

#include <iostream>

//
// 上記のテンプレートの定義(省略)
//

struct A{
    static void foo();
    static void foo(int x);
};
struct B{
    static void foo();
};
struct C{
};

void f();

int main()
{
    static_assert(      has_foo0<A>::value );
    static_assert(      has_foo0<B>::value );
    static_assert(not has_foo0<C>::value );   // struct C に C::foo() は無い
    static_assert(not has_foo0<int>::value ); // intにも int::foo()は無い

    static_assert(      has_foo1<A>::value );   // foo(int) は struct Aのみにある
    static_assert(not has_foo1<B>::value );
    static_assert(not has_foo1<C>::value );
    static_assert(not has_foo1<int>::value );
    return 0;
}

正しく T::foo() や T::foo(int)が検出できるようになりました!

補足:コンパイラのバージョンについて

今回のコードは,とりあえず C++17 を使って書きました.
type_traits のstd::true_type やら variadic templates を使っているのでコンパイルには C++11以降が必須だと思います.