Search code examples
gccvisual-c++clangc++17icc

Using declaration of constructors compromises access specifiers and isn't consistent with other types of members


Today I've learned a new shocking reality, all popular compilers (those I could put my hands on, on godbolt.org) are good with this code (it compiles) and I can't explain why:

class A
{
protected:
    A()
    { }
};

class B : private A
{
    using A::A;
};

int main()
{
    auto b = B{};
    return 0;
}

view on godbolt.org

My reasoning: it should fail at auto b = B{};, since using declaration is in private scope, therefore that's where the constructors, implicitly provided by the compiler, as the result of that using, should go.

Be it any other member: function or variable, it's access modifier would be determined, based on where is the using declaration is placed (public/protected/private section).

But, now, this doesn't compile:

class A
{
protected:
    A(int)
    { }
};

class B : private A
{
    using A::A;
};

int main()
{
    auto b = B{1};
    return 0;
}

view on godbolt.org

And that's predictable and intuitive:

<source>:15:14: error: calling a protected constructor of class 'A'
    auto b = B{1};
             ^
<source>:4:5: note: declared protected here
    A(int)

But, unfortunately, it doesn't compile (as IS intuitive I believe) for other reasons, because this doesn't as well:

class A
{
protected:
    A(int)
    { }
};

class B : public A
{
public:
    using A::A;
};

int main()
{
    auto b = B{1};
    return 0;
}

view on godbolt.org


It seems that using declaration is either badly worded or badly understood. Unfortunately, many compilers (some of them, fortunately, not longer, in a HEAD) also struggle with friend permissions:

class C;

class A
{
    friend class C;

protected:
    A(int)
    { }
};

class B : public A
{
public:
    using A::A;
};

class C
{
public:
    B make_b()
    {
        return B{1};
    }
};

int main()
{
    auto b = C{}.make_b();
    return 0;
}

view on godbolt.org

Can some language lawyer analyse this and shed some light? Am I wrong with my assumptions and this is how it should be?


Solution

  • class.default.ctor

    If there is no user-declared constructor for class X, a non-explicit constructor having no parameters is implicitly declared as defaulted

    There are no user-declared constructors for class B. The constructor that B inherits from A is not a constructor for B, it is a constructor for A. Inherited constructors are considered when looking up constructors for the derived class, but they are still not constructors for the derived class.

    The standard never explicitly says that inheriting constructors does or does not create similar constructors for the derived class. The standard does say that the constructors for the base class are made available for lookup and overload resolution as if they were constructors for the derived class. This IMO means that they are not considered constructors for the derived class, though it would be better if the standard explicitly said that. At any rate, the compilers seem to interpret it this way.

    Edit This is a change from C++14, where inherited constructors were injected ino the derived class. Even in C+14, these constructors were implicitly declared and not user-declared.

    Thus B has a public implicitly declared as defaulted default constructor, regardless of whatever it inherits from A.

    namespace.udecl

    Base-class constructors considered because of a using-declarator are accessible if they would be accessible when used to construct an object of the base class; the accessibility of the using-declaration is ignored.

    Thus A::A(int) is not accessible when constructing B, even though the using declaration that imports it is accessible.