Guidelines for File Structures

Relation File vs. Type

  1. One primary type per hpp/cpp module.

  2. A primary type can be a class, struct, enum class or even a using directive. A primary class template and its explicit or partial specializations are one logical type and stay in the same header.

  3. A header ending in Traits.hpp can collect multiple thematically related traits. A header ending in Types.hpp can 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.

  4. Relaxed impl directories: a hpp/cpp module can also contain a logical group of helper functions/templates/types.

  5. The filename of a source file always matches its primary type. E.g. the file for the class Example is Example.hpp.

  6. A cpp file 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.

  1. The pattern for files related to a class is: Class_part.cpp, where part is a descriptive name for the logical functionality. E.g. Integer_addition.cpp for the addition implementation of the Integer class. Important is to separate the part from the class using an underscore character _.

  2. Template implementations should be placed in Class_part.tpp. Same principle as point 1. Only include the partial headers at the bottom of Class.hpp. Do not add #include "Class.hpp" in Class_part.tpp.

  3. 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:

  1. If Class_fwd.hpp exists, Class.hpp must include it. This lets the compiler diagnose declaration and definition mismatches.

  2. Use the canonical forward header for friends, pointers, references, function declarations with by-value parameters or returns, and aliases that accept incomplete types.

  3. Include the full definition for inheritance, by-value data members, inline, template, or constexpr bodies, nested-type access, sizeof, or default arguments that construct or access the type.

  4. Template constraints and default template arguments belong in the canonical forward declaration. The implementation header defines the same template without repeating defaults.

  5. 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.

  1. Keep all specializations for a domain in its StdFormat.hpp header.

  2. Do not include these headers from regular domain headers or generated all.hpp headers.

  3. Include the matching formatter header explicitly in a unit test that compares domain values with REQUIRE_* or CHECK_* macros.

  4. A formatter based on erbsland::text::String includes text/StdFormat.hpp and derives from the matching std::formatter base.

Directories and Namespaces

  1. The src directory matches the global namespace.

  2. For each used namespace, a subdirectory is created with the namespace name. E.g. the class erbsland::math::Integer is located in src/erbsland/math/Integer.hpp

  3. Additional directories are allowed, to further group files in the same namespace. So, math/additions and math/subtractions may exist to group files in the math namespace. Such subgroupings make sense if a directory exceeds 30 files.

Separating Implementation Details from the Public API

  1. All types/classes/etc. declared in regular directories and namespaces are considered part of the public API.

  2. The namespace and subdirectory impl marks the boundary between public API and implementation details.

  3. All types/classes/etc. declared inside a namespace and directory tree that contains impl is considered as private implementation detail.

  4. For classes, it is ok to have a impl variant. E.g. math::Example may have a math::impl::Example if this makes sense for hiding implementation details.

  5. If naming clashes must be avoided, adding the suffix Impl to functions is ok – but discouraged.