TIL: inheriting constructors, default parameters and enable_if

published at 26.04.2018 23:11 by Jens Weller
Save to Instapaper Pocket

... might not mix that well. And its an interesting language detail causing it.

So this story starts in the C++ Slack, where a user posts a question, regarding a compilation error from Visual Studio. The question is, why is the error on that specific line: using base::base; when calling the child class constructor with an invalid parameter. When looking at the code, it seems that Visual Studio is generating this error out of thin air, instead of erroring at the construction side. Which would be the preferred place to error.

And while looking at this code, I felt something was odd, the error seems to hint at something, which one does not expect. Its hard to understand the error, because something is happening, which isn't visible in the code. Something that is in the language, some unexpected mechanic, which leads to an odd error.

The rough error message posted was:

No instance of constructor A<B>::A  [with B=C] matches the argument list (const float). detected during implicit generation of C::C(const V& v) [ with V=float]

A tiny example to reproduce the issue causing this:

#include <type_traits>
struct myInt
{
    myInt(){}
    template< class T>
    myInt(const T&t,
    typename std::enable_if< std::is_integral< T>::value>::type* = 0)
    {}
    int n=0;
};

struct myNum : myInt
{
    myNum(){}
    using myInt::myInt;
};

int main()
{
    myNum x(4.0f);
}

The base class has a templated constructor, which uses enable_if to be only visible if T is also an integral. Just for this example, in the real world this will be more complex. So this will not compile, but for a reason you likely don't expect. After all, myNum does not have a fitting constructor, you think. It does have one. Inherting constructors do have their own set of rules they play by, and one of them is:

d) For each constructor template with default arguments or the ellipsis, all constructor signatures that are formed by dropping the ellipsis and omitting default arguments from the ends of argument lists one by one

So, the child class inherits constructors, but any parameter which has a default will omit this defaul... As the enable_if exactly that, it gets 'dropped' when the constructor is inherited. So myNum ends up having a templated constructor, but without the enable_if checks. But the parent does not provide such a constructor, as enable_if and SFINAE prevent this, hence the code does not compile.

So when you use enable_if in a constructor that might be pulled into a child class with using, try to avoid using it as a default parameter. As a template parameter it works fine. And, every other parameter with a default value present, will also be inherited into the child class without its default value.

Though, it is nice to know that C++17 fixes this, according to Casey Carter.

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!