---
title: 'Managing Compile Times'
source: 'https://academia.sh/en/courses/cpp-templates/managing-compile-times'
course: 'Templates and Performance'
language: en
updated: '2026-10-21T05:02:46+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains how many translation units a header is made visible to by forward declaration and by trimming unnecessary includes.'
    - 'Hides private members behind a forward declaration with the pimpl idiom.'
    - 'Explains why the pimpl idiom requires the destructor and the move operations to be defined in the .cpp file.'
    - 'Shows with an allocation count how the cost gained at compile time moves to run time.'
---

# Managing Compile Times

Forward declaration, trimming unnecessary includes, the pimpl idiom, opaque types, explicit instantiation and extern template, and how the cost gained at compile time moves to run time per object.

The previous lesson showed that the profile finds the hot spot and that where the
measurement starts changes the result. But there is one more cost that no counter can catch:
the compile cost paid before the program runs. When a header file changes, every translation
unit that includes that header, directly or indirectly, is rebuilt. The more units a header
is visible to, the more expensive a change is. This lesson examines ways to reduce that
visibility and measures where one of the ways, the **pimpl idiom**, makes the compile cost it
gained be paid at run time. The answer is the kind **run**. A distinction from M11/K05: there,
the state of a moved object was not the measure; here too we do not look at the state of a
moved object, we only count whether the move makes an allocation. Compile time is not
printed; the dependency is described as "how many units see this header", and the number
measured is the allocation per object.

## Reducing the Visibility of a Header

The first tool is the **forward declaration**. If a header uses another type only as a
pointer or a reference, it does not need the full definition of that type; writing
`class Window;` is enough. Including the header that holds the full definition with
`#include` makes this header rebuild too every time that header changes. A forward
declaration cuts this chain: the full definition is included only in the `.cpp` file that
really uses it.

The second tool is deleting unnecessary `#include` lines. A line added to a header is a cost
paid by all the units that include that header; an unused line makes them pay the same cost.
The third tool is specific to templates: if a frequently used template specialization is
instantiated again in every unit, **explicit instantiation** and the `extern template`
declaration gather it into one unit. `extern template class Box<int>;` is written in the
header, and the instantiation is done with `template class Box<int>;` in a single `.cpp`
file; the other units link to the definition in that unit instead of regenerating the
non-inline members. There is a small example of this tool below; its effect does not show in
the run output, so it is not measured.

## Why a Forward Declaration Lightens a Header

The work of a forward declaration shows in an example. A drawing function takes the
`Window` type as a parameter. The full definition of the type is not needed for the
**declaration** of the function: an incomplete type can be a parameter in a function
declaration, whether as a reference or by value. The full definition is needed only in the
**definition** of the function and at the place where the function is **called**. For this
reason the drawing header writes only `class Window;` and does not include `window.hpp` at
all; `window.hpp` is visible only inside `drawing.cpp`.

A unit that includes `drawing.hpp` does not see the inside of `Window`; even if the members
of `Window` change, that unit is not rebuilt. A unit includes the full definition itself only
if it creates a `Window` object or calls one of its members.

```cpp
// drawing.hpp — Window is visible only through a forward declaration
#pragma once

class Window;

int draw(const Window& w);
int draw_copy(Window w);
```

```cpp
// drawing.cpp — the full definition is included only here
#include "drawing.hpp"
#include "window.hpp"

int draw(const Window& w) { return w.width(); }
int draw_copy(Window w) { return w.width(); }
```

The two functions return the same width, but the way the parameter is passed is different.
`draw` takes a reference and does not build a new `Window`. `draw_copy` takes a value: a copy
is built during the call, and the copy of a class with the pimpl idiom makes a heap
allocation. The header shows both functions with the same forward declaration; the cost
depends on the form of the parameter, not on the header.

## Explicit Instantiation and extern template

For templates a forward declaration is not enough: the body of the template has to sit in the
header, because the compiler substitutes the type at every place of use. If a template is
used with the same type in many units, each unit generates the same instantiation inside
itself and the linker picks one of the copies. The running program is correct, but this
generation is written into the compile time of every unit.

`extern template` cuts this repetition. If `extern template class Box<int>;` is written in
the header, the units do not instantiate the non-inline members of `Box<int>` inside
themselves; the definition waits in another unit. Members defined in the class body are
implicitly inline and the declaration does not cover them: the compiler may still generate
them to inline them. The gain grows in members whose bodies are defined outside; because the
members of the `Box` below are defined in the class body, the example shows the syntax, not
the gain. **Explicit instantiation** is done with `template class Box<int>;` in a single
`.cpp` file, and the single copy of the definition is generated there. The effect shows in
compile time; it does not show in the output. The program below only verifies that the pair
of declarations compiles and links.

```cpp
// box.hpp — the template body is in the header, the int specialization comes from outside
#pragma once

template <typename T>
class Box {
public:
    explicit Box(T value) : value_(value) {}
    T get() const { return value_; }

private:
    T value_;
};

extern template class Box<int>;
```

```cpp
// box.cpp — the single definition of the Box<int> instantiation
#include "box.hpp"

template class Box<int>;
```

## The Pimpl Idiom

The fourth tool also takes the **private members** of a class out of the header. The private
fields of a class are visible in the header, because the compiler has to know the size of the
object. If the type of a private field changes, every unit that includes the class is
rebuilt. The **pimpl idiom** moves these fields into an inner structure named `Impl` and
keeps in the header only its forward declaration and a `std::unique_ptr<Impl>`. At this point
`Impl` is an **opaque type**: the unit that sees the header knows it exists but not what is
inside.

The header below carries only the forward declaration and the pointer. Notice that the
destructor and the move operations are only **declared** here; their definitions are in the
`.cpp` file.

```cpp
// window.hpp — only a forward declaration and a unique_ptr
#pragma once
#include <memory>

class Window {
public:
    Window();
    ~Window();
    Window(Window&&) noexcept;
    Window& operator=(Window&&) noexcept;
    Window(const Window&);
    Window& operator=(const Window&);

    int width() const;
    void set_width(int value);

    static int object_count();
    static int alloc_count();

private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};
```

When a `std::unique_ptr<Impl>` is destroyed it calls the destructor of `Impl`; for this it
has to see the full definition of `Impl`. Since `Impl` stands in the header only with a
forward declaration, leaving the destructor implicit in the header or writing `= default`
makes the compiler try to generate the destructor while `Impl` is an incomplete type, and
the build fails. For this reason the destructor and the move operations are only declared,
and defined with `= default` in the `.cpp` file where `Impl` is defined.

```cpp
// window.cpp — the Window implementation, Impl is defined here
#include "window.hpp"

namespace {
int objects = 0;
int allocs = 0;
}  // namespace

struct Window::Impl {
    int width = 0;
};

Window::Window() : impl_(std::make_unique<Impl>()) {
    ++objects;
    ++allocs;
}

Window::~Window() = default;
Window::Window(Window&&) noexcept = default;
Window& Window::operator=(Window&&) noexcept = default;

Window::Window(const Window& other) : impl_(std::make_unique<Impl>(*other.impl_)) {
    ++allocs;
}

Window& Window::operator=(const Window& other) {
    if (this != &other) *impl_ = *other.impl_;
    return *this;
}

int Window::width() const { return impl_->width; }
void Window::set_width(int value) { impl_->width = value; }
int Window::object_count() { return objects; }
int Window::alloc_count() { return allocs; }
```

If a field is added to `Impl`, or the type of `width` changes, only `window.cpp` is rebuilt.
`window.hpp` stays as it is, and no unit that includes it is rebuilt. Being able to change the
implementation without changing the header is the compile cost the pimpl idiom gains.

## Why the Destructor Is in the .cpp File

The most common mistake in this design is leaving the destructor in the header. The rule is
simple: when a `std::unique_ptr<T>` is destroyed, `delete` is called, and `delete` needs the
full definition of `T`. If the destructor of `Window` is not written in the header or is
written as `= default`, the compiler tries to generate it in **every unit that destroys a
`Window` object**. In those units `Impl` is still an incomplete type and the `delete` is
invalid. The fix is to only declare the destructor in the header and define it with
`= default` in the `.cpp` file where `Impl` is fully visible. The same reason applies to the
move assignment: the assignment has to destroy the old `unique_ptr`. The move constructor is
also defined in the `.cpp` file, because if construction is cut halfway, the code that
destroys the constructed members is generated in it too.

We do not quote the error message here and we do not run it; it varies from compiler to
compiler. What matters is that the error appears in the **user unit**: not where the
destructor is written, but where it is called. This makes the error harder to find.

The difference shows when compared with `std::shared_ptr`. A `shared_ptr` captures its
deleter when the object is **constructed**; the destructor can be written in the header even
for an incomplete type, because the deleter is formed in the unit where the construction is
done, while the type is fully visible. In a `unique_ptr` the deleter is part of the type and
is generated at the place where it is destroyed. A `shared_ptr` is thereby freed from the
destructor-in-the-header rule, but in return it carries a control block: `std::make_shared`
builds it together with the object in a single allocation, while a separately constructed
`shared_ptr` pays a second allocation for it. A `unique_ptr` with the default deleter is, in
common implementations, the size of a single pointer. In this lesson `unique_ptr` was chosen
and the allocation count is read through it.

## How Many Units See This Header

Compile cost is measured by how many translation units a header is visible to. We cannot read
this from the output; we read it from the setup of the project, so we give the number as a
**counting example**, not as a measurement. Consider a hypothetical project with five
translation units; all the units include `window.hpp`. When the type of a field inside `Impl`
changes, which units are rebuilt?

| Layout | Units that include `window.hpp` | Units rebuilt when `Impl` changes |
|---|---|---|
| Private fields in the header | 5 | 5 |
| Pimpl idiom, `Impl` in the `.cpp` | 5 | 1 |

The numbers in the table come from the setup of this hypothetical project. The only unit
rebuilt in the second row is `window.cpp`, which defines `Impl`. By the same logic, if
`drawing.hpp` included `window.hpp`, every unit that includes `drawing.hpp` would be tied to
`window.hpp` as well; the forward declaration cuts this indirect connection. Indirect
includes quietly grow this number: a header also brings in everything that the headers
including it include.

For this reason two things are checked before adding an `#include` to a header. Is the added
type really needed in the header with its full definition, or is a forward declaration
enough? And how many headers include this header, and how many units do they in turn
include? The length of the chain comes from the answers to these questions; the compiler does
not do this count for us and warn us, because the program compiles correctly and only more
work is done.

## Measurement: Allocation per Object

```cpp
// window_measurement.cpp — the allocation per object of the pimpl idiom
#include <iostream>
#include <utility>

#include "box.hpp"
#include "drawing.hpp"
#include "window.hpp"

int main() {
    Window a;
    Window b;
    Window c;
    a.set_width(640);
    b.set_width(480);
    std::cout << "pimpl idiom run objects=" << Window::object_count()
              << " allocs=" << Window::alloc_count() << "\n";

    int before = Window::alloc_count();
    Window moved(std::move(a));
    int move_allocs = Window::alloc_count() - before;

    before = Window::alloc_count();
    Window copied(b);
    int copy_allocs = Window::alloc_count() - before;

    std::cout << "pimpl object run move=" << move_allocs
              << " copy=" << copy_allocs << "\n";
    std::cout << "width moved=" << moved.width()
              << " copy=" << copied.width() << "\n";

    int start = Window::alloc_count();
    int w1 = draw(b);
    int by_reference = Window::alloc_count() - start;
    start = Window::alloc_count();
    int w2 = draw_copy(b);
    int by_value = Window::alloc_count() - start;
    std::cout << "forward declaration run reference=" << by_reference
              << " value=" << by_value << " width=" << w1 + w2 << "\n";

    Box<int> box(7);
    std::cout << "explicit instantiation run box=" << box.get() << "\n";
    return 0;
}
```

```text
pimpl idiom run objects=3 allocs=3
pimpl object run move=0 copy=1
width moved=640 copy=480
forward declaration run reference=0 value=1 width=960
explicit instantiation run box=7
```

**CP24.** The first line shows that each of the three `Window` objects allocated one `Impl`
at construction: `objects=3`, `allocs=3`. The pimpl idiom makes you pay the cost of reducing
the visibility of the header as a heap allocation on every object; the inner structure is no
longer inside the object but in a separate block of memory.

**CP25.** The third line confirms that the two objects carry the right values: the moved
object got the width `640` and the copy got `480`. The state of the moved-from `a` was not
looked at; this lesson measures not its state but whether the move makes an allocation.

**CP26.** The fourth line shows that a `Window` shown through a forward declaration made no
allocation when passed by reference (`reference=0`) and made one allocation when passed by
value (`value=1`). The width total `960` confirms that both calls read the value `480`. The
header carried both functions with the same forward declaration; the cost came not from the
header but from the form of the call.

**CP27.** The fifth line prints `box=7`. The `extern template` declaration and the
`template class` definition were compiled and linked together; the output does not show that
there is a single instantiation, it only shows that the pair of declarations is valid. The
effect on compile time cannot be read from this output.

**CP28.** Nothing in the output verifies the five against one in the table; it is a counting
example that comes from the setup of the project. Its correctness depends on the assumption:
that all five units include `window.hpp` and that only one defines `Impl`. In a real project
indirect includes change this number.

**CP29.** The `move=0` in the second line depends on the move constructor being defined with
`= default` in the `.cpp` file: while `Impl` is fully defined, the move only takes over the
`unique_ptr`. If the same constructor were defined with `= default` in the header, a problem
could appear because `Impl` is incomplete. For this reason the measurement is meaningful only
in a layout that applies the rule.

## The Bounding Measurement

The mechanism this lesson measured is moving the cost gained at compile time to run time per
object. The second line shows when this cost is paid again. **Moving** an object with the pimpl
idiom makes no new allocation: `move=0`, because the move only takes over the pointer of the
`unique_ptr` and `Impl` stays in place. **Copying** the same object duplicates `Impl` and
makes one allocation: `copy=1`.

That is, the compile cost gained turns into a run cost at every place an object is produced:
producing and copying pay an allocation, moving does not. The forward declaration shows the
same limit: `reference=0` and `value=1`. The call that passes by reference does not pay the
cost of the pimpl idiom; the call that passes by value is the one that pays. The limit is
this: if the class is copied a hundred thousand times in a hot loop, the time gained in
compiling corresponds to a hundred thousand allocations at run time. For this reason the
pimpl idiom is justified for classes whose objects are produced rarely, whose header is
visible to many units, and whose implementation changes often.

## Summary

- The mechanism this lesson measured moves the cost to the kind **run**: the pimpl idiom
  reduces the visibility of the header and in return every object has to pay a heap
  allocation.
- When a header changes, every unit that includes it is rebuilt; forward declaration,
  deleting unnecessary `#include` lines and `extern template` shorten this chain.
- The pimpl idiom hides the private members behind `Impl`; the implementation can change
  without the header changing. The destructor and the move operations are defined in the
  `.cpp` file while `Impl` is fully defined.
- `pimpl idiom` measured `objects=3 allocs=3` on three objects: each object pays one
  allocation.
- A forward declaration lightens the header; with `reference=0` and `value=1` the cost
  depends on the form of the call. `extern template` gathers the template instantiation into
  one unit.
- Moving made `move=0` allocations and copying made `copy=1`: the compile cost gained is paid
  again at run time in copying.

## Next Step

This lesson showed reducing the visibility of a header, but we still described the effect of
the reduction by guessing. The next lesson is the close of the course: with the question of
how many units a changing header rebuilds, we will evaluate together all the mechanisms we
measured in this course.
