Search code examples
c++overloadingsfinaec++17type-traits

Why does SFINAE not work in such a case?


#include <iostream>
#include <type_traits>

template<typename T>
struct A
{
    using m = std::remove_pointer_t<T>&;
};

template
<
    typename T,
    typename = std::void_t<>
>
struct Test
{
    enum { value = 0 };
};

template<typename T>
struct Test<T, typename A<T>::m>
{
    enum { value = 1 };
};

int main()
{
    std::cout << Test<void*&>::value; // ok, output 0
    std::cout << Test<void*>::value; // error : cannot form a reference to 'void'
}

The first case outputs 0, which means the primary template is selected. So, I think the second case should also select the primary template rather than the specialized one; then, there should not be an error.

It is expected that Test<void*&> is ok; what surprised me is that Test<void*> should be not ok!

Why does SFINAE not work in the latter case?


Solution

  • Your second case is a hard error.

    SFINAE @ cppreference.com says:

    Only the failures in the types and expressions in the immediate context of the function type or its template parameter types 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.

    The first case is ok because remove_pointer has no effect if T is void*&, then m is void*& because of reference collapsing and a reference to pointer to void is a valid type while a reference to void is not.

    The type in the first case is still Test<void*&, void> not Test<void*&, void*&> because you only specify the first template argument.

    The reason why the second case fails but not the first is that the compiler has to instantiate the specialized template because the second parameter is a non-deduced context so the compiler cannot tell right away whether the specialization would be a better match. But in the second case the instantiation produces a hard error while in the first case it doesn't.

    The primary template is still selected because it is a better match (while still the specialized template will be instantiated to check whether it matches).

    Note: I can't say whether the specialization is actually fully instantiated or whether the compiler is just looking up typename A<T>::m in order to check whether this specialization would be a better match. The result is the same however. In case of void* there is a hard error.

    Also note that when using C++17 anyway one would probably rather use a constexpr member than an enum.

    template<typename T>
    struct Test<T, typename A<T>::m>
    {
        static constexpr unsigned value = 1u;
    };