I am trying to combine nested classes and forward declarations to keep the code legible even though it has a terribly complicated class structure. The forward declarations allow me to reduce the level of indentation here.
The following code compiles just fine on both g++-9.3 and clang++-10:
class A {
public:
class B;
};
class A::B {
public:
int foo=0;
};
However, when I'm doing the same nested within another class, this construct works without any warnings on g++, but fails on clang++:
class Outer {
public:
class A {
public:
class B;
};
class A::B {
public:
int foo=0;
};
};
The failure for clang++ is:
test.cpp:7:16: error: non-friend class member 'B' cannot have a qualified name
class A::B {
~~~^
1 error generated.
I guess this is in some way invalid code that gcc is benevolent enough to interpret correctly? I realize I could just move the class definition directly within class A, but let's say I want to keep this version with forward declarations.
From [class]/11:
If a class-head-name contains a nested-name-specifier, the class-specifier shall refer to a class that was previously declared directly in the class or namespace to which the nested-name-specifier refers, or in an element of the inline namespace set of that namespace (i.e., not merely inherited or introduced by a using-declaration), and the class-specifier shall appear in a namespace enclosing the previous declaration. In such cases, the nested-name-specifier of the class-head-name of the definition shall not begin with a decltype-specifier.
In your failing example, A::
is the nested-name-specifier and A::B { ... }
is the class-specifier. The criteria
[...] the class-specifier shall appear in a namespace enclosing the previous declaration.
is not fulfilled. Particularly, note that the class-specifier contains the class-head, which in turns contains a class-head-name, which in turn contains, optionally, a nested-name-specifier, as well as (non-optionally) a class-name. If the nested-name-specifier is present, [class]/11 applies, in which case (as emphasized above) the requirement of where the class-specifier is allowed applies; particularly, not being allowed nested within a class.
Thus your program is ill-formed; Clang is right.
We may note that Clang accepts the program if the class-specifier for B
appears at namespace scope (enclosing the previous in-class declaration):
class Outer {
public:
class A {
public:
class B;
};
};
class Outer::A::B {
public:
int foo=0;
};
Finally, note that [class.nest]/3 mentions defining nested class in-class of the class in which they were declared, as well as at namespace scope of the enclosing namespace:
If class
X
is defined in a namespace scope, a nested classY
may be declared in classX
and later defined in the definition of classX
or be later defined in a namespace scope enclosing the definition of classX
. [_ Example:_class E { class I1; // forward declaration of nested class class I2; class I1 { }; // definition of nested class }; class E::I2 { }; // definition of nested class
— end example ]
which allows your first example:
class A { public: class B; }; class A::B { public: int foo=0; };
to be re-factored as
class A {
public:
class B {
public:
int foo=0;
};
};
or
class A {
public:
class B; // forward declaration
// ...
class B {
public:
int foo=0;
};
};
but there is no conflict here with [class]/11, which specifically governs the allowed grammar for when a class-head-name contains a nested-name-specifier.
I think the code is valid, and this is a clang bug. According to class.qual#1:
If the nested-name-specifier of a qualified-id nominates a class, the name specified after the nested-name-specifier is looked up in the scope of the class ([class.member.lookup]), except for the cases listed below. The name shall represent one or more members of that class or of one of its base classes ([class.derived]).
...
The exceptions to the name lookup rule above are the following:
(1.1) the lookup for a destructor is as specified in [basic.lookup.qual];
(1.2) a conversion-type-id of a conversion-function-id is looked up in the same manner as a conversion-type-id in a class member access (see [basic.lookup.classref]);
(1.3) the names in a template-argument of a template-id are looked up in the context in which the entire postfix-expression occurs;
(1.4) the lookup for a name specified in a using-declaration ([namespace.udecl]) also finds class or enumeration names hidden within the same scope.
None of the exceptions apply to the case of A::B
. There is no destructor being named, no conversion-type-id, no templates, and no using declaration.
A
is in scope, and B
is an accessible member of A
, so the lookup should succeed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With