Search code examples
templatesc++17sfinae

Substitution failure causes a compile error


I'm trying to get a better understanding of SFINAE, and have noticed that the following code yields a compile error if the first enable_if that checks whether the non-type parameter g has a function type is commented out.

#include <type_traits>

// Require that a function have a return type of void
template <typename T>
struct ReturnTypeHelper 
{};

template <typename R, typename ... Args>
struct ReturnTypeHelper<R(Args...)> {
    using type = R;
};

template <typename T>
struct ReturnVoid : std::is_same<typename ReturnTypeHelper<T>::type, void> 
{};

enum class moo {
    yes,
    no,
};

// First candidate
template <auto f, moo m = moo::yes>
void foo() {}

// Second candidate
// Compile error if the first enable_if is commented out
template <auto f, auto g, moo m = moo::yes,
          // Require that g be a function
          std::enable_if_t<std::is_function_v<std::remove_pointer_t<decltype(g)>>, void>* = nullptr,
          // Require that g have a return type of void
          std::enable_if_t<ReturnVoid<typename std::remove_pointer_t<decltype(g)>>::value, void>* = nullptr>
void foo() {}

void p() {}

int main()
{
    foo<p, moo::yes>();

    return 0;
}

The error is:

<source>: In instantiation of 'struct ReturnVoid<moo>':
<source>:32:101:   required by substitution of 'template<auto f, auto g, moo m, std::enable_if_t<ReturnVoid<typename std::remove_pointer<decltype (g)>::type>::value, void>* <anonymous> > void foo() [with auto f = p; auto g = moo::yes; moo m = moo::yes; std::enable_if_t<ReturnVoid<typename std::remove_pointer<decltype (g)>::type>::value, void>* <anonymous> = <missing>]'
<source>:39:21:   required from here
<source>:14:8: error: no type named 'type' in 'struct ReturnTypeHelper<moo>'
   14 | struct ReturnVoid : std::is_same<typename ReturnTypeHelper<T>::type, void>
      |        ^~~~~~~~~~
Compiler returned: 1

For template candidate 2 and the function call foo<p, moo::yes>(), since moo::yes is not a function type, the primary template of struct ReturnTypeHelper is chosen, which does not have the type member. But in case of this failure, why doesn't the compiler simply choose template candidate 1, but instead reports this error?

Update: godbolt


Solution

  • Observe:

    constexpr bool a = ReturnVoid<void()>::value; // true
    constexpr bool b = ReturnVoid<int()>::value; // false
    constexpr bool c = ReturnVoid<int>::value; // compilation error
    

    Now see SFINAE on cpperefertence

    Only the failures in the types and expressions in the immediate context of the function type or its template parameter types or its explicit specifier (since C++20) are SFINAE errors. If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors.

    So this is exactly that. You need to rewrite ReturnVoid such that ReturnVoid<int>::value is false, not a compilation error. The simplest way is to change the default ReturnTypeHelper to

    template <typename T>
    struct ReturnTypeHelper 
    { using type = void(); };
    

    (No function can return void(), which is another function)