Guidelines for File Structures
Relation File vs. Type
One primary type per
hpp/cppmodule.A primary type can be a
class,struct,enum classor even ausingdirective. A primary class template and its explicit or partial specializations are one logical type and stay in the same header.A header ending in
Traits.hppcan collect multiple thematically related traits. A header ending inTypes.hppcan collect a coherent set of micro-types, such as aliases, concepts, or lightweight shells derived from one detailed base and adding only one or two overrides.Relaxed
impldirectories: ahpp/cppmodule can also contain a logical group of helper functions/templates/types.The filename of a source file always matches its primary type. E.g. the file for the class
ExampleisExample.hpp.A
cppfile is added if there are implementation details that cannot/should not be in the header file.
Splitting Implementation over Multiple Files
Handwritten C++ source files must not exceed 500 physical lines.
If the implementation for a class would exceed this limit, split it into multiple cpp files.
The pattern for files related to a class is:
Class_part.cpp, wherepartis a descriptive name for the logical functionality. E.g.Integer_addition.cppfor the addition implementation of theIntegerclass. Important is to separate the part from the class using an underscore character_.Template implementations should be placed in
Class_part.tpp. Same principle as point 1. Only include the partial headers at the bottom ofClass.hpp. Do not add#include "Class.hpp"inClass_part.tpp.Inline implementations should be placed in
Class_part.hpp. Same principle as point 1.
Forward Declarations
If forward declarations to a nontrivial declared class or template are used from multiple files, put them into a special
header Class_fwd.hpp, that only contains the fwd implementation and all required includes.
The forward header is the authoritative declaration location:
If
Class_fwd.hppexists,Class.hppmust include it. This lets the compiler diagnose declaration and definition mismatches.Use the canonical forward header for friends, pointers, references, function declarations with by-value parameters or returns, and aliases that accept incomplete types.
Include the full definition for inheritance, by-value data members, inline, template, or
constexprbodies, nested-type access,sizeof, or default arguments that construct or access the type.Template constraints and default template arguments belong in the canonical forward declaration. The implementation header defines the same template without repeating defaults.
A forward header should include other canonical forward headers. A full project header is only appropriate when an exposed alias or declaration genuinely requires its definition.
Out-of-Line Implementations
Keep headers focused on declarations and code that must be visible to callers.
Move non-template, non-constexpr implementations to the matching cpp file when they contain more than a
defaulted special member or one simple member expression.
Move dependencies used only by the extracted body to the cpp file as well.
Templates stay in the owning tpp file and are included through that header.
Do not add a heap-backed PImpl only to reduce compile time.
Representation splitting is appropriate only when an existing heap or shared-storage design allows it without another
allocation or a semantic or runtime regression.
Include Ownership
Each header and source file directly includes the declarations it uses. After moving an implementation out of a header, remove dependencies that were used only by that implementation. Source files are reviewed independently for stale includes and direct dependency completeness; they must not rely on unrelated transitive includes.
Optional Standard-Library Formatters
The production API uses the Erbsland Core StringFormat system.
Optional std::formatter specializations exist only to improve diagnostics in unit tests and other explicit
standard-library interoperability code.
Keep all specializations for a domain in its
StdFormat.hppheader.Do not include these headers from regular domain headers or generated
all.hppheaders.Include the matching formatter header explicitly in a unit test that compares domain values with
REQUIRE_*orCHECK_*macros.A formatter based on
erbsland::text::Stringincludestext/StdFormat.hppand derives from the matchingstd::formatterbase.
Directories and Namespaces
The
srcdirectory matches the global namespace.For each used namespace, a subdirectory is created with the namespace name. E.g. the class
erbsland::math::Integeris located insrc/erbsland/math/Integer.hppAdditional directories are allowed, to further group files in the same namespace. So,
math/additionsandmath/subtractionsmay exist to group files in themathnamespace. Such subgroupings make sense if a directory exceeds 30 files.
Separating Implementation Details from the Public API
All types/classes/etc. declared in regular directories and namespaces are considered part of the public API.
The namespace and subdirectory
implmarks the boundary between public API and implementation details.All types/classes/etc. declared inside a namespace and directory tree that contains
implis considered as private implementation detail.For classes, it is ok to have a
implvariant. E.g.math::Examplemay have amath::impl::Exampleif this makes sense for hiding implementation details.If naming clashes must be avoided, adding the suffix
Implto functions is ok – but discouraged.