Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 741
tly Avatar asked Mar 04 '15 15:03

tly


1 Answers

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.

like image 150
Mike Kinghan Avatar answered Sep 24 '22 09:09

Mike Kinghan