Resolve build errors due to circular dependency amongst classes

Viewed 270616

I often find myself in a situation where I am facing multiple compilation/linker errors in a C++ project due to some bad design decisions (made by someone else :) ) which lead to circular dependencies between C++ classes in different header files (can happen also in the same file). But fortunately(?) this doesn't happen often enough for me to remember the solution to this problem for the next time it happens again.

So for the purposes of easy recall in the future I am going to post a representative problem and a solution along with it. Better solutions are of-course welcome.


  • A.h

    class B;
    class A
    {
        int _val;
        B *_b;
    public:
    
        A(int val)
            :_val(val)
        {
        }
    
        void SetB(B *b)
        {
            _b = b;
            _b->Print(); // COMPILER ERROR: C2027: use of undefined type 'B'
        }
    
        void Print()
        {
            cout<<"Type:A val="<<_val<<endl;
        }
    };
    

  • B.h

    #include "A.h"
    class B
    {
        double _val;
        A* _a;
    public:
    
        B(double val)
            :_val(val)
        {
        }
    
        void SetA(A *a)
        {
            _a = a;
            _a->Print();
        }
    
        void Print()
        {
            cout<<"Type:B val="<<_val<<endl;
        }
    };
    

  • main.cpp

    #include "B.h"
    #include <iostream>
    
    int main(int argc, char* argv[])
    {
        A a(10);
        B b(3.14);
        a.Print();
        a.SetB(&b);
        b.Print();
        b.SetA(&a);
        return 0;
    }
    
12 Answers

The way to think about this is to "think like a compiler".

Imagine you are writing a compiler. And you see code like this.

// file: A.h
class A {
  B _b;
};

// file: B.h
class B {
  A _a;
};

// file main.cc
#include "A.h"
#include "B.h"
int main(...) {
  A a;
}

When you are compiling the .cc file (remember that the .cc and not the .h is the unit of compilation), you need to allocate space for object A. So, well, how much space then? Enough to store B! What's the size of B then? Enough to store A! Oops.

Clearly a circular reference that you must break.

You can break it by allowing the compiler to instead reserve as much space as it knows about upfront - pointers and references, for example, will always be 32 or 64 bits (depending on the architecture) and so if you replaced (either one) by a pointer or reference, things would be great. Let's say we replace in A:

// file: A.h
class A {
  // both these are fine, so are various const versions of the same.
  B& _b_ref;
  B* _b_ptr;
};

Now things are better. Somewhat. main() still says:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#include, for all extents and purposes (if you take the preprocessor out) just copies the file into the .cc. So really, the .cc looks like:

// file: partially_pre_processed_main.cc
class A {
  B& _b_ref;
  B* _b_ptr;
};
#include "B.h"
int main (...) {
  A a;
}

You can see why the compiler can't deal with this - it has no idea what B is - it has never even seen the symbol before.

So let's tell the compiler about B. This is known as a forward declaration, and is discussed further in this answer.

// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
  A a;
}

This works. It is not great. But at this point you should have an understanding of the circular reference problem and what we did to "fix" it, albeit the fix is bad.

The reason this fix is bad is because the next person to #include "A.h" will have to declare B before they can use it and will get a terrible #include error. So let's move the declaration into A.h itself.

// file: A.h
class B;
class A {
  B* _b; // or any of the other variants.
};

And in B.h, at this point, you can just #include "A.h" directly.

// file: B.h
#include "A.h"
class B {
  // note that this is cool because the compiler knows by this time
  // how much space A will need.
  A _a; 
}

HTH.

You can avoid compilation errors if you remove the method definitions from the header files and let the classes contain only the method declarations and variable declarations/definitions. The method definitions should be placed in a .cpp file (just like a best practice guideline says).

The down side of the following solution is (assuming that you had placed the methods in the header file to inline them) that the methods are no longer inlined by the compiler and trying to use the inline keyword produces linker errors.

//A.h
#ifndef A_H
#define A_H
class B;
class A
{
    int _val;
    B* _b;
public:

    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

//B.h
#ifndef B_H
#define B_H
class A;
class B
{
    double _val;
    A* _a;
public:

    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

//A.cpp
#include "A.h"
#include "B.h"

#include <iostream>

using namespace std;

A::A(int val)
:_val(val)
{
}

void A::SetB(B *b)
{
    _b = b;
    cout<<"Inside SetB()"<<endl;
    _b->Print();
}

void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

//B.cpp
#include "B.h"
#include "A.h"
#include <iostream>

using namespace std;

B::B(double val)
:_val(val)
{
}

void B::SetA(A *a)
{
    _a = a;
    cout<<"Inside SetA()"<<endl;
    _a->Print();
}

void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

//main.cpp
#include "A.h"
#include "B.h"

int main(int argc, char* argv[])
{
    A a(10);
    B b(3.14);
    a.Print();
    a.SetB(&b);
    b.Print();
    b.SetA(&a);
    return 0;
}

Things to remember:

  • This won't work if class A has an object of class B as a member or vice versa.
  • Forward declaration is way to go.
  • Order of declaration matters (which is why you are moving out the definitions).
    • If both classes call functions of the other, you have to move the definitions out.

Read the FAQ:

I once solved this kind of problem by moving all inlines after the class definition and putting the #include for the other classes just before the inlines in the header file. This way one make sure all definitions+inlines are set prior the inlines are parsed.

Doing like this makes it possible to still have a bunch of inlines in both(or multiple) header files. But it's necessary to have include guards.

Like this

// File: A.h
#ifndef __A_H__
#define __A_H__
class B;
class A
{
    int _val;
    B *_b;
public:
    A(int val);
    void SetB(B *b);
    void Print();
};

// Including class B for inline usage here 
#include "B.h"

inline A::A(int val) : _val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif /* __A_H__ */

...and doing the same in B.h

Unfortunately, all the previous answers are missing some details. The correct solution is a little bit cumbersome, but this is the only way to do it properly. And it scales easily, handles more complex dependencies as well.

Here's how you can do this, exactly retaining all the details, and usability:

  • the solution is exactly the same as originally intended
  • inline functions still inline
  • users of A and B can include A.h and B.h in any order

Create two files, A_def.h, B_def.h. These will contain only A's and B's definition:

// A_def.h
#ifndef A_DEF_H
#define A_DEF_H

class B;
class A
{
    int _val;
    B *_b;

public:
    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

// B_def.h
#ifndef B_DEF_H
#define B_DEF_H

class A;
class B
{
    double _val;
    A* _a;

public:
    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

And then, A.h and B.h will contain this:

// A.h
#ifndef A_H
#define A_H

#include "A_def.h"
#include "B_def.h"

inline A::A(int val) :_val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif

// B.h
#ifndef B_H
#define B_H

#include "A_def.h"
#include "B_def.h"

inline B::B(double val) :_val(val)
{
}

inline void B::SetA(A *a)
{
    _a = a;
    _a->Print();
}

inline void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

#endif

Note that A_def.h and B_def.h are "private" headers, users of A and B should not use them. The public header is A.h and B.h.

Unfortunately I can't comment the answer from geza.

He is not just saying "put forward declarations into a separate header". He says that you have to spilt class definition headers and inline function definitions into different header files to allow "defered dependencies".

But his illustration is not really good. Because both classes (A and B) only need an incomplete type of each other (pointer fields / parameters).

To understand it better imagine that class A has a field of type B not B*. In addition class A and B want to define an inline function with parameters of the other type:

This simple code would not work:

// A.h
#pragme once
#include "B.h"

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}

// B.h
#pragme once
class A;

class B{
  A* b;
  inline void Do(A a);
}

#include "A.h"

inline void B::Do(A a){
  //do something with A
}

//main.cpp
#include "A.h"
#include "B.h"

It would result in the following code:

//main.cpp
//#include "A.h"

class A;

class B{
  A* b;
  inline void Do(A a);
}

inline void B::Do(A a){
  //do something with A
}

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}
//#include "B.h"

This code does not compile because B::Do needs a complete type of A which is defined later.

To make sure that it compiles the source code should look like this:

//main.cpp
class A;

class B{
  A* b;
  inline void Do(A a);
}

class A{
  B b;
  inline void Do(B b);
}

inline void B::Do(A a){
  //do something with A
}

inline void A::Do(B b){
  //do something with B
}

This is exactly possible with these two header files for each class wich needs to define inline functions. The only issue is that the circular classes can't just include the "public header".

To solve this issue I would like to suggest a preprocessor extension: #pragma process_pending_includes

This directive should defer the processing of the current file and complete all pending includes.

In some cases it is possible to define a method or a constructor of class B in the header file of class A to resolve circular dependencies involving definitions. In this way you can avoid having to put definitions in .cc files, for example if you want to implement a header only library.

// file: a.h
#include "b.h"
struct A {
  A(const B& b) : _b(b) { }
  B get() { return _b; }
  B _b;
};

// note that the get method of class B is defined in a.h
A B::get() {
  return A(*this);
}

// file: b.h
class A;
struct B {
  // here the get method is only declared
  A get();
};

// file: main.cc
#include "a.h"
int main(...) {
  B b;
  A a = b.get();
}

First we need a few definitions.

Definitions

Declaration

extern int n;
int f();
template<typename T> int g(T);
struct A;
template<typename T> struct B;

Definition

int n;
int f() { return 42; }
template<typename T> int g(T) { return 42; }
struct A { int f(); };
template<typename T> struct B { int g(T*); };

The difference is that repeating a definition causes a One Definition Rule (ODR) violation. The compiler will give an error along the lines of "error: redefinition of '...'".

Note that a "forward declaration" is just a declaration. Declarations can be repeated since they don't define anything and therefore cause no ODR.

Note that default arguments may only be given once, possibly during the declaration, but only for one of the declarations if there are multiple. Therefore one could argue that that is a definition because it may not be repeated (and in a sense it is: it defines the default arguments). However, since it doesn't define the function or template, lets call those a declaration anyway. Default arguments will be ignored below.

Function definitions

(Member) function definitions generate code. Having multiple of those (in different Translation Units (TU's), otherwise you'd get an ODR violation already during compile time) normally leads to a linker error; except when the linker resolves the collision which it does for inline functions and templated functions. Both might or might not be inlined; if they are not 100% of the time inlined then a normal function (instantiation) needs to exist; that might cause the collision that I am talking about.

Non-inline, non-template (member) functions need to exist only in a single TU and should therefore be defined in a single .cpp.

However, inline- and/or template (member) functions are defined in headers, which might be included by multiple TU's, and therefore need special treatment by the linker. They too are considered to generate code however.

Class definitions

Class definitions might or might not generate code. If they do, then that is for functions that the linker will resolve any collisions of.

Of course, any member function that is defined inside the class is per definition "inline". If it is a problem that such a function is defined during the declaration of the class, it can simply be moved outside the class declaration.

Instead of,

struct A {
  int f() const { return 42; }
};

do

struct A {
  inline int f() const;
}; // struct declaration ends here.

int A::f() const { return 42; }

Therefore we are mostly interested in code generation (function instantiations) that both, can not be moved outside the class declaration and requires some other definition in order to be instantiated.

It turns out that this usually involves smart pointers and default destructors. Assume that struct B can not be defined, only declared, and struct A looks as follows:

struct B;
struct A { std::unique_ptr<B> ptr; };

then an instantiation of A while the definition of B is not visible (some compilers might not mind if B is defined later in the same TU) will cause an error because both, the default constructor as well as the destructor of A, cause the destructor of unique_ptr<B> to be generated, which needs the definition of B [e.g. error: invalid application of ‘sizeof’ to incomplete type ‘B’]. There is still a way around this though: do not use generated default constructor/destructor.

For example,

struct B;
struct A {
  A();
  ~A();
  std::unique_ptr<B> ptr;
};

will compile and just have two undefined symbols for A::A() and A::~A() which you can still compile inline outside of the definition of A as before (provided you define B before you do so).

Three parts, three files?

As such we can distinguish three part of a struct/class definition that we could each put in a different file.

  1. The (forward) declaration:

    A.fwd.h

  2. The class definition:

    A.h

  3. The inline and template member function definitions:

    A.inl.h

And then there is of course A.cpp with the non-inline and non-template member function definitions; but those are not relevant for circular header dependencies.

Ignoring default arguments, declarations won't require any other declaration or definition.

Class definitions might require certain other classes to be declared, yet others to be defined.

Inline/template member functions might require additional definitions.

We can therefore create the following example that show all possibilities:

struct C;
struct B
{
  B();
  ~B();
  std::unique_ptr<C> ptr;  // Need declaration of C.
};

struct A
{
  B b;    // Needs definition of B.
  C f();  // Needs declaration of C.
};

inline A g()  // Needs definition of A.
{
  return {};
}

struct D
{
  A a = g();  // Needs definition of A.
  C c();      // Needs declaration of C.
};

where B::B(), B::~B(), C A::f() and C D::c() are defined in some .cpp.

But, lets inline those as well; at that point we need to define C because all four need that (B::B and B::~B because of the unique_ptr, see above). And doing so in this TU then suddenly makes it unnecessary to put B::B() and B::~B() outside of the definition of B (at least with the compiler that I am using). Nevertheless, lets keep B as it is.

Then we get:

// C.fwd.h:
struct C;

// B.h:
struct B
{
  inline B();
  inline ~B();
  std::unique_ptr<C> ptr;
};

// A.h:
struct A
{
  B b;
  inline C f();
};

// D.h:
inline A g()
{
  return {};
}
struct D
{
  A a = g();
  inline C c();
};

// C.h:
struct C {};

// B.inl.h:
B::B() {}
B::~B() {}

// A.inl.h:
C A::f()
{
  D d;
  return d.c();
}

// D.inl.h:
C D::c()
{
  return {};
}

In other words, the definition of A looks like this:

// A.fwd.h:
struct A;
// A.h:
#include "B.h"      // Already includes C.fwd.h, but well...
#include "C.fwd.h"  // We need C to be declared too.
struct A
{
  B b;
  inline C f();
};
// A.inl.h:
#include "A.h"
#include "C.h"
#include "D.inl.h"
C A::f()
{
  D d;
  return d.c();
}

Note that in theory we could make multiple .inl.h headers: one for each function, if otherwise it drags in more than required and that causes a problem.

Forbidden patterns

Note that all #include's are at the top of all files.

(In theory) .fwd.h headers do not include other headers. Therefore they can be included at will and never lead to a circular dependency.

.h definition headers might include a .inl.h header, but if that leads to a circular header dependency then that can always be avoided by moving the function that uses the inlined function from that .inl.h to the .inl.h of the current class; in the case of smart pointers that might require to also move the destructor and/or constructor to that .inl.h.

Hence, the only remaining problem is a circular inclusion of .h definition headers, ie A.h includes B.h and B.h includes A.h. In that case you must decouple the loop by replacing a class member with a pointer.

Finally, it is not possible to have a loop of pure .inl.h files. If that is necessary you probably should move them to a single file in which case the compiler might or might not be able to solve the problem; but clearly you can't get ALL functions inlined when they use eachother, so you might as well manually decide which can be non-inlined.

Related