Carbon's Checked Generics: Definition-Time Checking vs C++ Template Chaos
Carbon checks generics at definition time against interfaces, not per instantiation like C++ templates. This yields earlier errors, separate compilation, and clearer diagnostics, but sacrifices template metaprogramming and specialization. C++ interop requires explicit bridging.
08 Jan 2026, 06:43 UTC

The Template Error Wall
Anyone who has worked with C++ templates knows the feeling: a single typo in a generic algorithm produces 200 lines of template instantiation backtrace, pointing at standard library headers instead of your code. The compiler only discovers the error when it tries to instantiate the template with your concrete types—instantiation-time checking. Carbon takes a different bet: check the generic body once, at definition time, against explicit interface constraints.
What Definition-Time Checking Actually Means
In Carbon, a generic function declares its requirements through an interface (similar to a C++20 concept or Rust trait). The compiler type-checks the generic body once against that interface, not per instantiation. If the body uses an operation the interface doesn't promise, you get an error at the generic definition—not at every call site.
interface Comparable {
fn LessThan[me: Self, other: Self] -> Bool;
}
fn Min[T:! Comparable](a: T, b: T) -> T {
if (a.LessThan(b)) { return a; }
return b;
}
// Error at definition if Comparable lacks LessThan
// Call sites only checked for interface conformance
Contrast with C++: the equivalent template compiles fine until you instantiate Min<MyType> with a type missing operator<. The error appears in the template body, but the diagnostic context is the instantiation site—often deep in a call chain.
Separate Compilation and Build Speed
Because the generic body is validated against its interface alone, Carbon can compile generic libraries to an intermediate form without knowing all concrete types. This enables separate compilation: a generic sorting library compiles once, and downstream crates only need the interface definitions to type-check their calls. C++ header-only templates, by contrast, must be re-parsed and re-instantiated in every translation unit that uses them.
For large codebases, this difference compounds. A Carbon generic library with 50 instantiations still compiles its body once. A C++ header-only library with 50 instantiations effectively compiles 50 times (modulo precompiled headers and module support). The design goal is measurable compile-time reduction, though real-world benchmarks await a production compiler.
The Trade-Off: No Template Metaprogramming
Definition-time checking closes the door on several C++ patterns:
- Non-type template parameters: No
template<int N> struct Array. Carbon's initial generics design only accepts type parameters. - Expression SFINAE and specialization: You cannot write
enable_if_t<sizeof(T) == 4>or partially specialize a generic for pointer types. - Compile-time computation: No template metaprogramming (factorials at compile time, type list manipulation).
Carbon replaces these with interface-based polymorphism and explicit impl blocks. Want different behavior for pointers? Define a PointerLike interface and implement it for pointer types. The cost is more upfront design; the gain is predictable, readable constraints.
C++ Interop Requires a Boundary
Carbon cannot directly consume a C++ template as a generic. To call a C++ template from Carbon, you must instantiate it on the C++ side for the concrete types you need, then expose those instantiations through a Carbon-compatible interface. The toolchain can generate bindings for specific instantiations, but there is no mechanism to say "this Carbon generic maps to that C++ template" and have the compiler bridge them automatically.
Migration path for a C++ template library:
- Identify the concrete instantiations your Carbon code actually needs.
- Write C++ wrapper functions that instantiate the templates for those types.
- Expose the wrappers via Carbon's C++ interop (extern "C++" declarations).
- Define Carbon interfaces that describe the operations you need.
- Implement those interfaces for the bridged types.
This is deliberate friction: it forces you to decide on a finite set of types at the boundary, which aligns with Carbon's separate-compilation model.
How to Evaluate This Today
Carbon is pre-0.1 (as of 2024) with no production compiler. The reference interpreter (carbon-lang on GitHub) lets you experiment with definition-time error reporting. Try writing a generic function with an interface constraint, then intentionally call a missing method—the error points to the generic definition, not the call site. Compare that output to a similar C++ template error in Clang or GCC. Read the generics design doc and the C++ interop design for templates (linked in the project repo) to see the current specification. The roadmap shows generics stabilization milestones; treat all compilation-speed claims as design goals until a production toolchain exists.
Bottom Line
Carbon's checked generics trade C++'s expressive template metaprogramming for earlier errors, clearer diagnostics, and separate compilation. If your C++ codebase suffers from template bloat and indecipherable errors, and you can redesign around interfaces instead of specialization, Carbon's model is worth tracking. If you rely on non-type parameters, SFINAE, or policy-based design, expect a rewrite—not a port. Start by prototyping a small generic module in the reference interpreter; the error-message difference is the most immediate signal of whether this model fits your team.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.