Skip to content

Instantly share code, notes, and snippets.

@iTrooz
Created June 1, 2022 07:20
Show Gist options
  • Save iTrooz/44ae8d50b401a61bcaa8718dcbaf7266 to your computer and use it in GitHub Desktop.
Save iTrooz/44ae8d50b401a61bcaa8718dcbaf7266 to your computer and use it in GitHub Desktop.
What are circular imports ?

Let's talk about C++, and more precisely, circular imports and the hell they are

How importations work in C++

Let's imagine we have theses 3 files :

main.cpp :

#include<iostream>
#include "myClass.h"

int main(){
    myClass obj;
    return 0;
}

myClass.h

#include "mySecondClass.h"


class myClass{
public:
	mySecondClass second;
};

mySecondClass.h

class mySecondClass{
public:
    int data;
};

The include statement in C++ does nothing more than copying the content of the file wanted, and pasting it in the calling file.

In this example, after executing the preprocessor (the one charged with interpreting the #include statements), we would have a code looking like this :

tmpfile

#include<iostream>
class mySecondClass{
public:
    int data;
};


class myClass{
public:
	mySecondClass second;
};

int main(){
    myClass obj;
    return 0;
}

This include statement is dumb and will copy contents of the file without any verification of any kind. And that can cause problems, as we are going to see

I'm going to assume you already use header guards (the #ifndef statement in all your header files) If you don't/would like to know how they work, you can check this bonus chapter

Circular imports

Let's take the follow example :

main.cpp

#include "myClass.h"

using namespace std;

int main(){
	myClass obj;
	printClass(obj);

	return 0;
}

myClass.h

#ifndef MYCLASS
#define MYCLASS

#include "myUtils.h"

class myClass{
public:
	int data;
};

#endif

myUtils.h

#ifndef MYUTILS
#define MYUTILS

#include<iostream>
#include "myClass.h"

using namespace std;

void printClass(myClass& cl);

#endif

myUtils.cpp

#include "myUtils.h"

void printClass(myClass& cl){
	cout << cl.data << endl;
}

I will not show the myUtils.cpp file every time since it won't change for now

Notice that the only differences between this example and the last one is that I added header guards, and that I moved the include "myUtils.h" inside the myClass.h file. This shouldn't break anything... right ?

When the code is compiled, we get a nice error inside myUtils.h about myClass not being declared. But we declared and imported it !

Let's take a look at this from the view of the preprocessor : main.cpp includes myClass.h, so myClass.h is copied but we see that myClass.h includes myUtils.h, so myUtils.h is copied but we see that myUtils.h also requires myClass.h, so... wait, we were already copying that file ! Thankfully, the header guards see this, and "cancel the importation". The problem is that now, the myClass.h importation inside myUtils.h is cancelled, so myUtils.h doesn't know about myClass.h !

What I just described is called a circular importation Two files mutually import themselves, and that REALLY causes a mess.

Don't be afraid to re-read this statement a few times, and read back the code to be sure to understand the problem, because you will not be able to fix it if you don't understand it

Solutions

Short answer : there is no magical way to fix this, there is no "remove circular imports button", at least not in any IDE I know of. You will need to modify your code manually in a way to prevent these circular importations from happening

The first thing you should do, is break down your files in smaller pieces. If you have more files, more specific, you will be less likely to have a circular import error.

If you still encounter circular imports errors (and you will), here is a way to handle them :

You can avoid importing a class using class <myClassName>; instead of #include "fileOfMyClass". That statement will tell C++ that a class with this name exists, and that even if it doesn't know its declaration, it should use it. This only works in files where you only use pointers and references to these classes, because else C++ will need to know the declaration of the class For example, you can replace

#ifndef MYUTILS
#define MYUTILS

#include<iostream>
#include "myClass.h"

using namespace std;

void printClass(myClass& cl);

#endif

with

#ifndef MYUTILS
#define MYUTILS

#include<iostream>
class myClass;

using namespace std;

void printClass(myClass& cl);

#endif

and yay, the code now compiles ! But you cannot do

#ifndef MYUTILS
#define MYUTILS

#include<iostream>
class myClass;

using namespace std;

void printClass(myClass cl);

#endif

Did you spot the difference ? I modified the printClass() function to take a myClass as argument instead of a myClass&. And this code will not compile because of it.

One last thing to modify : you remember the myUtils.cpp file ? Before, it was getting the declaration of myClass with the #include "myClass.h" statement in myUtils.h Now, we need to include that file directly inside the myUtils.cpp, that will now look like this :

myUtils.cpp

#include "myUtils.h"
#include "myClass.h"

void printClass(myClass& cl){
	cout << cl.data << endl;
}

You have two ways to apply this solution. You can either use the class <name>; statement every time it's possible to do so, so you are sure you never, ever (99%) run into circular import errors Or you can code normally, and track circular errors to put class <name>; where needed for the code to compiles Both work :)

If this "article" helped you, write me a comment ! :)

NOTICE : This is a bonus chapter of the article (to understand header guards). If you are reading this by scrolling past the end of the article... Well you should not do this

Header guards

Now, let's imagine this example (notice I removed the mySecondClass object)

main.cpp

#include "myClass.h"
#include "myUtils.h"

using namespace std;

int main(){
	myClass obj;
	printClass(obj);

	return 0;
}

myClass.h

class myClass{
public:
	int data;
};

myUtils.h

#include<iostream>
#include "myClass.h"

using namespace std;

void printClass(myClass& cl);

When we compile this example, we get an error about myClass being declared twice. If we run the preprocessor, we see that it is indeed declared twice :

tmpfile

class myClass{
public:
	int data;
};
#include<iostream>
class myClass{
public:
	int data;
};

using namespace std;

void printClass(myClass& cl);

using namespace std;

int main(){
	myClass obj;
	printClass(obj);

	return 0;
}

to fix this, we need to introduce Header Guards

Header Guards are condition that you put at the start and end of each header files (not .cpp files, since theses are never imported anywhere !)

They essentially check if the file is already imported, and if so, they "cancel" the current importation. They do that by checking if a variable (typically the name of the file) exists, and if not, they create it and import the file

Our files would look like this with Header Guards :

main.cpp

#include "myClass.h"
#include "myUtils.h"

using namespace std;

int main(){
	myClass obj;
	printClass(obj);

	return 0;
}

myClass.h

#ifndef MYCLASS
#define MYCLASS
class myClass{
public:
	int data;
};
#endif

myUtils.h

#ifndef MYTUILS
#define MYTUILS
#include<iostream>
#include "myClass.h"

using namespace std;

void printClass(myClass& cl);
#endif

and with the preprocessor (without executing the header guards):

tmpfile

#ifndef MYCLASS
#define MYCLASS
class myClass{
public:
	int data;
};
#endif
#ifndef MYTUILS
#define MYTUILS
#include<iostream>
#ifndef MYCLASS
#define MYCLASS
class myClass{
public:
	int data;
};
#endif

using namespace std;

void printClass(myClass& cl);
#endif

using namespace std;

int main(){
	myClass obj;
	printClass(obj);

	return 0;
}

now we process the header guards (they are also managed by the preprocessor):

tmpfile

class myClass{
public:
	int data;
};
#include<iostream>

using namespace std;

void printClass(myClass& cl);

using namespace std;

int main(){
	myClass obj;
	printClass(obj);

	return 0;
}

And now we have a clean code, that compiles !

That's it ! Now you can go back to the actual article

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment