Why Carbon Replaced C++ Templates with Checked Generics
Carbon replaces C++ templates with checked generics verified at the definition site against explicit interfaces — enabling separate compilation, cleaner errors, and an explicit static-vs-dynamic dispatch choice.
17 Dec 2025, 19:46 UTC

If you've ever stared at a 200-line C++ compiler error because you passed the wrong type to std::sort, you've experienced the core problem Carbon's generics system is designed to fix. C++ templates are checked at the point of instantiation — the compiler only discovers your mistake when it stamps out a concrete version of the template, deep inside someone else's header. Carbon's answer is checked generics: generic code is verified once, at its definition, against an explicitly declared interface. The error surfaces where the bug actually lives.
This post walks through what that design decision buys you, what the syntax looks like, and where the trade-offs land. One caveat up front: Carbon is experimental (pre-0.1 as of this writing), so treat the syntax below as illustrative of the design, not a stable reference. Check the current design docs in the carbon-lang repository before relying on details.
The problem with instantiation-time checking
A C++ template is essentially a compile-time macro with type awareness. The compiler doesn't fully check the template body until you call it with concrete types. That has three practical costs:
- Error messages point at the wrong place. A type error inside
std::sortreports deep in library internals, not at your call site. - No separate compilation. Every translation unit that uses a template must see its full definition, so templates live in headers and get re-parsed and re-instantiated everywhere. Build times suffer.
- Constraints are bolted on. C++20 concepts improved things enormously, but they're layered on top of a model that was never designed for definition-site checking, and unconstrained templates still behave the old way.
Carbon's designers decided that generics — not templates — would be the language's parametric polymorphism mechanism, with constraints mandatory rather than optional.
Checked generics: interfaces as contracts
In Carbon, a generic parameter declares what interface it satisfies, using the :! ("type-of-type") binding syntax:
fn Sort[T:! Comparable](data: Vector[T]) {
// Body may only use operations guaranteed by Comparable
}
The key semantic rule: the compiler checks the body of Sort using only the operations promised by Comparable. It doesn't matter what concrete T you eventually pass — the definition is verified once, standalone. If the body calls a method that Comparable doesn't provide, you get an error at the definition, even if some concrete type would have supported it.
This flips the C++ model on its head:
| Property | C++ templates | Carbon checked generics |
|---|---|---|
| Where the body is checked | At each instantiation | Once, at definition |
| Constraints | Optional (concepts since C++20) | Mandatory interface bounds |
| Separate compilation | No (definitions in headers) | Yes, by design |
| Error location | Instantiation backtrace | Definition site or call site, cleanly |
Why separate compilation is the real prize
The error-message improvement is nice, but the engineering payoff is compilation architecture. Because a generic's body is fully checked against its interface, Carbon can compile that definition once into a reusable artifact. Callers don't need the source. Monomorphization — generating the concrete machine code for each T — can happen at the consuming side, at link time, or be deferred.
For large codebases this matters in concrete ways: generic libraries can ship as compiled packages, incremental rebuilds don't re-instantiate the world when a header changes, and distributed build caches can treat generic definitions as stable units. Anyone who has watched a C++ build recompile thousands of template instantiations after touching a widely-included header knows what this is worth.
Static dispatch vs. type erasure
Carbon also gives you an explicit choice between monomorphized generics and dynamic dispatch, similar in spirit to Rust's impl Trait versus dyn Trait. A generic parameter bound to an interface gets static dispatch: a specialized copy of the code per concrete type, with full inlining potential and zero runtime indirection. Using a dynamic interface type instead erases the concrete type behind a vtable-like mechanism: one copy of the code, smaller binaries, but indirect calls and no inlining across the boundary.
The decision rule is the familiar one: reach for checked generics by default (performance, static verification), and switch to type erasure when code size dominates or you need heterogeneous collections — a Vector[dyn Drawable] holding many unrelated types, for example. The point is that Carbon makes this a deliberate, visible choice in the signature rather than an accident of how the template got instantiated.
Limitations and honest caveats
- The language is pre-0.1. Syntax, interface semantics, and the compilation model can all change. The explorer at carbon-lang.dev demonstrates basic generics, but full separate compilation of generics across package boundaries is still under active development.
- C++ interop is manual. There's no automatic translation of heavy template metaprogramming (SFINAE, template-template parameters, expression templates). Simple constrained templates map to interfaces reasonably well; the exotic stuff needs human redesign.
- The standard interface library is young. Interfaces like
Comparableare still evolving and don't yet cover the full surface of C++20 concepts. - No published performance data. Claims about monomorphization and type-erasure performance are design intentions, not benchmarked results.
How to check this yourself
The most productive way to evaluate the design is hands-on: open the Carbon explorer, write a generic function with an interface bound, then deliberately call an operation the interface doesn't provide. You should see the error attributed to the definition — that's the whole thesis in one experiment. Then read the generics design document in the carbon-lang repository under docs/design, and check the toolchain's status page for which parts of separate compilation are actually implemented versus still on the roadmap.
The takeaway: Carbon's checked generics aren't a cosmetic cleanup of templates. They're a bet that mandatory interface constraints — checked once, at the definition — are the foundation that makes separate compilation, fast incremental builds, and sane error messages all possible at the same time. If that bet holds, it removes one of the biggest structural taxes C++ codebases pay today.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.