Skip to content
Topos Uranos

Summary

So far, every program in the course fit in a single file. A program of a thousand lines can no longer be read in one go, and its functions deserve to serve other programs. This lesson teaches how to divide it: a header that declares what a module offers, an implementation file that defines it and a main.cpp that uses it. It presents include guards; the one-definition rule, which explains the two classic linker errors and the modern meaning of inline; separate compilation, which recompiles only what changed; and it closes with a custom library, made of functions from earlier lessons.

Learning objectives

  1. Divide a program into a header with declarations, an implementation file and a main.cpp, and compile it.
  2. Write include guards and explain what problem they solve; recognize #pragma once as an extension that every compiler accepts, but that the standard does not include.
  3. Explain the one-definition rule and diagnose its violations from the linker’s message (“multiple definition”, “undefined reference”).
  4. Compile separately with -c and link the objects, explaining what gets recompiled when each file changes.

Interface and implementation

Modularity

A function with a contract, like those of the lesson Designing with functions, already separates what it promises, written in its declaration and its contract, from how it keeps the promise, written in its body; whoever calls it needs only the former. If this is so, nothing prevents keeping the two parts in separate files. A module is a group of functions (and of the types they use) with a common purpose; its interface is what it offers to whoever uses it, and its implementation, the way it does so. In C++, the interface is written in a header file (extension .hpp, or .h, inherited from C) and the implementation in a .cpp file with the same name.

Three advantages follow from this. The program is understood in parts: to use the module, reading its header is enough. It is compiled in parts: a change in the implementation does not force recompiling those who only use the interface. And it is tested in parts, against each contract, as the lesson Testing will teach. The course LaTeX for Academic Writing divides a long document in the same way, in its lesson “Long documents”.

Three files

The plane geometry module of this example offers three functions; two of them come from lesson 5.3. The header only declares them, each with its contract. The three lines that begin with # will be explained in the next section; for now it is enough to know that every header has them.

geometry.hpp
// geometry.hpp: interface of the plane geometry module.#ifndef GEOMETRY_HPP#define GEOMETRY_HPP​// Area of the circle with radius radius.// Precondition: radius >= 0.// Postcondition: returns the area of the circle with radius radius.double circleArea(double radius);​// Whether three lengths are the sides of a triangle.// Precondition: none.// Postcondition: returns true if and only if there is a triangle with sides a, b and c.bool isTriangle(double a, double b, double c);​// Area of the triangle with sides a, b and c.// Precondition: isTriangle(a, b, c).// Postcondition: returns the area of the triangle with sides a, b and c (Heron's formula).double triangleArea(double a, double b, double c);​#endif

The implementation file includes its own header and defines the functions. The standard library headers it needs (<cmath> for the square root, <numbers> for π\pi) are included here and not in the header, because whoever uses the module does not need them.

geometry.cpp
// geometry.cpp: implementation of the plane geometry module.#include "geometry.hpp"​#include <cmath>#include <numbers>​double circleArea(double radius){    return std::numbers::pi * radius * radius;}​bool isTriangle(double a, double b, double c){    return a > 0 && b > 0 && c > 0 && a < b + c && b < a + c && c < a + b;}​double triangleArea(double a, double b, double c){    const double s{(a + b + c) / 2};    return std::sqrt(s * (s - a) * (s - b) * (s - c));}

Finally, main.cpp includes the header and uses the module, without seeing a single line of its implementation:

C++ program
#include <print>​#include "geometry.hpp"​int main(){    std::println("Circle of radius 2: {:.4f}", circleArea(2.0));    std::println("Is there a triangle with sides 3, 4, 5? {}", isTriangle(3.0, 4.0, 5.0));    std::println("Its area: {}", triangleArea(3.0, 4.0, 5.0));    std::println("And with sides 1, 2, 3? {}", isTriangle(1.0, 2.0, 3.0));}
Output
Circle of radius 2: 12.5664Is there a triangle with sides 3, 4, 5? trueIts area: 6And with sides 1, 2, 3? false

The quotation marks of #include "geometry.hpp" look for the file first of all in the folder of the file that includes it, and the angle brackets of #include <print>, in the system folders: quotation marks are for one’s own files. The file geometry.cpp includes its own header first, so that, if the header lacked an inclusion it needs itself, the error would appear when compiling the module and not in someone else’s file. And the triangle with sides 1, 2 and 3 does not exist (3=1+23 = 1 + 2), so the precondition of triangleArea obliges the caller to ask first.

To compile, the command receives the two .cpp files; the header is not named, because it enters each of them through #include. The course options are stored in a terminal variable, FLAGS, so as not to repeat them in every command; $FLAGS retrieves them. This is how it is written on Linux and macOS; in PowerShell variables are written differently, and it is better to write the options in full or to compile from the editor (section “Separate compilation”).

Terminal
$ FLAGS="-std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined"$ g++ $FLAGS main.cpp geometry.cpp -o main$ ./mainCircle of radius 2: 12.5664Is there a triangle with sides 3, 4, 5? trueIts area: 6And with sides 1, 2, 3? false
ExampleThe journey of three files

The figure takes up the one in the lesson Programs, compilers and C++ for this project.

Proof

  1. The programmer writes three files: a header and two .cpp files. Only the .cpp files are handed to the compiler.

  2. The preprocessor copies geometry.hpp into each file that includes it. There are, therefore, two copies of the declarations, one in each translation unit, and that is correct: a function can be declared as many times as one likes.

  3. Each unit is compiled separately into its object file. When compiling main.cpp, the compiler does not look at geometry.cpp: the declaration is enough for it to check the types of each call.

  4. The linker joins the two objects and the standard library, and resolves each call of main.o to circleArea with the definition that is in geometry.o.

A frequent error is to include the implementation file, #include "geometry.cpp". If it is also named in the command, its functions end up defined twice and linking fails, by the rule that the section “The one-definition rule” will explain; if it is not named, main.cpp becomes a single text again, which is recompiled entirely with every change. .cpp files are compiled; headers are included.