c++c++11templatessfinaeenable-if

Does SFINAE not apply here?


I was writing something to use SFINAE to not generate a function under certain conditions. When I use the meta code directly it works as expected, but when I use the code indirectly through another class, it fails to work as expected.

I thought that this was a VC++ thing, but looks like g++ also has this, so I'm wondering if there's some reason that SFINAE isn't being applied to this case.

The code is simple. If the class used isn't the base class of a "collection class", then don't generate the function.

#include <algorithm>
#include <type_traits>

#define USE_DIRECT 0
#define ENABLE 1

class A{};
class B{};
class C{};
class D{};
class collection1 : A, B, C {};
class collection2 : D {};

#if USE_DIRECT
template<typename X>
typename std::enable_if<std::is_base_of<X, collection1>::value, X>::type fn(X x)
{
    return X();
}

# if ENABLE
template<typename X>
typename std::enable_if<std::is_base_of<X, collection2>::value, X>::type fn(X x)
{
    return X();
}
# endif

#else // USE_DIRECT

template<typename X, typename COLLECTION>
struct enable_if_is_base_of
{
    static const int value = std::is_base_of<X, COLLECTION>::value;
    typedef typename std::enable_if<value, X>::type type;
};

template<typename X>
typename enable_if_is_base_of<X, collection1>::type fn(X x)
{
    return X();
}

# if ENABLE
template<typename X>
typename enable_if_is_base_of<X, collection2>::type fn(X x)
{
    return X();
}
# endif
#endif // USE_DIRECT

int main()
{
    fn(A());
    fn(B());
    fn(C());
    fn(D());
   
   return 0;
}

If I set USE_DIRECT to 1 and ENABLE to 0, then it fails to compile as there is no function fn that takes a parameter D. Setting ENABLE to 1 will stop that error from occurring.

However, if I set USE_DIRECT to 0 and ENABLE to 0, it will fail with different error messages but for the same case, there is no fn that takes parameter D. Setting ENABLE to 1 however will cause a failure for all 4 function calls.

Can someone explain what is happening here and why?

This looks like it may be related to Alias templates used in SFINAE lead to a hard error, but no one has answered that either.

For reference, here are the errors that were generated by g++:

main.cpp: In instantiation of 'struct enable_if_is_base_of<A, collection2>':                                                                   
main.cpp:45:53:   required by substitution of 'template<class X> typename enable_if_is_base_of<X, collection2>::type fn(X) [with X = A]'       
main.cpp:54:8:   required from here                                                                                                            
main.cpp:34:82: error: no type named 'type' in 'struct std::enable_if<false, A>'                                                               
  typedef typename std::enable_if<std::is_base_of<X, COLLECTION>::value, X>::type type;                                                        
                                                                                  ^                                                            
main.cpp: In instantiation of 'struct enable_if_is_base_of<B, collection2>':                                                                   
main.cpp:45:53:   required by substitution of 'template<class X> typename enable_if_is_base_of<X, collection2>::type fn(X) [with X = B]'       
main.cpp:55:8:   required from here                                                                                                            
main.cpp:34:82: error: no type named 'type' in 'struct std::enable_if<false, B>'                                                               
main.cpp: In instantiation of 'struct enable_if_is_base_of<C, collection2>':                                                                   
main.cpp:45:53:   required by substitution of 'template<class X> typename enable_if_is_base_of<X, collection2>::type fn(X) [with X = C]'       
main.cpp:56:8:   required from here                                                                                                            
main.cpp:34:82: error: no type named 'type' in 'struct std::enable_if<false, C>'                                                               
main.cpp: In instantiation of 'struct enable_if_is_base_of<D, collection1>':                                                                   
main.cpp:38:53:   required by substitution of 'template<class X> typename enable_if_is_base_of<X, collection1>::type fn(X) [with X = D]'       
main.cpp:57:8:   required from here                                                                                                            
main.cpp:34:82: error: no type named 'type' in 'struct std::enable_if<false, D>'                                                               

Solution

  • Only a substitution that takes place in an immediate context may result in a deduction failure:

    §14.8.2 [temp.deduct]/p8

    Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure. [ Note: The evaluation of the substituted types and expressions can result in side effects such as the instantiation of class template specializations and/or function template specializations, the generation of implicitly-defined functions, etc. Such side effects are not in the “immediate context” and can result in the program being ill-formed.end note ]

    The signature of fn requires a full declaration of enable_if_is_base's specialization to exist:

    §14.7.1 [temp.inst]/p1

    Unless a class template specialization has been explicitly instantiated (14.7.2) or explicitly specialized (14.7.3), the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program.

    The compiler fails to generate the specialization of:

    template<typename X, typename COLLECTION>
    struct enable_if_is_base_of
    {
        static const int value = std::is_base_of<X, COLLECTION>::value;
        typedef typename std::enable_if<value, X>::type type;
    };
    

    because the substitution of typename std::enable_if<value, X>::type results in a missing type if value evaluates to false, which is not in an immediate context.