The Danger of #include: How C++'s Preprocessor Destroys Architecture Without Senior Oversight [Blender Case Study]

www.cppdepend.com · 2026-10-01 · by Long Nguyen ·generic
Why AI’s true advantage is multi-agent orchestration, not raw intelligence: 10,000-agent swarms, zero-friction collaboration and formal verification compress decades of research into days.

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

The Danger of #include: How C++'s Preprocessor Destroys Architecture

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.

CppDepend quality dashboard for Blender showing a strong overall maintainability rating

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:

Dependency Structure Matrix of Blender core modules revealing widespread dependency cycles around bf_blenkernel

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

Call graph of bf_bmesh with a bi-directional cycle with bf_blenkernel highlighted

To isolate the exact types and methods consumed by the kernel from the mesh editing module, we can execute the following CQLinq query:

CQLinq query isolating the types consumed by bf_blenkernel from bf_bmesh

Let’s check for example the `BMesh` struct used by this function inside `bf_blenkernel` (`armature.cc`):

BMesh struct used 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:

    Cleaned kernel signature using plain types and offsets with no Bmesh header required

    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.

    CQLinq query listing abstract interface types in Blender

    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:

    LCOM query showing that less than 3 percent of Blender types are 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:

  • Prefer Forward Declarations: In header files, always prefer `class MyClass;` over `#include "MyClass.h"` unless inheriting or storing a direct value instance.
  • Enforce Layering Rules: Use static analysis tools (like CppDepend with CQLinq) to set up quality gates that fail builds if lower-level modules include higher-level headers.
  • Transition to C++20 Modules: Where possible, replace `#include` with `import`, which enforces explicit exports, avoids macro leakage, and eliminates textual inclusion cycles entirely.

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

www.cppdepend.com · 10 images · 70 links

Comments (0)

No comments yet