Search code examples
c++templatessfinaespecializationpartial-specialization

C++: template to check if expression compiles


When writing template specialization with SFINAE you often come to the point where you need to write a whole new specialization because of one small not-existing member or function. I would like to pack this selection into a small statement like orElse<T a,T b>.

small example:

template<typename T> int get(T& v){
    return orElse<v.get(),0>();
}

is this possible?


Solution

  • The intent of orElse<v.get(),0>() is clear enough, but if such a thing could exist, it would have to be be one of:

    Invocation Lineup

    orElse(v,&V::get,0)
    orElse<V,&V::get>(v,0)
    orElse<V,&V::get,0>(v)
    

    where v is of type V, and the function template thus instantiated would be respectively:

    Function Template Lineup

    template<typename T>
    int orElse(T & obj, int(T::pmf*)(), int deflt);
    
    template<typename T, int(T::*)()>
    int orElse(T & obj, int deflt);
    
    template<typename T, int(T::*)(), int Default>
    int orElse(T & obj);
    

    As you appreciate, no such a thing can exist with the effect that you want.

    For any anyone who doesn't get that, the reason is simply this: None of the function invocations in the Invocation Lineup will compile if there is no such member as V::get. There's no getting round that, and the fact that the function invoked might be an instantiation of a function template in the Function Template Lineup makes no difference whatever. If V::get does not exist, then any code that mentions it will not compile.

    However, you seem to have a practical goal that need not be approached in just this hopeless way. It looks as if, for a given name foo and an given type R, you want to be able to write just one function template:

    template<typename T, typename ...Args>
    R foo(T && obj, Args &&... args);
    

    which will return the value of R(T::foo), called upon obj with arguments args..., if such a member function exists, and otherwise return some default R.

    If that's right, it can be achieved as per the following illustration:

    #include <utility>
    #include <type_traits>
    
    namespace detail {
    
    template<typename T>
    
    T default_ctor()
    {
        return T();
    }
    
    // SFINAE `R(T::get)` exists
    template<typename T, typename R, R(Default)(), typename ...Args>
    auto get_or_default(
        T && obj,
        Args &&... args) ->
        std::enable_if_t<
            std::is_same<R,decltype(obj.get(std::forward<Args>(args)...))
        >::value,R>
    {
        return obj.get(std::forward<Args>(args)...);
    }
    
    // SFINAE `R(T::get)` does not exist
    template<typename T, typename R, R(Default)(), typename ...Args>
    R get_or_default(...)
    {
        return Default();
    }
    
    } //namespace detail
    
    
    // This is your universal `int get(T,Args...)`
    template<typename T, typename ...Args>
    int get(T && obj, Args &&... args)
    {
        return detail::get_or_default<T&,int,detail::default_ctor>
            (obj,std::forward<Args>(args)...);
    }
    
    // C++14, trivially adaptable for C++11
    

    which can be tried out with:

    #include <iostream>
    
    using namespace std;
    
    struct A
    {
        A(){};
        int get() {
            return 1;
        }
        int get(int i) const  {
            return i + i;
        }
    };
    
    struct B
    {
        double get() {
            return 2.2;
        }
        double get(double d) {
            return d * d;
        }
    };
    
    struct C{};
    
    int main()
    {
        A const aconst;
        A a;
        B b;
        C c;
        cout << get(aconst) << endl;    // expect 0
        cout << get(a) << endl;         // expect 1 
        cout << get(b) << endl;         // expect 0
        cout << get(c) << endl;         // expect 0
        cout << get(a,1) << endl;       // expect 2
        cout << get(b,2,2) << endl;     // expect 0
        cout << get(c,3) << endl;       // expect 0
        cout << get(A(),2) << endl;     // expect 4
        cout << get(B(),2,2) << endl;   // expect 0
        cout << get(C(),3) << endl;     // expect 0
        return 0;
    }
    

    There is "compound SFINAE" in play in the complicated return type:

    std::enable_if_t<
            std::is_same<R,decltype(obj.get(std::forward<Args>(args)...))
        >::value,R>
    

    If T::get does not exist then decltype(obj.get(std::forward<Args>(args)...) does not compile. But if it does compile, and the return-type of T::get is something other than R, then the std::enable_if_t type specifier does not compile. Only if the member function exists and has the desired return type R can the R(T::get) exists case be instantiated. Otherwise the catch-all R(T::get) does not exist case is chosen.

    Notice that get(aconst) returns 0 and not 1. That's as it should be, because the non-const overload A::get() cannot be called on a const A.

    You can use the same pattern for any other R foo(V & v,Args...) and existent or non-existent R(V::foo)(Args...). If R is not default-constructible, or if you want the default R that is returned when R(V::foo) does not exist to be something different from R(), then define a function detail::fallback (or whatever) that returns the desired default R and specify it instead of detail::default_ctor

    How nice it would be it you could further template-paramaterize the pattern to accomodate any possible member function of T with any possible return type R. But the additional template parameter you would need for that would be R(T::*)(typename...),and its instantiating value would have to be &V::get (or whatever), and then the pattern would force you into the fatal snare of mentioning the thing whose existence is in doubt.