Skip to content

Project Structure and Architectural Boundaries ​

Native project structure should follow build targets, public interfaces, and dependency direction. A folder taxonomy copied from a framework cannot substitute for those boundaries.

A modest library and application layout ​

text
report/
├── CMakeLists.txt
├── CMakePresets.json
├── cmake/
├── include/report/report.h
├── src/report.c
├── app/main.cpp
├── tests/report_tests.cpp
└── third_party/README.md

The installable public header has a namespaced include path: consumers write #include <report/report.h>. Private implementation remains under src. The CLI is a separate target linking the library, so it consumes the public surface rather than reaching into implementation files.

Build output belongs outside the source tree or under ignored build/... directories. Generated headers should go to a generated include directory and be attached to targets explicitly.

Targets are architectural units ​

A target should have one reason to exist as a compilation/link/deployment unit: library, executable, plugin, generator tool, or test. Splitting every source file into a library creates noise; combining unrelated platform adapters and domain rules creates tangled dependencies.

Prefer directional layers:

text
CLI/UI → application policy → domain library
                    ↓
             platform adapters

The domain can define interfaces or accept callbacks for filesystem/time/network behavior. Platform code depends inward on domain-shaped contracts, not the reverse.

Public headers are expensive dependencies ​

Every public include becomes part of source compatibility and compilation cost. Forward-declare your own classes where safe, use opaque C handles or PImpl where binary stability justifies it, and keep third-party types out of public APIs unless coupling is deliberate.

Do not hide simple value types behind heap-based opaque objects without need. ABI stability, compile-time isolation, and invariant enforcement have costs in allocation, indirection, and complexity.

Generated code needs ownership ​

State which tool generates it, from which source, at what build stage, and whether outputs are committed. Build-time generators that run on the host are distinct from target programs during cross-compilation. Generated outputs need declared prerequisites so parallel builds remain correct.

Configuration should flow through targets ​

Use configured headers for compiled constants that must agree across translation units, runtime configuration for deploy-time choices, and target compile definitions for genuinely conditional source. Avoid a global header whose macros silently alter every dependency.

Secrets do not belong in compiler definitions or generated binaries; compiled strings are observable.

Small projects should remain small ​

A two-file utility may need only main.c, one module, tests, and a Makefile. Add directories and targets when they express a real boundary. The aim is discoverability: a new contributor can find entry points, public interfaces, tests, and the authoritative build commands.

Optional prompts ​

Explain: Why should a CLI link the library rather than compile the library's .c files directly into itself?

Answer: Linking the target tests the public boundary, keeps usage requirements centralized, and lets tests/consumers share the same library product.

Explain: When is PImpl justified?

Answer: When reduced header coupling or a controlled binary ABI outweighs allocation, indirection, special-member complexity, and reduced transparency.

Further reference ​