Search code examples
c++templatesenable-if

Using std::enable_if in a parameters list


In C++, consider the following example:

template <typename first, typename... params> struct q;

template <typename first> struct q <first>
{
    q()
    {
        cout<<"x"<<endl;
    }
};

template <typename first, typename... params> struct q
{
    q()
    {
        cout<<"x";
        q <params...> ();
    }
};

int main()
{
    q <int, int> ();
    q <int, int, enable_if<true, bool>::type> ();
    // q <int, int, enable_if<false, bool>::type> ();
}

I defined a template struct that accepts an arbitrary amount of parameters. I then instantiate it with a set of parameters. Each constructor will build a new q, with all its parameters but the first. A q with only one parameter won't call any more recursion.

Every time the constructor of a q is called, it will print out an "x". Therefore, the first line of the main will cause the program to print out "xx", while the second will print out an "xxx" since the enable_if is actually enabled. Now, I would have greatly appreciated if the third line would cause my program to print out "xx" (that is: build a q ). Sadly, what I get is the error

No template named 'type' in std::enable_if <false, bool>

What could I do to correct my example and make it work?

Thank you


Solution

  • You have misunderstood the use of std::enable_if. (Many of us did, to begin with). This has led you to ask a technically misguided question instead of the more general one you want answered. The question you want answered is How can my template use or ignore a template argument, based only a compiletime condition?, and it's not a bad one.

    You have the impression that std::enable_if<Cond,T>::type is a template expression that will instantiate to T when Cond is true and will instantiate to nothing when Cond is false.

    That is correct when Cond is true, because std::enable_if<true,T> is defined like:

    std::enable_if<true,T> {
        typedef T type;
    };
    

    But std::enable_if<false,T> is defined like:

    std::enable_if<false,T> {};
    

    See those definitions here

    Hence std::enable_if<false,T>::type does not disappear; for any T, it becomes an ill-formed, uncompilable, reference to the member-type type of std::enable_if<false,T>, which does not exist. It becomes a compiler error.

    Thus std::enable_if can be used to make instantiation of a template class or function either possible or impossible based upon the compiletime value of some boolean constant. And since the compiler will dismiss from consideration any non-viable instantiation of a template, provided there is a viable alternative, it can therefore be used to make a compiletime choice between alternative implementations of a template class or function, based on such a condition, e.g.

    template<bool Cond>
    typename std::enable_if<Cond,int>::type foo(int i) // A
    {
        return i*i;
    }
    
    template<bool Cond>
    typename std::enable_if<!Cond,int>::type foo(int i) // B
    {
        return i + i;
    }
    

    Here, foo<some_bool_constant>(i) will yield i squared if some_bool_constant is true and i doubled if some_bool_constant is false, because in the first case the B overload has an ill-formed instantiation and in the second case it's the A overload that becomes ill-formed.

    The alternative overloads give the compiler a choice of instantiations to consider each time it sees foo<some_bool_constant>(x), with one turning out ill-formed and the other viable, depending on the truth-value of some_bool_constant. It picks the one that is viable in each case. This behaviour is called SFINAE, and that is what you need to understand before you can understand std::enable_if.

    But there are no alternative instantiations of your:

    q <int, int, enable_if<false, bool>::type> ()
    

    "depending on the value of false". false unconditionally means false and so this is just ill-formed code.

    And consider: Even if std::enable_if<false,bool>::type did somehow disappear, that would leave:

    q <int, int, > ()
    

    which is still ill-formed.

    What could I do to correct my example and make it work?

    It doesn't take much, in fact, and std::enable_if does not feature. Let's address the real question: How can my template use or ignore a template argument, based only a compiletime condition?

    You need to define your template q<First, ... Rest> and some other template accept_if<bool Cond, typename T> in such a way that for any instantiation of q:

    /*(Iq)*/    q<A0,....,Aj,....,Ak>  // 0 <= j <= k
    

    where Aj = accept_if<some_bool_constant,t>::type, then if some_bool_constant == true, (Iq) will have exactly the same effects as:

    q<A0,....,t,....,Ak>
    

    and if some_bool_constant == false then (Iq) will have the same effects as:

    q<A0,....,Ak>
    

    without resorting to any runtime test of some_bool_constant. (In (Iq), the '....' are just schematic place-holders for possible arguments: nothing to do with C++ packs or elipses.)

    But accept_if<some_bool_constant,t>::type must resolve to some type specifier at compiletime or the code is ill-formed. It cannot resolve to nothing. So in the false case it must resolve to a type specifier that the instantiation of (Iq) will handle as specifying no data-type.

    Now the compelling utility of possessing a type specifier that specifies no data-type in C/C++ was recognized as long ago as the very first ANSI Standard for C. That type specifier is void.

    From your comments we learn that the template parameters of q represent a sequence of data types which an instantiation of the constructor shall extract from a source passed by argument. In that case, void ideally fits the bill as a type specifer denoting nothing to be done. You cannot declare a void object, let alone extract one from somewhere.

    Hence I suggest that your accept_if be defined:

    template<bool Cond, typename T>
    using accept_if = std::conditional<Cond,T,void>::type;
    

    i.e. accept_if<true,T> = T and accept_if<false,T> = void. Read about std::conditional

    With this definition, you then have only to add to your existing specializations of q one more:

    template<> struct q<void> {};
    

    giving q<void> a default constructor that does nothing. Here is a sample program like your own, but well-formed, that illustrates this solution:

    #include <type_traits>
    #include <iostream>
    
    template<bool Cond, typename T>
    using accept_if = typename std::conditional<Cond,T,void>::type;
    
    template <typename first, typename... params> struct q;
    
    template<> struct q<void> {};
    
    template <typename first> struct q <first>
    {
        q()
        {
            std::cout << "x" << std::endl;
        }
    };
    
    template <typename first, typename... params> struct q
    {
        q()
        {
            std::cout << "x";
            q<params...>();
        }
    };
    
    int main()
    {
        q<int, int>();
        q<int, int, accept_if<true,bool>>();
        q<int, int, accept_if<false,bool>>();
        return 0;
    }
    

    The output is what you wanted:

    xx
    xxx
    xx
    

    In a different scenario it is possible that you would want even Aj = void not to be ignored in (Iq). But since we can create types at will, you can always a create a type that is by fiat different from all those that your template must not ignore, say: struct ignore {}; Then you would define:

    template<bool Cond, typename T>
    using accept_if = std::conditional<Cond,T,ignore>::type;
    

    and specialize:

    template<> struct q<ignore> {};