There is a similar rule in most style guides:
Include the .hpp in the corresponding .cpp as the first substantive line of code. Even if the .cpp is otherwise empty.
The latter sentence is popularized by John Lakos. See e.g. CppCon 2016: John Lakos “Advanced Levelization Techniques (part 1 of 3)", at 7:28:
This rule ensures, that our headers are self-contained, i.e. they compile in isolation, or in other words they don't require other headers in front of them to compile.
But, does this ensure, that we will never have include order dependencies?

It ensures in most cases that we will not have include order dependencies, and does not ensure it in at least the following cases.
1. Ordinary lookup of dependent names
There are three different roles in this story.
N::MyClassclass, which is intended to be used with FooBarBaz lib.N::MyClasswith FooBarBaz, but runs into problems.Alfred
Alfred plays first. FooBarBaz, this magnificent library has some functions which operate on client types, which are accepted as template parameters. The client's types are supposed to have a function named
FBBSize()whichint, andFBBSize()with ADL.The library calls
FBBSize()in its function templates, and it even has a conceptSupportsFBBSizewhich tells if a type has a correspondingFBBSize()function. Alfred's header-only libraryFBB.hppmight look something like this:Bob
Then, Bob writes a class
MyClassin namespaceNand wants to support its usage with the FooBarBaz lib. Thus, he declares anFBBSize()function in the header, but erroneously he puts the function into the global namespace. HisMyClass.hpplooks like this:Goofy
Now, both headers compile in isolation, but if Goofy wants to use
FBB::SomeFunction()withN::MyClass, only this include order will compile:If you swap the
#includedirectives, you get a compilation error.The reason is arcane. In
SomeFunction()FBBSize()is an unqualified dependent name, which is looked up usingIn the order as above,
FBBSize()is found using ordinary lookup (contrary to Alfred's intention) in the context of the definition ofSomeFunction(), i.e. "above"SomeFunction(). ADL finds nothing, becauseFBBSize()isn't in namespaceN. However, if you swap the#includedirectives,FBBSize()will be declared "below"SomeFunction(), so neither lookup will find it, and compilation will fail.It's even harder to notice the bug, if instead calling
FBB::SomeFunction()Goofy uses the conceptFBB::SupportsFBBSize. Themain()function will compile in both include orders, however, the value of the concept will depend on the order:The above
main()function returnstrue, but if you swap the#includedirectives, it will returnfalse.Solution
Goofy would have got the same result, i.e. the same compilation errors, and the same value for the concept, in both orders of the
#includedirectives,MyClass.hppBob would have putFBBSize()into namespaceN, orFBB.hppAlfred would have somehow "disabled" ordinary lookup ofFBBSize().The problem with ordinary lookup is not that it's ordinary, but that it's executed in the template definition context. If
FBBSize()is to be found "above" the template definition,FBB.hppwill require the header of the client's class in front of it to work as intended.This is not an actual, but a potential include order dependency. We could say,
FBB.hpphas the potential in it, to depend on the header of the client's class.To eliminate that potential, Alfred has to stop ordinary lookup of
FBBSize()at the scope of namespaceFBB. He could put a dummy declaration of anFBBSize()function in namespaceFBBbefore all facilities of the header:The dummy declaration has to be a function. If it would be a variable or a type, ADL would not be executed.
Now, when Goofy tries to compile his 1st example (which uses
FBB::SomeFunction()), it won't compile in any include order, because ADL finds nothing. It will always work the same way in both include orders, becauseFBBSize()can only be found in the instantiation context.The comment after the dummy declaration is a reminder for the clients. The compiler will show it in the error message when it cites that line from the code. It's a workaround until you cannot use
= delete("custom error message"), see EWG152.Goofy's 2nd example (which uses
FBB::SupportsFBBSize) will also work the same way in both include orders. The value of the concept will befalsein both cases.You can play with the above code snippets on Godbolt.
2. Qualified lookup of dependent names
Recall that the problem with ordinary lookup is not that it's ordinary, but that it's executed in the template definition context. Qualified lookup is also executed in the template definition context, and introduces the same potential include order dependency.
I mention this example for the sake of completeness, but I don't think, that it's a real life problem.
There are the same three actors in this play.
N::MyClassclass, which is intended to be used with FooBarBaz lib.N::MyClasswith FooBarBaz, but runs into problems.Alfred
This time Alfred wants the client types to have a
Size()function, whichint, andFBB).Alfred's header:
He implemented a "default"
Size()function. This way the client can put a publicSizeconstant in his class instead of declaring aSize()function. Another reason for having this function in front of every call to it, is that without itFBB.hppwon't compile in isolation. Compilers would notice, that there is noSizein namespaceFBB, therefore they can prove, that no specialization of these templates compile, and report the error early.Bob
Bob does everything according to FooBarBaz's documentation:
This header also compiles in isolation.
Goofy
Goofy wants to use
FBB::SomeFunction()andFBB::SupportsFBBSizewithN::MyClass:The 1st example compiles, and in the 2nd the
main()function returnstrue. However, if you swap the#includedirectives, the 1st won't compile, and the 2nd will returnfalse.Solution
There is no solution for this potential include order dependency.
You can play with the above code snippets on Godbolt.
3. Precompiled headers
Suppose, you work for a company, where for every DLL there is a dedicated precompiled header. Therefore, these headers can differ.
As a consequence by following the style guide rule you can verify that your header compiles in isolation, but that verification will be only valid in the DLL of the header, and not in the entire code base.
Suppose you write a class
MonkeyinZoo.dll, which uses classX. You forget to includeX.hpp, but supposeX.hppis inZoo.dll's precompiled header:Monkey.cppincludesMonkey.hppas the first substantive line of code. It compiles all right, because when it compiles,Zoo.dll's precompiled header (andX.hppin it) will be silently included in front of the entire translation unit.If however, someone else wants to use this header in another DLL, where
X.hppis not included in the precompiled header, they will get a compilation error.Solution
Two approaches come to mind.
#include "X.hpp"is not forgotten.B.dlldepends onA.dllthenB.pch.hppincludesA.pch.hpp.Library headers and Include What You Use is a different topic
This is a related problem, but not our topic. I mention it only because people seemingly think, that it belongs here.
The common example is the C++ Standard Library, where you use a facility, but you don't include the proper header. Consider the following
Fun.hpp:The type
std::int64_tis defined incstdint. In MSVCtype_traitsincludescstdint(see on GitHub), therefore,Fun.cpp(which includesFun.hppin its first line) compiles. But it doesn't compile with Clang and GCC. Demo.This is similar to an include order dependency. One could say,
Fun.hpprequirescstdintin front of it. But, in our topic we are talking about a fixed compiler, and about files under our control.Fun.hppis self-contained.Fun.cppwon't compile.Similarly, we could think about updating a 3rd party library to the latest version, and contrive similar examples. We could think about integrating code from other source branches. Finally, we could ask, what if MSVC won't include
cstdintintype_traitsin the future? All these problems are related, but different problems, and the solution to them is Include What You Use.In the above
Fun.hppwe run into compilation errors when porting from MSVC to a different compiler, but that's not becauseFoo.hppis not self-contained, but because we didn't include precisely what we use. We included the definition ofstd::int64_tindirectly.