Arty
Arty

Reputation: 16737

SFINAE not always works in C++?

I'm using C++17. I made following code that is supposed to use SFINAE to test if lambda is compilable or not (lambda is always syntactically correct but may be uncompilable e.g. due to absence of some methods used in body):

Try it online!

#include <type_traits>
#include <sstream>
#include <iostream>
#include <tuple>

template <typename ... Ts>
struct Types {
    using types = std::tuple<Ts...>;
    template <size_t I>
    using type = std::tuple_element_t<I, types>;
};

template <typename F, typename Enable = void, typename ... Ts>
struct Compilable : std::false_type {};

template <typename F, typename ... Ts>
struct Compilable<F,
        std::void_t<decltype((*(F*)0)(Types<Ts...>{}))>, Ts...>
    : std::true_type {};

template <typename ... Ts, typename F>
bool constexpr Comp(F const & f) {
    return Compilable<F, void, Ts...>::value;
}

template <typename T>
void Test() {
    std::cout << Comp<T>([](auto Ts){
        typename decltype(Ts)::template type<0> * p = 0;
        std::stringstream ss; ss << (*p); }) << std::endl;
}

struct X {};

int main() {
    Test<int>();
    Test<X>();
}

I hoped that uncompilable specialization will be silently excluded due to SFINAE, but instead it throws compilation error (for the second Test<X>() case, first test compiles):

<source>:30:34: error: invalid operands to binary expression 
   ('std::stringstream' (aka 'basic_stringstream<char>') and
   'typename decltype(Ts)::type<0>' (aka 'X'))
        std::stringstream ss; ss << (*p); }) << std::endl;

What am I doing wrong? Am I using SFINAE mechanism wrongly? What should be the right way to implement code above?

Upvotes: 0

Views: 171

Answers (1)

Yankes
Yankes

Reputation: 2115

I think problem is that you try SFINAE based error in body of lambda. And if I understand this is far to late to recovery from error.

Base idea of SFINAE is to remove incompatible function from overload set, not to to recover from compilation failures, line is bit blurred, but probably best rule of thumb is that error need happen only in function declaration.

e.g.


template<typename T>
struct Get
{
    using type = typename T::type;
};

void F1(...){}
template<typename T, typename TT = typename Get<T>::type>
void F1(T i){}

void F2(...){}
template<typename T, typename TT = typename T::type>
void F2(T i){}


int main() {
    //F1(3); //hard error
    F2(3); //SFINAE - work fine
}

Type Get<T> fail to be created, in theory compiler could recover from this but this could be costly (image error happens in deep hierarchy). F2 fail correctly because compiler only need to look on function header to see if it is correct.

If you could move required check to template header then it could in theory work. Something like:


template <typename T>
void Test() {
    std::cout << Comp<T>(
        [](auto Ts, decltype((*(std::stringstream*)0) << (*(typename decltype(Ts)::template type<0>*)0))* = {})
        {
            typename decltype(Ts)::template type<0> * p = 0;
            std::stringstream ss; ss << (*p);
        }
    ) << std::endl;
}

Second lambda parameter is evaluated on call site and can "visibly" fails applying arguments. Its not give results as you want but it compile, probably you would need update pram type to correctly reflect operation in lambda.

Upvotes: 2

Related Questions