Carbon's Checked Generics: Fixing the C++ Template Error Message Problem
Carbon's checked generics are validated against declared interfaces at definition time, not at instantiation like C++ templates. Here's what that changes, with a worked comparison and honest trade-offs.
26 May 2026, 06:39 UTC

If you've ever passed the wrong type to a C++ template and received a hundred-line error stack pointing into std::vector internals, you already understand the problem Carbon's generics system is designed to solve. The short version: Carbon checks generic code against its declared requirements at the point where the generic is defined, not where it's used. That single design decision changes where errors surface, how readable they are, and what library authors can promise their callers.
One caveat up front: Carbon is an experimental language, not a shipping product. Its syntax and semantics are still evolving, so treat the examples here as illustrations of the design rather than copy-paste-ready code. Check the current language design documents before relying on any specific syntax.
How C++ templates check constraints (late)
In classic C++, a template's requirements on its type parameters are implicit. You write operations on T, and the compiler only discovers whether T supports them when the template is instantiated with a concrete type:
template <typename T>
T Sum(const std::vector<T>& values) {
T total{};
for (const T& v : values) {
total += v; // requires: default-constructible, operator+=
}
return total;
}Nothing in the signature says T must support += or default construction. If you call Sum with a type that lacks operator+=, the compiler reports the failure inside the body of Sum — often several template-instantiation layers deep. The error is about the implementation, not the contract. C++20 concepts improve this significantly by letting you write template <std::semiregular T> and getting errors at the call site, but concepts are opt-in, and a large amount of existing template code remains unconstrained.
How Carbon checks generics (early)
Carbon's generics flip the default. A generic parameter declares an explicit interface constraint, and the compiler fully type-checks the generic's body against that interface when the generic is defined — before any call exists. The design intent is that a successfully compiled generic is valid for every type satisfying its interface, not just the ones you happened to test with.
Conceptually, the same sum looks like this (syntax simplified and subject to change — verify against the current design docs):
interface Addable {
fn Add[self: Self](other: Self) -> Self;
fn Zero() -> Self;
}
fn Sum[T:! Addable](values: Slice(T)) -> T {
var total: T = T.Zero();
for (v: T in values) {
total = total.Add(v);
}
return total;
}Two things follow from this structure:
- Definition-time checking. If the body of
Sumcalls a method thatAddabledoesn't declare, the error points atSumitself, immediately — even if no one ever calls it. The library author finds out, not the downstream user. - Call-site errors reference the contract. If a caller passes a type that doesn't implement
Addable, the error says so directly: this type doesn't satisfy this interface. There's no instantiation backtrace because the body was already validated against the interface.
This is sometimes described as the difference between "duck typing with late diagnosis" (templates) and "nominal contracts with early diagnosis" (checked generics, similar in spirit to Rust traits or Swift protocols).
Why this matters beyond nicer errors
Readable diagnostics are the visible benefit, but the deeper one is a stable contract between library and caller. Because a Carbon generic is checked against its interface alone, the library author can change the implementation without risking new compile errors in downstream instantiations, and callers know exactly what capabilities they must provide. It also enables separate type-checking of generic definitions, which has implications for compile times in large codebases — though Carbon's own compiler is not mature enough to make performance claims about this today.
The trade-offs
Checked generics aren't free:
- More upfront annotation. You must name and declare the interface, and implement it explicitly for your types. C++ templates let any type with the right shape work with zero ceremony.
- Less accidental flexibility. A C++ template silently works with any type that happens to have a suitable
operator+=. In Carbon, a type that structurally fits but doesn't declare the interface is rejected until someone writes the impl. That's usually what you want, but it adds friction for quick scripts and for wrapping third-party types. - C++ is closing the gap. C++20 concepts deliver much of the error-message improvement. Carbon's advantage is that constraints are the default and checking is complete at definition time — an ergonomics and defaults argument, not a wholly new capability.
Try it yourself
The most convincing way to evaluate this is a side-by-side experiment:
- Write the unconstrained C++
Sumabove and instantiate it with a type lackingoperator+=. Read the diagnostic. - Add a C++20
conceptconstraint and compare the new error. - Write the equivalent generic in Carbon using the current explorer or toolchain build, and trigger both failure modes: a body that exceeds its interface, and a caller whose type doesn't implement it.
Compare where each error points and how much context you need to decode it. That comparison — not any benchmark — is the honest case for Carbon's generics design. And keep the experimental status in mind: Carbon is a design exploration with an open future, so treat what you learn as insight into where systems-language generics are heading, not as a migration plan.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.