A reusable library is one that other people pick up, integrate, and don't regret. Most "reusability" problems aren't about features — they're about packaging, headers, ABI, and dependencies. This page is the checklist.
- 1. Decide What the Library Is
- 2. Public Headers and the Include Surface
- 3. Namespacing and Symbol Hygiene
- 4. Header-Only vs Compiled
- 5. CMake Packaging
- 6. Versioning and ABI
- 7. Dependencies — As Few As Possible
- 8. Error Reporting
- 9. Documentation and Examples
- 10. Reusability Checklist
Before any code, answer:
- Scope. One thing, well. "JSON parser." Not "JSON parser plus HTTP client plus logging."
- Audience. Application developers? Other library authors? Embedded? Each implies different defaults (allocator support, exception use, RTTI).
- Consumption model. Header-only? Static? Shared?
find_package-able?add_subdirectory-able? Conan/vcpkg? - C++ standard. Pick the lowest version that does the job. Bumping later is easy; lowering is impossible.
- Stability promise. SemVer with a real ABI commitment, or "headers only, recompile on every release"?
Write these down in a DESIGN.md. They will drive every later decision.
Public headers are your contract. Treat them surgically:
- Put public headers under
include/<libname>/...and never include fromsrc/...or relative paths. - Forward-declare aggressively in public headers. Pull in heavy STL headers only when the type is actually needed by value.
- Avoid
using namespaceand macros at file scope. Both leak into every consumer. - Don't expose implementation types — use PIMPL or opaque handles for class members that change often.
- Make sure each header compiles standalone (
#include-it-first test).
mylib/
├── include/mylib/ # public, installable
│ ├── core.hpp
│ └── detail/ # public-but-private; don't promise stability
├── src/ # implementation, never installed
└── test/
The detail/ convention signals "you can see this, but if you depend on it your code may break."
- Top-level namespace = library name. No exceptions.
- Use an inner
detailnamespace for private helpers visible from headers. - Use inline namespaces for ABI versioning (
inline namespace v1 { ... }). - For shared libraries, mark only the public symbols visible (
-fvisibility=hidden+ an export macro).
#if defined(_WIN32)
#ifdef MYLIB_BUILDING
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
#else
#define MYLIB_API __attribute__((visibility("default")))
#endif| Header-only | Static lib | Shared lib | |
|---|---|---|---|
| Build complexity for consumer | None | Low | Medium |
| Compile time hit | High (rebuilt everywhere) | Once | Once |
| ABI stability matters? | No (consumer recompiles) | No | Yes |
| Templates everywhere | Easy | Possible (extern template) | Painful |
| Plugin-style hot-swap | No | No | Yes |
Default: static unless you have a reason. Header-only for small utility libraries (expected, span-like). Shared for plugin systems and where binary distribution matters.
A modern, reusable library exposes one (or a few) IMPORTED targets. Consumers should write:
find_package(mylib 2.0 REQUIRED)
target_link_libraries(myapp PRIVATE mylib::mylib)Skeleton:
cmake_minimum_required(VERSION 3.20)
project(mylib VERSION 1.2.0 LANGUAGES CXX)
add_library(mylib)
add_library(mylib::mylib ALIAS mylib)
target_sources(mylib PRIVATE src/foo.cpp)
target_include_directories(mylib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(mylib PUBLIC cxx_std_20)
include(GNUInstallDirs)
install(TARGETS mylib EXPORT mylibTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT mylibTargets
FILE mylibTargets.cmake
NAMESPACE mylib::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mylib)
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion)Things to get right:
- Use
BUILD_INTERFACE/INSTALL_INTERFACEso headers work both in-tree and after install. - Provide a
mylib::mylibalias so consumers can write the same code in both modes. - Generate and ship a
mylibConfigVersion.cmakesofind_package(mylib 2.0)works.
Use semantic versioning: MAJOR.MINOR.PATCH.
| Change | Bump |
|---|---|
| Bug fix, no API change | PATCH |
| New API, old code still compiles | MINOR |
| Removed/renamed/changed signature | MAJOR |
| Changed layout of public class, vtable, exception type | MAJOR (ABI break) |
For shared libraries you also need an SONAME tied to the major version. See API and ABI Design for the full rulebook.
Every dependency is a constraint you push onto your consumers (compiler version, standard, build system, license). Rules of thumb:
- The standard library is free, except
<filesystem>and<regex>which historically had link-time gotchas. - Boost is fine for app code, painful as a library dep — splits across versions, large download, header-time cost.
- Other libraries should be considered carefully; if you only need one function, copy it (with attribution) instead of dragging a transitive dep.
- For deps you really need, expose them only privately (
target_link_libraries(mylib PRIVATE foo)) so consumers don't inherit the include path.
A library cannot dictate how its callers handle errors. Pick a single mechanism, document it, and stick with it:
- Exceptions — natural for C++, costs nothing on the success path, painful in
-fno-exceptionsconsumers. - Error codes — small, predictable;
std::error_codeis the standard idiom. std::expected<T,E>(C++23) — the modern compromise; explicit, no exceptions, value-semantic.
Document which functions can fail and how. Never std::terminate from library code unless the precondition was clearly violated.
A library nobody can figure out is not reusable.
- A
README.mdwith: 30-second pitch, install snippet, smallest meaningful example, link to full docs. - Doxygen on every public symbol — at minimum a one-line description and the failure mode.
- An
examples/folder that builds with the library. Treat it as a test of your API ergonomics. - Changelog (
CHANGELOG.mdin Keep a Changelog format).
- Public headers under
include/<libname>/, each compiles standalone. - All public symbols inside
<libname>::namespace; private helpers in<libname>::detail. -
mylib::mylibCMake target works for bothfind_packageandadd_subdirectory. - No
using namespacein public headers. - No transitive dependencies leaked through public headers unless intended.
- Versioned via SemVer; ABI commitment documented.
- Single, documented error-reporting mechanism.
- At least one buildable example.
- CI builds on the matrix of compilers/standards you claim to support.
- License file at the repo root.
- API and ABI Design
- SOLID Principles
- Modern CMake Examples
- Large-Scale C++ Software Design, John Lakos.