c++templatesincludeprecompiled-headersname-lookup

Including the corresponding header first. What problems does it NOT solve?


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?


Solution

  • 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.

    1. Alfred is a library developer. He writes the FooBarBaz lib.
    2. Bob writes an N::MyClass class, which is intended to be used with FooBarBaz lib.
    3. Goofy is an innocent character, he just wants to use N::MyClass with 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() which

    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);
        }
    }
    
    Bob

    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; }
    
    Goofy

    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.

    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 #include directives,

    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.

    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.

    1. Alfred writes the FooBarBaz lib.
    2. Bob writes an N::MyClass class, which is intended to be used with FooBarBaz lib.
    3. Goofy just wants to use N::MyClass with FooBarBaz, but runs into problems.
    Alfred

    This time Alfred wants the client types to have a Size() function, which

    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

    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

    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.

    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 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.

    Solution

    Two approaches come to mind.

    1. The build system should compile all headers in isolation and without precompiled headers. Preprocessing and semantic analysis is enough. The goal is to ensure, that #include "X.hpp" is not forgotten.
    2. The build system should ensure, that if B.dll depends on A.dll then B.pch.hpp includes A.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:

    #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.

    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.