A close-up of a person signing a document with a pen. Photo · Jakub Żerdzicki / Unsplash
Headers are the contract — a class's promise to the rest of the program. Implementation is the back-office work.

Every C++ project beyond a single file confronts the same question: where do I put the declarations, where do I put the bodies, and how do they find each other? C++ inherited an answer from C in 1972 that is, fifty years later, still somehow the answer — with all its rough edges. C++20 finally added modules as a real fix, but headers will be everywhere for the foreseeable future. Knowing the model is a survival skill.

Two kinds of file

The compiler sees one .cpp file at a time (a translation unit). It includes any headers that .cpp mentions, compiles to an object file, and the linker glues all the object files together into a final binary.

A worked split

// === account.h — the contract ===
#pragma once                // or use #ifndef include guards

class Account {
public:
    Account(int id);            // declarations only
    void   deposit(double amt);
    double balance() const;
private:
    int    id_;
    double balance_;
};
// === account.cpp — the bodies ===
#include "account.h"

Account::Account(int id): id_(id), balance_(0) {}

void Account::deposit(double amt) {
    balance_ += amt;
}

double Account::balance() const {
    return balance_;
}

The header is the public face — short, scannable, no implementation. The .cpp is the back office. Other code includes account.h and gets full access to the class's interface; the bodies live in one place, are compiled once, and are linked in.

#pragma once and include guards

Headers can be (and routinely are) included by many files, sometimes transitively several times within one translation unit. Without protection, the compiler would re-process the header repeatedly and complain about duplicate definitions. Two ways to prevent this:

// classic include guards (works on every C++ compiler ever)
#ifndef ACCOUNT_H
#define ACCOUNT_H
// ... contents ...
#endif

// modern shorthand (every modern compiler supports it)
#pragma once

Either is fine; #pragma once is shorter and slightly faster (the compiler can short-circuit the file open). Mix-and-match is OK too.

What goes in the header — and what doesn't

The most useful rule of thumb:

The "templates must live in headers" caveat is one of the most painful parts of C++. Because the compiler instantiates a fresh copy of a template per type used, it must see the full body wherever the template is used — which means the body has to be in the header. This is a major reason heavily-templated headers (Boost, Eigen, parts of the STL) cause long compile times.

Forward declarations — when you don't need the full type

Including a header is expensive (parse cost, transitive cost) and creates a build-time dependency. If your header only needs to mention a type — as a pointer or reference — you can forward-declare it instead of including:

// in widget.h
class Renderer;      // forward declaration; no include needed

class Widget {
public:
    void draw(Renderer* r); // pointer is fine without full definition
};

// in widget.cpp:
#include "renderer.h" // the full include lives here

This pattern dramatically reduces compile times in big codebases. Headers stay slim; "deep" includes happen only in .cpp files.

The Pimpl idiom — hiding even more

Sometimes you want callers' code to depend only on a class's public interface, not the layout of its private fields. The "pointer to implementation" idiom hides every private detail behind a forward-declared opaque pointer:

// widget.h
class Widget {
public:
    Widget();
    ~Widget();
    void draw();
private:
    struct Impl;     // declared, not defined
    std::unique_ptr<Impl> pimpl_;
};

Callers see only the public interface and one opaque pointer. They can be recompiled without depending on the actual layout of Impl. Used heavily in Qt, in PIMPL-friendly libraries, and anywhere ABI stability matters across versions.

Build systems

For one or two files, you can call g++ by hand. For anything bigger, you'll meet a build system: CMake (the de-facto standard for cross-platform C++), Bazel (Google's, used by TensorFlow), Meson, Make. They all do the same job: figure out which .cpps have changed, recompile only those, link the result, manage dependencies. Don't try to learn build systems before you need to. When you need to, the answer is almost always CMake.

Why this matters for AI

llama.cpp's source tree has roughly 30 .cpp files and 30 .h/.hpp files. PyTorch has thousands. Every one of them follows the same pattern: declarations in headers, bodies in .cpp, build-system glue elsewhere. By the end of this week the ritual should not be opaque.

Phase 3 is now closed. You have C++ — the language and the discipline. Next week we change directions: not new syntax, but a new way of thinking. How do you measure your code? How do you decide when an algorithm is too slow? Phase 4 is the toolbox of the working programmer.

Headers describe; .cpp files do. Keep that boundary clean and your build will be fast.

Try it yourself

What's next — and welcome to Phase 4

Phase 3 is done. You can now read, write, and reason about a real piece of C++. From next week we leave language-as-language behind and ask the question that animates the rest of the course: what makes one solution to a problem better than another?

Week 29 is Arrays & Vectors — the foundational data structure of every program ever written. Phase 4 — The Toolbox — begins.

Photo credit

Photo free under the Unsplash license. Contract · Jakub Żerdzicki.