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
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)