tly
tly

Reputation: 1272

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?

Upvotes: 6

Views: 910

Answers (2)

Mike Kinghan
Mike Kinghan

Reputation: 61620

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.

Upvotes: 2

Jonathan Hopkins
Jonathan Hopkins

Reputation: 43

Yes, this is more or less possible. It is known as a "member detector". See this wikibooks link for how to accomplish this with macros. The actual implementation will depend on whether you are using pre- or post-C++11 and which compiler you are using.

Upvotes: 2

Related Questions