Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested classes with forward declaration cause an error in clang++, but pass without warning on g++

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.

like image 286
data Avatar asked Nov 18 '20 15:11

data


2 Answers

A class-specifier whose class-head-name contains a nested-name-specifier may not be appear within a class; only within the enclosing namespace

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 class Y may be declared in class X and later defined in the definition of class X or be later defined in a namespace scope enclosing the definition of class X. [_ 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.

like image 98
dfrib Avatar answered Nov 16 '22 00:11

dfrib


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.

like image 2
cigien Avatar answered Nov 16 '22 01:11

cigien