# Measuring C++ Architecture: Coupling, Cycles, LCOM4

> Source: <https://blog.stackademic.com/measuring-c-architecture-coupling-cycles-lcom4-7c8d3fad7be8?source=rss----d1baaa8417a4---4>
> Published: 2026-08-16 01:41:57+00:00

I ‘ve been building RAAG, a platform that turns source code into a quantified dependency graph and uses it to scope AI-assisted refactoring. Last post covered the C++ extraction layer — parsing 579 files in parallel with a std::jthreadpool. This one is about what happens once you have the graph: turning it into an actual judgement about the code's architecture.

I ran it against two real, widely-used C++ libraries: nlohmann/json and fmt.

Here's the headline output:

Files are nodes. An edge runs from the dependent to the dependency — if a.cpp includes b.hpp, the edge is a → b. That direction is the entire basis of everything below it. Reverse it and every metric inverts while the numbers still look plausible.

Every import gets checked against the file set RAAG actually parsed:

```
3,354 imports checked  546 resolved      → matched a file in the repository  340 ambiguous     → matched several files; closest path wins2,468 external      → standard library, third-party, outside the repo
```

**26.4% internal**. That number matters more than it looks. RAAG has no compiler include path and no sys.path — it matches import text against the files it parsed, with tie-break rules for the ambiguous cases. Reporting that ratio on every run is the honest thing to do: a graph built from 26% resolved imports and one built from 90% resolved imports describe very different amounts of truth, and a tool that hides the difference is overstating its own accuracy.

Two numbers per file:

Combined into Instability:

I = Ce / (Ca + Ce)

Here’s the part that took real thought to get right.

**A high instability score is not automatically a problem.** An adapter class, a plugin, a CLI entry point — these should score near 1.0. Depending outward while nothing depends on them is exactly their job. If I flagged every unstable file, the report would be mostly noise from code that’s working exactly as designed.

Look at the top of the raw instability table from this run:

```
Ca   Ce   I      File--   --   ----   ------------------------------ 1    8   0.89   detail/input/parser.hpp 1    8   0.89   detail/output/serializer.hpp 1    7   0.88   detail/iterators/iter_impl.hpp
```

Ca of 1. Almost nothing depends on these. Their instability is high, but it’s irrelevant — nothing breaks if they change, because barely anything relies on them.

Compare to what actually triggered an error:

```
[ERROR] instability   detail/input/input_adapters.hpp        4 modules depend on this file, but it depends on 5 others        (I=0.56, limit 0.40).
```

**Ca of 4.** Real modules rely on this one, and it’s simultaneously standing on 5 other dependencies. That’s the actual risk — a foundation that shifts under its own weight. So the check is gated on a minimum afferent coupling before instability is even judged. Below that threshold, a high I score isn’t a violation — it’s just an unstable leaf doing its job.**9 files cleared both bars** on this run. The worst: input_adapters.hpp at I=0.56, four dependents, five dependencies.

```
[ERROR] dependency-cycle   fmt/include/fmt/base.h        3 files depend on each other in a cycle.
```

Three files in fmt mutually depend on one another. None of them can be understood, tested, or changed in isolation — touch one and you're implicitly touching all three.

RAAG finds these using **strongly connected components** rather than enumerating individual cycles. Cycle enumeration is exponential in the worst case, and a densely tangled header set is precisely the worst case — the analysis would hang on exactly the input it's most needed for. Tarjan's algorithm finds every SCC in linear time, and a component of more than one file already tells you everything actionable: this group is tangled, full stop.

This is the finding I didn't expect.

```
LCOM4  Methods  Class55     61       size_padding (fmt/format.h)
```

LCOM4 — Hitz & Montazeri's cohesion metric — builds a graph where methods are nodes, joined when they share a field or when one calls the other. The score is the number of connected components. A score of 1 means the class is one coherent thing. A score of *n* means it splits cleanly into *n* independent groups.

size_padding scored 55. Sixty-one methods, and the tool's own words for it:

This class may be doing 55 separate jobs.

The important part isn't the number — it's that LCOM4 names the groups:

```
{<anonymous:4139>, <anonymous:4161>, get_significand_size ...},{<anonymous:4159>, adjust_precision, format_dragon ...},{<anonymous:4187>} ...
```

That’s not a vague “this class is too big” complaint. Those are the literal method sets that could become their own classes tomorrow.

I went with LCOM4 over the more commonly cited LCOM1 specifically because of this case. LCOM1 counts method pairs that share no field against pairs that do — and it badly overstates on classes with accessors. A getter touching exactly one field looks “unrelated” to every method that doesn’t touch that field, so a well-designed class with several properties can score terribly under LCOM1 for no real reason. LCOM4 fixes this by also connecting methods that call each other, which is what a delegating method actually does even when it touches no fields itself.

When LCOM1 is high but LCOM4 is 1, the honest read is usually accessors, not a design flaw. The two disagreeing is itself informative — which is why RAAG reports both.

```
Ca   Ce   I     File335  32  0.09   nlohmann/json.hpp
```

335 files depend on this single header. It has an instability of 0.09 — about as stable as a file can be. That’s not a coincidence; it’s nlohmann/json's entire public API surface in one file, exactly where you'd want maximum stability to live.

This is what a healthy foundation looks like in the numbers: extremely high Ca, very low I. It's the mirror image of the input_adapters.hpp violation above — foundational and stable, instead of foundational and shaky.

Beyond individual files, RAAG groups the entire graph into dependency layers — foundations first:

```
Layer 0: 83 files   (depend on nothing internal)Layer 1: 28 filesLayer 2: 41 files...Layer 7: 5 files... and 10 more layers
```

18 layers total. This comes from** condensing** the graph — collapsing each cycle into a single node — before running a topological sort. The condensation of any graph is always acyclic (if two components had a cycle between them, they’d be one component), which is what lets a repository with a real cycle in it still produce a usable layering. The tangle just shows up as one wide layer instead of breaking the whole analysis.

These numbers — the graph, the metrics, the resolution confidence — become the input to RAAG’s retrieval layer. When someone asks for a refactor, the request gets scoped to exactly the blast radius the dependency graph says a change can reach, and the metrics get injected directly into the prompt: not “here’s some code,” but “here’s this file, its 4 dependents, its instability score, and why that matters.”

That’s the part where this stops being a static analysis tool and starts being useful for AI-assisted refactoring.

*Building RAAG in public. Next post covers wiring these metrics into a GraphRAG pipeline that scopes AI refactoring suggestions to exactly the blast radius a change can reach.*

*Originally published at **https://amankarki.hashnode.dev** on August 15, 2026.*

[Measuring C++ Architecture: Coupling, Cycles, LCOM4](https://blog.stackademic.com/measuring-c-architecture-coupling-cycles-lcom4-7c8d3fad7be8) was originally published in [Stackademic](https://blog.stackademic.com) on Medium, where people are continuing the conversation by highlighting and responding to this story.
