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.
There are three different roles in this story.
N::MyClass
class, which is intended to be used with FooBarBaz lib.N::MyClass
with FooBarBaz, but runs into problems.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()
which
int
, andFBBSize()
with ADL.The library calls FBBSize()
in its function templates, and it even has a concept SupportsFBBSize
which tells if a type has a corresponding FBBSize()
function. Alfred's header-only library FBB.hpp
might look something like this:
// --- FBB.hpp ---
namespace FBB {
template <class T>
concept SupportsFBBSize = requires(const T& value) { FBBSize(value); };
template <class T>
void SomeFunction(const T& value) {
FBBSize(value);
}
}
Then, Bob writes a class MyClass
in namespace N
and wants to support its usage with the FooBarBaz lib. Thus, he declares an FBBSize()
function in the header, but erroneously he puts the function into the global namespace. His MyClass.hpp
looks like this:
// --- MyClass.hpp ---
namespace N {
class MyClass {};
}
inline int FBBSize(const N::MyClass&) { return 123; }
Now, both headers compile in isolation, but if Goofy wants to use FBB::SomeFunction()
with N::MyClass
, only this include order will compile:
#include "MyClass.hpp"
#include "FBB.hpp"
int main() {
N::MyClass mc;
FBB::SomeFunction(mc);
}
If you swap the #include
directives, you get a compilation error.
The reason is arcane. In SomeFunction()
FBBSize()
is an unqualified dependent name, which is looked up using
In the order as above, FBBSize()
is found using ordinary lookup (contrary to Alfred's intention) in the context of the definition of SomeFunction()
, i.e. "above" SomeFunction()
. ADL finds nothing, because FBBSize()
isn't in namespace N
. However, if you swap the #include
directives, 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 concept FBB::SupportsFBBSize
. The main()
function will compile in both include orders, however, the value of the concept will depend on the order:
#include "MyClass.hpp"
#include "FBB.hpp"
int main() {
return FBB::SupportsFBBSize<N::MyClass>;
}
The above main()
function returns true
, but if you swap the #include
directives, it will return false
.
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 #include
directives,
MyClass.hpp
Bob would have put FBBSize()
into namespace N
, orFBB.hpp
Alfred would have somehow "disabled" ordinary lookup of FBBSize()
.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.hpp
will 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.hpp
has 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 namespace FBB
. He could put a dummy declaration of an FBBSize()
function in namespace FBB
before all facilities of the header:
// --- FBB.hpp ---
namespace FBB {
int FBBSize(...) = delete; // declare FBBSize() in the same file and namespace as your type
template <class T>
concept SupportsFBBSize = requires(const T& value) { FBBSize(value); };
template <class T>
void SomeFunction(const T& value) {
FBBSize(value);
}
}
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, because FBBSize()
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 be false
in both cases.
You can play with the above code snippets on Godbolt.
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::MyClass
class, which is intended to be used with FooBarBaz lib.N::MyClass
with FooBarBaz, but runs into problems.This time Alfred wants the client types to have a Size()
function, which
int
, andFBB
).Alfred's header:
// --- FBB.hpp ---
namespace FBB {
template <class T>
requires requires { T::Size; }
int Size(const T&) {
return T::Size;
}
template <class T>
concept SupportsFBBSize = requires(const T& value) { FBB::Size(value); };
template <class T>
void SomeFunction(const T& value) {
FBB::Size(value);
}
}
He implemented a "default" Size()
function. This way the client can put a public Size
constant in his class instead of declaring a Size()
function. Another reason for having this function in front of every call to it, is that without it FBB.hpp
won't compile in isolation. Compilers would notice, that there is no Size
in namespace FBB
, therefore they can prove, that no specialization of these templates compile, and report the error early.
Bob does everything according to FooBarBaz's documentation:
// --- MyClass.hpp ---
namespace N {
class MyClass {};
}
namespace FBB {
inline int Size(const N::MyClass&) { return 123; }
}
This header also compiles in isolation.
Goofy wants to use FBB::SomeFunction()
and FBB::SupportsFBBSize
with N::MyClass
:
#include "MyClass.hpp"
#include "FBB.hpp"
int main() {
N::MyClass mc;
FBB::SomeFunction(mc);
}
#include "MyClass.hpp"
#include "FBB.hpp"
int main() {
return FBB::SupportsFBBSize<N::MyClass>;
}
The 1st example compiles, and in the 2nd the main()
function returns true
. However, if you swap the #include
directives, the 1st won't compile, and the 2nd will return false
.
There is no solution for this potential include order dependency.
You can play with the above code snippets on Godbolt.
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 Monkey
in Zoo.dll
, which uses class X
. You forget to include X.hpp
, but suppose X.hpp
is in Zoo.dll
's precompiled header:
// --- Monkey.hpp ---
class Monkey {
X x;
};
Monkey.cpp
includes Monkey.hpp
as the first substantive line of code. It compiles all right, because when it compiles, Zoo.dll
's precompiled header (and X.hpp
in 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.hpp
is not included in the precompiled header, they will get a compilation error.
Two approaches come to mind.
#include "X.hpp"
is not forgotten.B.dll
depends on A.dll
then B.pch.hpp
includes A.pch.hpp
.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
:
#include <type_traits>
std::int64_t Fun();
The type std::int64_t
is defined in cstdint
. In MSVC type_traits
includes cstdint
(see on GitHub), therefore, Fun.cpp
(which includes Fun.hpp
in 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.hpp
requires cstdint
in front of it. But, in our topic we are talking about a fixed compiler, and about files under our control.
Fun.hpp
is self-contained.Fun.cpp
won'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 cstdint
in type_traits
in the future? All these problems are related, but different problems, and the solution to them is Include What You Use.
In the above Fun.hpp
we run into compilation errors when porting from MSVC to a different compiler, but that's not because Foo.hpp
is not self-contained, but because we didn't include precisely what we use. We included the definition of std::int64_t
indirectly.