How to Fix Include Loops in C++ Projects: A Guide to Forward Declarations

How to Fix Include Loops in C++ Projects: A Guide to Forward Declarations

In C/C++, header files offer a great way to "extend" or "re-use" existing libraries, enabling us to build upon what's already been created. These files typically contain function declarations, macro definitions, and sometimes classes or structs. Depending on the library, some of these elements might be hidden in .c or .cpp files to keep certain implementation details private. C++, however, introduces the concept of "self-contained" header files, allowing us to package both declarations and implementations within the same header file.

Traditionally, header files are used to inform our programmes about available functions, their signatures, and return types. We then link them to pre-compiled binaries, either statically or dynamically. With self-contained headers, the linking step is unnecessary, as the header files carry not only the definitions but also their corresponding implementations. This simplifies the compilation process, bundling everything into a single binary with much less hassle.

How Do We Use Header Files?

To use a C/C++ header file, we simply include it in the source or header file where it's needed. For example, to use standard C++ libraries, we include their headers like this:

#include <iostream> // For cin/cout
#include <memory>   // For smart pointers

Pretty straightforward, right?

Include Loops

In small projects, include loops are rarely a problem, but as projects grow more complex, they become a common issue. So, what exactly is an include loop?

An include loop, or circular dependency, occurs when two files depend on each other. For example, if class A depends on class B and vice versa.

What's the Risk?

If you include b.h inside a.h, the compiler brings the contents of class B into class A. But if class B also includes a.h, it creates a circular dependency where both files endlessly depend on each other. When this happens, the C++ compiler will throw an error, forcing you to break the loop.

The Solution?

The C++ committee recommends breaking this circular dependency. While it sounds simple, it can be tricky. Read the blog post here.

  • For small projects, you might solve the problem by placing both class A and class B in the same header file. However, for more complex projects, this solution becomes impractical.
  • A better approach is to use forward declarations. Let’s dive into how this solves the issue.

Using Forward Declarations

Forward declaration is simply telling the compiler that a class exists without providing its full definition. This is typically done in the header file. For both a.h and b.h, we forward declare the other class without including the full header file. This way, class B is known inside a.h, and class A is known inside b.h without actually including the full definition.

// a.h header file

... other code

class B;

class A {
    A() {}

};


// b.h header file

... other code

class A;

class B {
    B() {}

};


The Catch?

While forward declaration resolves the circular dependency, it comes with a limitation: you can't create an instance of class B inside class A because the compiler doesn't know its size. To work around this, we declare a pointer to class B instead, which can later be allocated on the heap. This ensures the compilation works as long as we avoid accessing the nullptr.


If you are new to pointers, check out my previous post on pointer basics.

Pointers in Programming: Essential Concepts and Memory Management - Part 1
If you’ve delved into the world of software engineering, you’ve likely encountered the term “pointers.” This concept is closely tied to what some programming languages refer to as “memory safety,” a term often associated with languages like Rust or those with garbage collectors like Go, JavaScript, and others. But why

// a.h 

class B;

class A {
    A();
    
private:
    // B b; // Error
    B* b_ptr; // Works OK!

};

Finally, in the source file, we include the header files of the forward-declared classes to make the full definition available. The key here is that include guards prevent multiple copies of the same header file from being loaded, ensuring smooth compilation.

// a.cpp

...
#include "b.h"
...

// b.cpp

...
#include "a.h"
...

Conclusion

And there you have it! By understanding the risks of include loops and the power of forward declarations, you can avoid common pitfalls when managing dependencies in C++. Below is the full source code to explore. Happy coding!

// -----------------------------------------
// a.h
// -----------------------------------------

#ifndef A_H
#define A_H

#include <iostream>

class B;

class A
{
public:
    A();
    ~A();
private:
    B* b_ptr;
};

#endif // A_H


// -----------------------------------------
// a.cpp
// -----------------------------------------

#include "a.h"
#include "b.h"

A::A()
{
    // Create B instance
    b_ptr = new B();
}

A::~A()
{
    // Remember to clean up the ptr
    delete b_ptr;
}


// -----------------------------------------
// b.h
// -----------------------------------------

#ifndef B_H
#define B_H

#include <iostream>

class A; // Forward declare

class B
{
public:
    B();
    ~B();
private:
    A* a_ptr;
};

#endif // B_H


// -----------------------------------------
// b.cpp
// -----------------------------------------

#include "b.h"
#include "a.h"

B::B()
{
    // Create A instance
    a_ptr = new A();
}

B::~B()
{
    // Remember to clean up the ptr
    delete a_ptr;
}