Search code examples
c++templatesc++11enable-if

C++ template instantations: using enable_if directly, or with an auxiliary class


This code works fine:

#include <type_traits>

using namespace std;

enum class Type : char { Void };
struct FieldA { static constexpr Type type = Type::Void; };

template<typename Field> struct Signal {};
template<typename Field> struct SignalA : public Signal<Field> {};
struct SignalB : public Signal<void> {};
struct DerivedB : public SignalB {};

template<typename Signal, typename = void> struct Apply;

template<typename Field>
struct Apply<SignalA<Field>, typename std::enable_if<Field::type == Type::Void>::type> {};

template<typename Signal_t>
struct Apply<Signal_t, typename enable_if<is_base_of<SignalB, Signal_t>::value>::type>
{};

int main ()
{ Apply<SignalA<FieldA> > a; }

But what I want is improve readability erasing the long enable_ifs, so, let's attack, for example, the second enable_if with an auxiliary class:

template<typename Signal>
struct IsBaseOfB
{ using type = typename enable_if<is_base_of<SignalB, Signal>::type; };

And change the second Apply partial specialization with it:

template<typename Signal_t>
struct Apply<Signal_t, typename IsBaseOfB<Signal_t>::type>
{};

Even when the only possible specialization is the first one, gcc throws me the following error:

main.cpp: In instantiation of 'struct IsBaseOfB<SignalA<FieldA> >':
main.cpp:21:78: error: no type named 'type' in 'struct std::enable_if<false, void>'

{ using type = typename enable_if<is_base_of<SignalB, Signal_t>::value>::type; };

Which is obvious since the enable_if condition doesn't fit for SignalA<FieldA>.

Which I don't understand is why that specialization failure is not ignored to get the first specialization (which I know it works).


Solution

  • The error is not in "the immediate context" of the template argument deduction, and so SFINAE does not apply (i.e. it's an error, not a substitution failure).

    The C++ standard doesn't really define "immediate context" but I have attempted to give a hand-wavy explanation in the accepted answer to What is exactly the “immediate context” mentioned in the C++11 Standard for which SFINAE applies?

    Briefly, the problem is that the compiler sees Apply<SignalA<FieldA>> and happily substitutes the template arguments into struct Apply<Signal_t, typename IsBaseOfB<Signal_t>::type> which happens without an error because IsBaseOf does have a nested type member, but then that triggers the instantiation of IsBaseOf::type and that's ill-formed. The error is not in the immediate context of the deduction (it's elsewhere in the body of IsBaseOf which is instantiated as a side effect).

    Another way to look at the problem is that IsBaseOf::type is declared unconditionally, not only when the is_base_of trait is true, so it is always declared as a type ... but sometimes its definition is ill-formed. The deduction finds the declaration and continues past the point where SFINAE applies, then a fatal error is found when the definition of IsBaseOf::type is required.

    You can either solve it using an alias template (as shown in Filip's answer) or using inheritance, so that IsBaseOf::type only exists conditionally:

    template<typename Signal>
    struct IsBaseOfB : enable_if<is_base_of<SignalB, Signal>::value>
    { };
    

    This way the nested type member is only present when the enable_if base class declares it.