Chapter 15 of 24 | C++
Multi-file programs, packages, and libraries
As a program grows, split it into files with clear responsibilities. A header describes an interface; a source file implements it. Each source file is compiled as a translation unit, and the linker combines the resulting object files.
// calculator.hpp
#pragma once
int add(int left, int right);
// calculator.cpp
#include "calculator.hpp"
int add(int left, int right)
{
return left + right;
}
// main.cpp
#include <iostream>
#include "calculator.hpp"
int main()
{
std::cout << add(2, 3) << '\n';
}
Compile both source files in one command, for example g++ -std=c++23 -Wall -Wextra -Wpedantic main.cpp calculator.cpp -o calculator. Do not compile a header as if it were an independent program. A linker error about an undefined reference often means a definition was not compiled or its signature does not match the declaration.
Designing library boundaries
Keep public interfaces small and stable. Put implementation details in source files or private headers. Use namespaces to group library names. A library may be static or shared; the choice affects distribution and linking, not the basic responsibility of a function interface.
Third-party package managers can help fetch and build dependencies, but each ecosystem has its own conventions. Record dependency versions and licensing, and prefer well-maintained libraries that target your platform and compiler.
One Definition Rule
The One Definition Rule (ODR) prevents conflicting multiple definitions of entities across a program. If a normal function is defined in a header included by several source files, the linker may report duplicate definitions. Use one implementation file, or define the function as inline when that is truly appropriate. Templates generally need their definitions visible at instantiation sites.
Understand the compiler's view of a project
A source file together with the text brought in through its includes forms a translation unit. The compiler checks that unit independently. It generally does not see another source file's implementation while compiling the first one; a declaration is the contract that allows the call to be checked. The linker later resolves the call to a definition with a compatible signature.
This means the same public header should work no matter which source file includes it first. A header that silently depends on another header having been included earlier is fragile. Test header self-sufficiency by including it first in a small source file or by keeping each implementation file's own includes explicit.
Diagnose common linker failures
An "undefined reference" or "unresolved external symbol" often means the function was declared and called but its definition was not linked. Check that the implementation file is part of the target, that the spelling and parameter types match, and that the correct library is linked. A duplicate-symbol error often means the same non-inline definition appears in several translation units.
Some linkers also distinguish C++ symbols by encoded names that represent namespaces and parameter types. If a library was built with a different compiler, calling convention, or incompatible configuration, the visible function name may not match the expected one. Use the library's documented build and linking instructions rather than guessing flags.
Public API and implementation stability
A public header is a promise to users of a library. Changing a parameter type, removing an overload, or changing the layout of an exposed class can break consumers. Keep interfaces as small as practical and avoid exposing internal state that callers do not need. For a widely distributed library, learn about API and ABI compatibility, versioning, and supported compiler combinations.
Use namespaces to keep a library's types and functions grouped under a distinctive name. Do not put unrelated helper functions in the global namespace. Avoid using-directives in public headers because they affect every source file that includes them.
Dependencies are part of the build
A dependency needs more than a header: it may need compiled libraries, generated code, configuration options, and a compatible compiler. A build system should record these relationships so a clean build can find them reliably. Do not rely on a library installed accidentally on one developer's machine. Document supported versions and licenses, and pin versions when reproducibility or security requires it.
Practice
- Split a program into
main.cpp, a header, and an implementation file. - Intentionally omit an implementation file from the build and recognize the linker diagnostic.
- List which headers a library user needs and which should remain private.
- Intentionally omit a required source file and diagnose the linker error, then add it to the build.
- Compile a source file that includes its public header first to check that the header is self-contained.
- Identify which parts of an example are public API and which are implementation details.
Key idea: multi-file programs work when declarations, definitions, and build inputs agree. Treat a library's interface as a contract.
Static libraries, shared libraries, and ABI
A static library is typically a collection of object code that the linker incorporates into a final executable. A shared library is loaded or linked as a separate runtime component, subject to the platform's loader and deployment rules. Each choice affects distribution, startup, updates, and how dependencies are found. These details are platform-specific, so use the toolchain's documented packaging and linking workflow.
An application binary interface (ABI) includes low-level conventions such as symbol naming, calling conventions, object layout, exception handling, and runtime-library compatibility. Two components can agree on the source-level function declaration but still be ABI-incompatible if they were built with incompatible settings or toolchains. C++ name mangling is implementation-specific; exporting a C++ class across a stable plugin boundary can therefore be more fragile than a carefully designed C interface or opaque-handle API.
Controlling implementation exposure
A public class definition exposes its object layout to every caller that constructs it by value. Adding a private field can still change its size and break ABI compatibility. The Pimpl idiom places implementation details behind an owning pointer to an incomplete type, allowing some implementation changes without changing the public class layout. It adds indirection and allocation costs and requires deliberate destructor and move definitions where the implementation type is incomplete.
Templates and inline functions usually expose implementation in headers, which allows instantiation and optimization but couples consumers to that implementation. A non-template compiled function hides implementation and can reduce rebuilds, but creates a library linking boundary. Choose based on performance, compatibility, compile time, and distribution requirements.
Symbol visibility and versioning
Shared-library platforms may export many symbols by default or require explicit export annotations. Export only the intended interface when possible: it reduces accidental dependencies and can improve load/link behavior. Libraries that evolve over time need a versioning strategy for source compatibility, binary compatibility, and deprecation. These are separate promises; maintaining one does not automatically maintain the others.
Inspecting a symbol table or linker map can help diagnose missing exports, duplicate symbols, and unexpected dependencies. Such tools expose implementation details and differ across platforms, so treat them as part of a specific build investigation rather than portable language behavior.
Advanced exercise
- Build a small static library and a shared library, then compare how the executable depends on each.
- Design an opaque-handle interface that can preserve a stable boundary while hiding a C++ implementation.
- Change a public class layout and explain why it can break ABI even when source callers still compile.
- Inspect exported symbols and document which are intentionally public.