Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mutually-dependent C++ classes, held by std::vector

Tags:

c++

vector

I was curious if was possible to create two classes, each of which holds a std::vector of the other. My first guess was that it would not be possible, because std::vector requires a complete type, not just a forward declaration.

#include <vector>

class B;
class A { std::vector<B> b; };
class B { std::vector<A> a; };

I would think that the declaration of std::vector<B> would cause an immediate failure, because B has an incomplete type at this point. However, this compiles successfully under both gcc and clang, without any warnings. Why doesn't this cause an error?

like image 956
Eldritch Cheese Avatar asked Feb 17 '16 15:02

Eldritch Cheese


2 Answers

T.C commented that this is actually undefined behavior which is being addressed in a change request to the standard. The rule which is violated is apparently [res.on.functions] 2.5:

In particular, the effects are undefined in the following cases: [...]

  • if an incomplete type (3.9) is used as a template argument when instantiating a template component, unless specifically allowed for that component.

The reason this works anyway (and the reason that it can be standardized) is two-fold:

  1. Your program only contains type definitions; no objects are created, no functions are called, no code is generated. If we simplify it by eliminating B's definition (it's not needed) and then try to create an instance of A, it fails:

    class B;
    class A { std::vector<B> b; };
    A a; // error: ctor and dtor undefined for incomplete type B.
    

    What also fails, as expected, is a simple

    std::vector<B> b;
    

    The reason in both cases is that the compiler must produce code, as opposed to a mere type declaration which is only grammatically relevant.

    The use of the vector type is ok also in other contexts:

    typedef std::vector<B> BVec;
    
  2. Class A can be defined because, as Nikolay correctly says in his answer, the size of std::vector<B>, and hence the size of A's member b, does not depend on the definition of B (because a vector holds a pointer to an array of elements, not an array proper).

like image 192
Peter - Reinstate Monica Avatar answered Nov 03 '22 16:11

Peter - Reinstate Monica


This compiles and runs ok because std::vector uses pointers and not the exact definition of A or B. You could modify your example to use single instance or array in the definition of the classes like so

class B;
class A { public: B b[2]; };
class B { public: A a[2]; };

This obviously fails to compile since you're trying to use definition of the classes. And the error by the compiler would be as you're expecting

error: field ‘b’ has incomplete type ‘B [2]’

However

class B;
class A { public: B* b; };
class B { public: A* a; };

would work just like std::vector. So you don't need fully defined class to use pointer or reference to this type.

Also there is simplified example with templates

template<typename T>
struct C {
    T* t;
};

class B;
class A { public: C<B> b; };
class B { public: C<A> a; };

This would also work and of course you can instantiate it

int main() {
    B b;
    A a;
}
like image 4
Nikolay K Avatar answered Nov 03 '22 15:11

Nikolay K