The Danger of #include: How C++'s Preprocessor Destroys Architecture Without Senior Oversight [Blender Case Study]
Share this article [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share on X") [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share on LinkedIn") [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share on Facebook") [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share on Reddit") [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share by email") Copy link

Blender is one of the world’s most successful high-performance open-source 3D suites. Running a CppDepend analysis across its core modules reveals a surprisingly strong overall maintainability rating—a remarkable feat for a massive codebase encompassing over 16,000 types and 136,000 methods.
However, when we inspect the structural relationships between its core modules, the Dependency Structure Matrix (DSM) paints a much more complex picture, uncovering widespread dependency cycles:
When evaluating C++ software architecture with CppDepend, a Dependency Structure Matrix (DSM) provides undeniable evidence of dependency health. In a well-structured, layered system, dependencies flow in one direction—resulting in a clean triangular matrix with cells populated only on one side of the main diagonal.
However, inspecting Blender’s core engine matrix reveals a striking visual pattern around `bf_blenkernel`.
How does a project built by world-class engineers end up with so many architectural cycles?
The root cause isn’t developer carelessness. The culprit is C and C++’s legacy preprocessor model: the `#include` directive.
For decades, modern programming languages have used explicit module systems, namespaces, and explicit export controls to manage dependency graphs. C and C++, however, inherited a textual substitution model from the 1970s preprocessor: the `#include` directive.
The `#include` directive’s greatest strength is also its most dangerous flaw: extreme simplicity and flexibility. While textual substitution allows developers to rapidly pull dependencies anywhere in a codebase without rigid compile-time enforcement, this unrestricted freedom quickly becomes a trap as a project scales. Without strict architectural oversight, `#include` makes it effortless to introduce subtle dependency cycles that silently weave modules into a single, tightly coupled monolith. In large-scale C++ projects, these circular header linkages compound over time into massive technical debt—exponentially increasing build times, crippling unit testing capabilities, and making refactoring a minefield. Consequently, maintaining and evolving such a codebase ceases to be a routine engineering task; it demands constant, vigilant intervention from senior developers who must manually enforce structural boundaries where the language compiler fails to do so.
The Structural Flaws of Textual Inclusion
To understand why `#include` damages architecture, consider how it interacts with class definitions and modularity.
1. Inclusion Is Transitive and Leaky
When `FileA.h` includes `FileB.h`, and `FileB.h` includes `FileC.h`, `FileA.h` indirectly depends on `FileC.h`. The internal details of FileB leak into FileA. Over time, developers lose track of what a module actually depends on, creating a web of implicit, hidden dependencies.
2. Physical Structure Dictates Logical Architecture
In a clean design, interface boundaries dictate dependencies. With `#include`, physical file organization forces structural decisions. If Class A needs a single enum defined inside `B.h`, A must include `B.h`, bringing along every other type, pointer, and template header that `B.h` relies on.
3. Circular Dependencies Are Forced by Physical Need
Because C++ requires full type definitions to determine layout sizes, developers frequently put `#include` directives in header files where forward declarations (`class X;`) would have sufficed. The moment two headers need each other’s full definitions, the preprocessor creates a circular inclusion loop, leading to missing type errors, guard tricks, or fragile header ordering.
Deep Dive: The bf\_blenkernel ↔ bf\_bmesh Cycle
An example of how `#include` mechanics create structural dependency loops in Blender exists between the core kernel module (`bf_blenkernel`) and the mesh editing system (`bf_bmesh`).
In a clean, layered architecture, `bf_bmesh` (the higher-level, interactive editing system) should depend on `bf_blenkernel` (the lower-level data structures and math kernel). However, because `#include` directives make it easy to reach across boundaries without structural gatekeeping, `bf_blenkernel` directly references BMesh data structures.
The call graph for `bf_bmesh` demonstrates a remarkably clean, layered architecture—distinctly color-coded with dependencies (used modules) in blue and dependents (used-by modules) in green—with the notable exception of a bi-directional cycle with `bf_blenkernel`.
To isolate the exact types and methods consumed by the kernel from the mesh editing module, we can execute the following CQLinq query:
Let’s check for example the `BMesh` struct used by this function inside `bf_blenkernel` (`armature.cc`):
Why This Creates an Architectural Cycle
- Direct Concrete Type Usage: The kernel function calls `BKE_editmesh_bmesh_get(...)` to obtain a pointer to `const BMesh *bm` and accesses `bm->vdata` directly.
- Forced Header Inclusion: To access the internal member `bm->vdata`, the kernel file MUST include `"bmesh.h"` (or `"bmesh_class.h"`). The Inverse Dependency: Simultaneously, `bf_bmesh` headers and source files include kernel headers (`BKE_.h`) for basic data types (`Object`, `ID`, `CustomData`), memory management, and math helpers.
This forms a hard bi-directional dependency cycle. `bf_blenkernel` cannot be compiled, unit-tested, or reused independently of `bf_bmesh`.
How to Refactor and Break the Cycle
To break this cycle and enforce a clean unidirectional hierarchy (`bf_bmesh` → `bf_blenkernel`), the dependency on BMesh inside the kernel must be inverted or abstracted.
#### Solution 1: Opaque Abstraction / CustomData Offset Extraction
Notice that `BKE_armature_deform_coords_with_editmesh` only accesses `bm->vdata` to extract a single `int` offset: `cd_dvert_offset`. It does not perform actual mesh topology manipulations inside this wrapper.
Instead of passing or retrieving a `BMesh*` inside blenkernel, pass the `cd_dvert_offset` directly into the kernel function, or use a function pointer / callback abstraction:
The caller inside `bf_bmesh` (where including both `bmesh.h` and `BKE_armature.h` is architecturally valid) extracts the `cd_dvert_offset` and calls the kernel. `bf_blenkernel` no longer needs to know BMesh exists.
#### Solution 2: Dependency Inversion via Callback or Delegate
If `bf_blenkernel` requires dynamic access to BMesh data during complex operations, define an abstract interface or function delegate in blenkernel:
// In BKE_armature.h (bf_blenkernel)
using BMeshOffsetGetter = std::function<int(const Object &ob)>;// BMesh logic is injected from higher layers
// without blenkernel knowing the concrete BMesh layout
High-Level Architectural Rigor: GRASP Patterns and High Cohesion
Despite the header-level dependency loops introduced by C++’s preprocessor mechanism, Blender’s underlying code design is exceptionally well-engineered. A deeper look at its class hierarchy and module organization shows strict adherence to general responsibility assignment software patterns (GRASP) and modern domain-driven design principles:
Polymorphism and Abstraction: Blender makes heavy use of abstract interface classes (`bContext`, space type definitions, and operator abstractions) to decouple high-level workflows from implementation details.
High Cohesion: Individual modules maintain sharp domain focus—`bf_bmesh` handles low-level mesh topology, `bf_nodes` manages execution graphs, and `bf_gpu` isolates hardware abstraction. Internal data structures within these modules demonstrate high functional cohesion. Indeed less than 3% of types are considered not cohesive:
Protected Variation: Core subsystems insulate themselves against underlying platform and hardware variations through well-defined internal APIs, keeping lower-level graphics and OS abstraction clean.
The presence of cyclic dependencies across modules is not a symptom of sloppy design, but rather an inevitable side effect of scaling a multi-million-line C/C++ codebase using textual `#include` directives. Without language-level module boundaries, even highly cohesive, interface-driven architectures eventually succumb to transitive header leakage.
Architectural Lessons for C++ Developers
The `#include` mechanism teaches us an enduring lesson: When the compiler doesn’t enforce architectural boundaries, entropy wins.
To mitigate the architectural damage of `#include` in your own projects:
Share this article [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share on X") [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share on LinkedIn") [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share on Facebook") [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share on Reddit") [](https://www.cppdepend.com/blog/the-danger-of-include-cpp-preprocessor/# "Share by email") Copy link
