Appearance
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.mdThe 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 adaptersThe 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.