Skip to content

C++ Library Coding Guidelines

Applies on top of the general C++ coding guidelines and the library design guidelines.

Header Layout

Public Headers — include/<library-name>/

Public headers form the installed API surface of the library. Only declarations that consumers need belong here. Everything else is an implementation detail.

include/
  <library-name>/
    greeter.hpp   ← installed to <prefix>/include/<library-name>/greeter.hpp
  • Use #pragma once (not include guards).
  • Minimise includes: prefer forward declarations over full includes where possible.
  • Never include implementation-only headers (e.g. <format>, internal helpers) in public headers.
  • Keep the directory name identical to the CMake target name and the Conan package name.

Consumers include public headers as:

#include <library-name/greeter.hpp>

Private Headers — src/

Implementation details, helpers, and internal types belong in src/. They are never installed and never appear in the public API.

src/
  greeter.cpp          ← implementation
  greeter_detail.hpp   ← private helper (not installed)

CMake Target Design

Expose the public header directory through the BUILD_INTERFACE / INSTALL_INTERFACE generator expressions so that the same target works both during the build and after installation:

target_include_directories(<target>
    PUBLIC
        $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
)

Export the target so consumers can use find_package:

install(TARGETS <target>
    EXPORT <target>Targets
    ARCHIVE DESTINATION lib
)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/
    DESTINATION include
)
install(EXPORT <target>Targets
    FILE <target>Targets.cmake
    NAMESPACE <target>::
    DESTINATION lib/cmake/<target>
)

Conan Recipe

Use a full recipe (not a consumer recipe) at the project root. A full recipe declares name, version, exports_sources, build(), package(), and package_info().

def package_info(self):
    self.cpp_info.libs = ["<library-name>"]

Verify the package is consumable via a test package in packaging/test_package/: conan create . --test-folder=packaging/test_package

The test package must build and run a minimal consumer — it is the integration test for the Conan recipe.

ABI and API Stability

  • Mark symbols with visibility attributes ([[gnu::visibility("default")]]) when building a shared library.
  • Avoid exposing STL containers in the public API across shared-library boundaries.
  • Increment SOVERSION in CMake when breaking ABI; increment the Conan version when breaking API.

Acceptance Tests

A thin, test-only CLI wrapper ("CLI-shim") around the public API lives under test/bdd/cli_shim/; it is built only when BUILD_TESTING is on and never installed or packaged. pytest-bdd drives it through a subprocess, exactly like a CLI tool's acceptance tests. This gives a library without a running process of its own an executable acceptance-test tier.

What to Avoid

  • Putting implementation .cpp files under include/.
  • Putting private headers under include/.
  • Linking private dependencies as PUBLIC in target_link_libraries — this leaks them to consumers.
  • Using target_include_directories with SYSTEM on your own headers — it suppresses warnings that consumers need to see.

Directives

  • Public headers in include/<library-name>/; private headers and implementations in src/
  • Use BUILD_INTERFACE/INSTALL_INTERFACE generator expressions in target_include_directories
  • Full Conan recipe at project root; include a test package in packaging/test_package/
  • Link private dependencies as PRIVATE in target_link_libraries — never PUBLIC for private deps
  • Increment SOVERSION on ABI break; increment Conan version on API break
  • A test-only CLI-shim under test/bdd/cli_shim/ exposes the public API to pytest-bdd; never install or package it