Reusing Code and Modularity in C++ Libraries

Book: The C++ Programmer’s Mindset
Author: Sam Morley
ISBN: 978-1-83588-842-1

Chapter 6 is where Morley stops talking about solving one-off problems and starts talking about what happens when other people (including future you) have to use your code. The whole point is simple: code only matters if someone can actually reuse it.

Previous: Data Structures (Part 2) | Next: Outlining the Challenge

Static, shared, and header-only libraries

Morley walks through the three main packaging options. Static libraries get baked into your binary at link time. Shared libraries load at runtime through the dynamic linker (ld on Linux). Header-only libraries (templates, much of Boost and the STL) compile straight into whoever includes them.

The tradeoffs are practical. Static linking can mean bigger binaries and weird edge cases if the same static lib gets linked twice into different components. Shared libs save disk space and let you ship bug fixes without rebuilding every app. But shared libs on Windows are painful: no RPATH equivalent, DLL search path headaches, especially when testing inside the build tree.

Header-only is convenient until your compile times explode. Templates are flexible, but the compiler pays for that flexibility every single build.

Every C++ program already links libc for malloc/free and friends. Morley uses that as a reminder that library boundaries are everywhere, even when you do not think about them.

Public vs private, API vs ABI

Public interfaces need to stay stable. Private stuff can change whenever you want. Morley ties this to API stability (source compatibility) and ABI stability (binary compatibility). Change a class data member in a shared library header and you just broke every downstream user who linked against the old binary.

His fixes are the usual good ones: detail or internal namespaces, pimpl idiom, inline namespaces for versioning (ct::v1_0::function re-exported as ct::function). Symbol deprecation cycles before removal. For shared libs, explicit symbol export control matters far more than for static or header-only code.

Exporting symbols with CMake

GCC and Clang export all symbols by default. MSVC requires explicit dllexport/dllimport. Morley’s approach: hide everything, export on purpose.

set(CMAKE_VISIBILITY_PRESET hidden)
generate_export_header(MySharedLibrary)

The generated MYSHAREDLIBRARY_EXPORT macro expands to the right platform directive when building vs consuming. Even on static targets the macro becomes empty, so one header works everywhere.

Designing stable interfaces

Morley gives three design rules that age well:

  1. Layer the interface. Core routines handle the hard cases. Templates and inline wrappers sit on top for convenience.
  2. Keep it minimal. Fewer exported symbols means less maintenance.
  3. Encapsulate implementation data. Pimpl keeps binary compatibility when internal fields change.

He also covers memory across boundaries. A FILE* from fopen must be closed with the matching fclose from the same runtime. Wrap it in unique_ptr<FILE, FileDeleter> with the deleter defined in the .cpp that owns the allocation logic.

C++ modules: promising, not ready

C++20 modules replace header text inclusion with export module interface units and separate implementation units. Non-exported symbols stay internal to the module even if they exist in the compiled binary.

Morley is blunt: as of early 2025, compiler support is spotty, precompiled standard library modules are not universally available, and combining modules with shared library export on Windows is still murky. Interesting future. Not a portable choice today.

Splitting big projects in CMake

The ODE solver example is the chapter capstone. Euler and RK4 solvers live in separate static libs (ctode_euler, ctode_rk4) with their own GoogleTest targets. The shared ctode library links them privately and exposes only ode_solve to users.

Position-independent code (POSITION_INDEPENDENT_CODE ON) is required because static libs will end up inside a shared library. Without -fPIC, linking fails on Linux.

When solvers share a base class, pull common code into ctode_common and link it into every solver target. The linker removes duplicates at the final step.

Morley also mentions target_sources for organizing files within a single target when full separation is not worth the CMake overhead. Small thing, but it helps future-you find related code faster.

My take

This chapter closes Part 1 of the book. All the theory chapters lead here: your code needs to survive contact with other codebases. The module section will age quickly (that is C++ for you), but the library packaging and CMake decomposition advice is solid and immediately useful.

If you have only ever written single-target CMake projects, the ODE directory layout alone is worth the read. Separating concerns is one of the four pillars of computational thinking Morley keeps returning to, and here it shows up in build system form.


Previous: Data Structures (Part 2) | Next: Outlining the Challenge