c++nestedlanguage-lawyerinner-classesforward-declaration

Visibility of C++ nested class complete definition in the presence of forward declarations


In C++, forward declarations introduce an incomplete type. Incomplete types can be used to declare pointers, but they cannot be used to initialize values or access members because the complete definition has not been given.

This rule apparently does not to hold for nested classes. Consider the following working example:

struct C { // #1
    // Forward declaration only
    struct A; // #2
    struct B {
        // Pointer declaration is allowed with incomplete type
        A* a;

        int foo() {
            // Member requires complete definition of A
            return a->value;
        }   
    };

    // Complete definition of A given after B
    struct A {
        int value;
    };  

    // Members and constructor
    A a; B b;
    C(int x) : a{x}, b{&a} {}
};

int main() {
    C c{42};
    return c.b.foo(); // returns 42
}

This snippet compiles successfully with g++ and clang. Seemingly any tweak causes this "forward looking" referencing to the complete definition of struct A to fail.

Lifting the definitions of A/B verbatim out of the enclosing struct C (i.e. removing #1) yields the expected error about improper use of an incomplete type. Deleting the forward declaration entirely (i.e. removing #2) yields an "unknown type name 'A'" error.

This is not a theoretical concern: Open Motion Planning Library, an important robotics library, leverages this pattern in several places to define comparators for priority queue order. (Search the repo for MotionCompare to see more complete examples).

So the question: why does this compile? Is this an obscure standard-compliant feature of nested types, or a common compiler bug? Has the validity of this construction changed as the language standard has evolved?

EDIT: This related question has a similar answer but is not the same question. In particular, the two could not be identified as duplicates without already knowing the answer. This question is about an explicit forward declaration of a sibling class; the other is about an implicit forward declaration of the parent class. The fact that the same name lookup rules apply to both situations was not obvious. Also, because the other question does not explicitly reference forward-declaration or sibling classes, it is unlikely to be discovered by someone looking for an answer in those cases.


Solution

  • The body of a member function is a complete-class-context ([class.mem.general]p10):

    A complete-class context of a class (template) is a :

    • function body ([dcl.fct.def.general]),
    • [...]

    within the member-specification of the class or class template.
    [Note 4: A complete-class context of a nested class is also a complete-class context of any enclosing class, if the nested class is defined within the member-specification of the enclosing class. — end note]

    So, before the class is complete, only the declarations of each members are parsed, then the complete-class contexts are parsed, at which point the definition of A is also complete ([class.pre]p2):

    A class is considered defined after the closing brace of its class-specifier has been seen even though its member functions are in general not yet defined.

    So, the order the compiler parses it is:

    struct C {
        struct A; // #2
        struct B {
            A* a;
    
            int foo(); // Body is a complete-class context and is only parsed when B and C are complete
        };  // Closing } means B is now defined so it's declarations are pased
    
        struct A {
            int value;
        };  // A is complete now
    
        A a; B b;
        C(int x);
    };  // C is now complete, go back and parse the complete class context
    
    int C::B::foo() {
        return a->value;  // int A::value is visible here
    }
    
    C::C(int x) : a{x}, b{&a} {}