Zebrafish
Zebrafish

Reputation: 14308

Is this why a project can fail to compile if two headers include each other?

I'm trying to understand why two mutually included headers (each header including the other) doesn't work the way you might expect. I was wondering if my reasoning is correct:

If you have FooHeader.h:

#pragma once
#include "BarHeader.h"

struct Foo { Bar bar;};

And a BarHeader.h:

#pragma once
#include "FooHeader.h"
struct Bar { Foo foo; };

The reason why this might not compile is the following:

Say you have main.cpp that:

#include "BarHeader.h"

int main() { }

Now the preprocessor does it's job, replaces the "BarHeader.h" so that it looks like this:

|remove|
#include "BarHeader.h"

|add|
#include "FooHeader.h"
struct Bar { Foo foo; };

int main() {}

OK, that's step 1. But the preprocessor still has work to do, to substitute "FooHeader.h", so the next step is:

|remove|
#include "FooHeader.h"

|add|
#include "BarHeader.h"
struct Foo { Bar bar;}



struct Bar { Foo foo; }

int main() {}

Now the file looks like this:

#include "BarHeader.h"


struct Foo { Bar bar;}

struct Bar { Foo foo; }

int main() {}

It still has an include there, BUT, seeing as that header file has already been included, it doesn't include it again, so the end result is:

struct Foo { Bar bar;}
    
struct Bar { Foo foo; }
    
int main() {}

This is why there are compilation problems, right?

Wouldn't this be solved if the preprocessor simply moved the already included header to the point of the it's highest/first occurrence within the file? So something like "I see I have already included you, but you've been included up here, so I'll move it up here, and then things are fine."? That would solve the problem right?

Upvotes: 0

Views: 155

Answers (2)

Thibe
Thibe

Reputation: 381

No, circular includes of header files does not cause any problems when this is protected by include guards like #pragma once.
The include guards make sure that you can include FooHeader.h and BarHeader.h as often and in any order you like, including any recursions.

Edit: As pointed out by a comment, circular includes can cause problems, see bottom section.

Circular includes can and probably should be omitted but that has nothing to do with compile errors but more with clean code and fewer include dependencies (how much do you have to recompile when you change a single header file).

Your example with Foo and Bar cannot work, no matter if you put it into one or two header files.
Besides of the definition problem, the size of the structures would be infinitly large.
At least one of the two type definitions requires to use a pointer or reference to a forward declaration to resolve the circular dependency.
This forward declaration also helps to get rid of the circular include as one of the two includes is no longer needed.

The pointer with the option to be a nullptr is also one way to stop the infitive instance recursion.
Something like this:

FooHeader.h

#pragma once
#include "BarHeader.h"

struct Foo { struct Bar bar; };

BarHeader.h

#pragma once
// no include needed
struct Foo; // forward declaration which is sufficient for pointers

struct Bar { struct Foo* pFoo; };

Edit: Circular includes can cause problems when the inner header tries to access a symbol which is not yet defined on the outer header.
The following non-recursive type example should cause such issues:

FooHeader.h

#pragma once
#include "BarHeader.h"

struct Foo { struct Bar2 bar; };

BarHeader.h

#pragma once
#include "FooHeader.h"

struct Bar { struct Foo foo; };
struct Bar2 { int i; };

Every header on its own looks fine but the definitions cannot be fully resolved.
When you include FooHeader.h, then struct Foo foo in BarHeader.h cannot be resolved.
When you include BarHeader.h, then struct Bar2 bar in FooHeader.h cannot be resolved. The definitions would work when they are put in the order

  1. Bar2
  2. Foo
  3. Bar

Upvotes: 2

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275820

#include "BarHeader.h"

int main() { }

becomes

// #include "BarHeader.h"
#pragma once // BarHeader
#include "FooHeader.h"
struct Bar { Foo foo; };

int main() { }

Then we expand the #include directive

// #include "BarHeader.h"
#pragma once // BarHeader
// #include "FooHeader.h"
#pragma once // FooHeader
#include "BarHeader.h"

struct Foo { Bar bar;};
struct Bar { Foo foo; };

int main() { }

now we expand #include "BarHeader.h" - this does nothing, because #pragma once "BarHeader.h" has already been evaluated, and we included 'BarHeader.h".

// #include "BarHeader.h"
#pragma once // BarHeader
// #include "FooHeader.h"
#pragma once // FooHeader
// #include "BarHeader.h"

struct Foo { Bar bar;};
struct Bar { Foo foo; };

int main() { }

and we get an error.

Your rule, "moving it to the earliest spot", makes no sense - we are already party way through BarHeader.h when we run into another BarHeader.h include. You can't "rewind" it.

The techniques to deal with it are easy to understand.

What more, your specific example isn't solvable by any header order. You cannot have class A inside class B, because classes in C++ are actual instances of the class not references to the class (as in C#/Java/etc).

The pre-declaration rules are actually there to help avoid circular structure definitions.

Upvotes: 1

Related Questions