Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In a class, what is `using Base::BaseOfBase;` supposed to do?

Tags:

c++

c++20

Consider this code:

#include <iostream>
#include <type_traits>

struct A
{
    A(int x) {std::cout << "A(" << x << ")\n";}
};

struct B : A
{
    using A::A;
    B(int x, int y) : A(x) {std::cout << "B(" << x << "," << y << ")\n";}
};

struct C : B
{
    using B::A; // <--

    // C() : B(0,0) {}
};

int main()
{
    C c(1);
}

gcc.godbolt.org

It compiles on GCC and prints A(1), which means that an instance of B was "constructed" without calling the constructor. If you uncomment C(), then C c(1); no longer compiles (GCC can't find a suitable constructor)

Clang doesn't say anything about using B::A;, but refuses to compile C c(1); (also can't find a suitable constructor).

MSVC stops right at using B::A;, basically saying that you can only inherit constructors from direct bases.

Cppreference doesn't mention inheriting constructors from indirect bases, so it seems to be disallowed.

Is it a GCC & Clang bug, or what's going on here?

like image 887
HolyBlackCat Avatar asked May 30 '20 21:05

HolyBlackCat


1 Answers

The constructor is not inherited. Primarily because

[namespace.udecl]

3 In a using-declaration used as a member-declaration, each using-declarator's nested-name-specifier shall name a base class of the class being defined. If a using-declarator names a constructor, its nested-name-specifier shall name a direct base class of the class being defined.

But the kicker is that B::A doesn't even name a constructor at all.

[class.qual]

2 In a lookup in which function names are not ignored and the nested-name-specifier nominates a class C:

  • if the name specified after the nested-name-specifier, when looked up in C, is the injected-class-name of C (Clause [class]), or
  • in a using-declarator of a using-declaration that is a member-declaration, if the name specified after the nested-name-specifier is the same as the identifier or the simple-template-id's template-name in the last component of the nested-name-specifier,

the name is instead considered to name the constructor of class C. [ Note: For example, the constructor is not an acceptable lookup result in an elaborated-type-specifier so the constructor would not be used in place of the injected-class-name.  — end note ] Such a constructor name shall be used only in the declarator-id of a declaration that names a constructor or in a using-declaration. [ Example:

struct A { A(); };
struct B: public A { B(); };

A::A() { }
B::B() { }

B::A ba;            // object of type A
A::A a;             // error, A​::​A is not a type name
struct A::A a2;     // object of type A

 — end example ]

Neither of the two bullets above applies. So B::A is not the name of the constructor. It's just the injected class name A, which is already available to use in C. I guess it should be just like bringing in any old type definition from a base class. I.e. Clang would let you define

C::A a(0);

Which appears correct. The only utility of this is if B was inheriting from a protected A. In which case the injected class name would also be inaccessible by default, until brought forward with a using declaration. Tinkering with your example on godbolt confirms it.

MSVC is probably too zealous in rejecting this code outright.

As far as which compiler is correct, C++20 introduced aggregate initialization via parenthesized list of values. C is an aggregate, so C c(1); is in fact aggregate initializing c by using 1 to copy-initialize the B sub-object. So no constructor needs to be inherited by C for this code to be valid.

GCC is indeed doing that (because making the c'tors explicit makes it reject the code), while Clang seems to have not implemented P0960 yet.

like image 186
StoryTeller - Unslander Monica Avatar answered Nov 20 '22 04:11

StoryTeller - Unslander Monica