Optimizing C++ header files

Guy Golan
Guy Golan reading time: 4 minutes
March 2, 2019

C++ is a dynamic language, which requires a proactive approach to try and reduce compile-time dependencies. A common way of doing so involves moving the dependencies from the headers to source files. This is typically achieved by using a functionality called Forward Declaration.

In a nutshell, the declaration just tells the compiler what parameters the function takes and returns, while the actual definition takes care of the actual work.
Let’s look at two guidelines that should improve your compilation times and decrease portability issues, as well as include-what-you-use, a tool that automatically applies these guidelines in your codebase.

1. Forward declare types to speed up compile time

In order to improve up compilation times, replace #include directives with forward declarations of types whenever possible. For example:


#include <iostream> 

#include "a.hpp"
#include "b.hpp"

class MyClass
{
    A a_; 
    B* b_;

public:
    B& foo(std::string arg);
};

std::ostream& operator<<(std::ostream& out, const MyClass& obj);

The compiler only needs to know the full definition of a type if it needs to know its size or interface. This isn’t the case for B – the size of a pointer/reference is the same for every type. So it’s include can be replaced by a forward declaration.

The compiler also doesn’t need the definition of std::string and std::ostream but because they are templates (with potentially additional default templated parameters we don’t know), we can’t forward declare them. Luckily, for std::ostream the header <iosfwd> provides a forward declarations. Even if the definition of std::ostream was needed, <ostream> provides only it without the input streams or std::cin,std::cout,…

The improved file only has the following includes and forward declarations:


#include <iosfwd>
#include "a.hpp"
class B;
</​code></​pre>

This can make the final file size a lot smaller!

Guideline: Use type forward declarations whenever possible, but be careful when doing it with external types.

speed up c++

2. Include portability issues

If header A includes header B, you’ll get the definitions of B when including A. This can lead to subtle portability issues with the standard library. It is not defined – with a few exceptions – which headers include which other headers.

To improve compilation times, many implementations include smaller private headers when needing certain declarations instead of the big public headers. So when you (accidentally) rely on such an indirect include, your code may no longer compiler with a different standard library as you’re missing an include!

This is the case with the previous example: It is missing the #include <string>. On my platform it still compiles as it is implicitly included by the stream header, but this is not guaranteed.

Guideline: Include what you use – if you need a declaration, include the corresponding header. Even if you end up having redundant includes in your file, they are basically free thanks to header guards.

include what you use

The good news is that you don’t have to manually follow these two guidelines. There is a tool, include what you use (IWYU): https://include-what-you-use.org/

It is a clang based tool developed by Google that does both things for you: It converts include directives into forward declarations whenever possible and adds includes if you rely on indirect includes.

After building it from source or getting one of the pre-built binaries from here, running it is relatively easy if your using make or CMake. With make all you need to do is set the CXX variable to IWYU, with CMake just set the CMAKE_CXX_INCLUDE_WHAT_YOU_USE option. Check their documentation for more details. You can also run it manually, it accepts the same options as clang itself.

The tool will give you a summary of changes you should make to each file. For our example file above it prints:

header.hpp should add these lines:
#include <string> // for string
class B;

header.hpp should remove these lines:
– #include “b.hpp” // lines 6-6

The full include-list for header.hpp:
#include <iostream> // for ostream
#include <string> // for string
#include “a.hpp” // for A
class B;

There is also python script fix_includes.py. If you give it the output of IWYU, it will automatically apply those changes.

Guideline: Occasionally run IWYU over your codebase. It can improve compilation times and improve portability.

How does 30X faster build time sound?

Forward Declaration can only take you so far. More and more companies today are facing a growing demand for increased computing power during peak times and growing pressure to improve their time to market. It’s now possible to take on heavy build-time projects during peak times and accelerate software development without changing your source code or purchasing additional hardware.

Incredibuild’s innovative solution accelerates time-consuming tasks, such as builds, tests and more, by distributing them across the user’s local network or VM’s and running them simultaneously. Incredibuild also offers a dedicated cloud solution to go beyond local resources and utilize thousands of automatically provisioned cloud compute instances.

free trial

 

Guy Golan
Guy Golan reading time: 4 minutes minutes March 2, 2019
March 2, 2019

Table of Contents

Related Posts

4 minutes 8 Reasons Why You Need Build Observability

Read More  

4 minutes These 4 advantages of caching are a game-changer for development projects

Read More  

4 minutes What Level of Build Observability Is Right for You?

Read More