The strong typedef problem is simple to describe and surprisingly hard to solve without compiler help. You want two types that share the same underlying representation but are distinct from the type system’s point of view, so that passing one where the other is expected is a compile error. C++ has had workarounds for this since before C++11, but they all carry a cost. C++26 static reflection, finalized in the working draft after the Wroclaw meeting in November 2024, offers something closer to a real solution, and understanding why requires looking at how earlier approaches fell short.
The Problem with Aliases
typedef and using create aliases, not distinct types. This is by design. They exist to give a long type a shorter name, not to introduce type-level distinctions.
using Meters = double;
using Seconds = double;
void simulate(Meters distance, Seconds time) { /* ... */ }
double dist = 9.8;
double time = 3.0;
simulate(time, dist); // Compiles. Silently wrong.
The compiler sees double in all three places and accepts the call. No warning, no error. This class of bug appears in real physics simulations, financial calculations, and any domain that has multiple quantities of the same primitive type.
The correct mental model is that you want a newtype, to borrow Haskell’s term. A newtype wraps exactly one value, is a distinct type from the compiler’s perspective, delegates arithmetic and other operations to the underlying value, and has zero runtime overhead. Haskell’s version is clean:
newtype Meters = Meters Double deriving (Num, Show, Eq, Ord)
newtype Seconds = Seconds Double deriving (Num, Show, Eq, Ord)
The deriving clause forwards the requested typeclasses automatically. Rust has structural newtypes:
struct Meters(f64);
struct Seconds(f64);
These are zero-cost and distinct types. The catch in Rust is that there is no automatic trait forwarding. If you want Meters to support addition, you implement Add manually or pull in the derive_more crate, which uses procedural macros to generate the forwarding code. Rust proc macros operate syntactically, on token streams, before the type checker runs. They cannot inspect what traits f64 implements and forward only those. The programmer has to enumerate them.
C++ library solutions hit the same wall, reached from a different direction.
What Library Solutions Can Do
NamedType, Jonathan Boccara’s library, uses CRTP and a mixin system to opt into behavior:
using Meters = NamedType<double, struct MetersTag,
Addable, Comparable, Printable>;
using Seconds = NamedType<double, struct SecondsTag,
Addable, Comparable, Printable>;
Addable, Comparable, and Printable are skill types that inject the relevant operators. The type is distinct, zero-cost, and meaningfully usable. The problem is that the skills must be listed manually. If the source type has 40 methods and you want a wrapper that forwards all 40, you write 40 entries, or you invent a macro, or you accept a partial wrapper that will silently fail to compile when a caller tries to use a method you forgot.
type_safe by Jonathan Müller takes a similar approach with different ergonomics. BOOST_STRONG_TYPEDEF predates both and requires even more boilerplate. These libraries solve the distinction problem. None of them can introspect the source type at compile time and automatically forward its interface, because that requires asking the compiler what members and methods a type has. That was not possible before C++26.
Why the Value-Based Design Matters
The reflection proposal that actually landed, P2996, is the third major attempt. The first serious push resulted in reflexpr (N4428, around 2014-2015), which was type-based: reflection operations produced types, and you queried them with trait-style templates.
// reflexpr design (never standardized)
using meta_T = reflexpr(MyStruct);
using members = std::reflect::get_data_members_t<meta_T>;
The model was familiar because it built on template metaprogramming patterns that C++ developers already knew. That familiarity was also its failure. Type-based operations compose badly. The compile cost is O(N^2) in the number of members because each query instantiates new types. You cannot feed the results into standard library algorithms because those operate on values, not type lists. The paper reached C++20 consideration and was rejected. C++23 consideration, same result.
P2996 took a fundamentally different approach: reflections are values of type std::meta::info. The ^^ operator produces one:
constexpr std::meta::info r = ^^int;
constexpr std::meta::info s = ^^MyStruct;
Queries are consteval functions that accept and return std::meta::info or ranges of it:
// Iterate all public non-static data members
for (std::meta::info member : nonstatic_data_members_of(^^MyStruct)) {
// name_of(member), type_of(member), is_public(member), etc.
}
Because reflections are values, they can be stored in constexpr variables, passed to functions, returned from functions, put in arrays, and processed with standard algorithms. Compile cost scales with what you actually do, not with the mechanical overhead of type instantiation. This is what defeated reflexpr: it required instantiating a type for every step of a traversal, and the instantiation cost accumulated.
The [::] splice operator puts a reflection back into code as a declaration or expression:
using T = [:^^int:]; // T is int
auto val = [:some_member:]; // access via reflection
Combine the ^^ operator, the query functions, and std::meta::define_class(), which synthesizes a new class definition at compile time, and you have everything needed to generate a strong typedef automatically.
What Automated Generation Looks Like
The example discussed on isocpp.org in the context of P2996 demonstrates wrapping an existing type:
struct Item {
std::string name() const;
double price() const;
void set_price(double p);
};
struct FoodItem;
struct BookItem;
struct MovieItem;
consteval {
make_strong_typedef(^^FoodItem, ^^Item);
make_strong_typedef(^^BookItem, ^^Item);
make_strong_typedef(^^MovieItem, ^^Item);
}
After this, FoodItem, BookItem, and MovieItem each have name(), price(), and set_price(), forwarded from Item. They are distinct types that cannot be mixed silently. The forwarding is generated, not written.
The make_strong_typedef function iterates member_functions_of(source), filters by is_public(), and uses define_class() to inject forwarding method stubs into the target type. Adding a method to Item propagates to all three wrappers automatically on the next compilation.
One caveat deserves mention. The consteval { ... } block syntax shown above uses EDG’s experimental implementation of code injection (P3294). Code injection was deliberately separated from P2996 and deferred, at minimum to C++29. The WG21 rationale was to ship introspection cleanly while unresolved design questions around injection semantics were worked through separately. With only P2996 and no injection support, the two-stage build approach applies: a stage-1 tool generates a header file using reflection, and stage-2 compiles against it. That is workable, if not as seamless as the one-block form.
What This Compares To
The uniqueness of P2996’s position is worth being precise about. Four properties together distinguish it from prior art: the reflection information is semantic (post-type-checking, so it knows about resolved overloads, const qualifiers, and access specifiers), it runs at compile time with zero runtime overhead, the reflections are first-class values that work with standard algorithms, and it operates in-language with no external tools or separate code generation step.
D has __traits and mixin(string) code injection. The introspection is shallow and the injection is string-based, which means you are writing code into a string and relying on the compiler to parse it correctly. Java has runtime reflection with heap allocation and no compile-time guarantees. Swift’s typealias is transparent, providing no type distinction at all and no reflection path to generate one.
Rust’s proc macro system is the closest functional comparison. It can generate strong typedef boilerplate and derive_more does exactly that. But proc macros operate on token streams before type checking; they cannot see that the wrapped type has certain trait implementations and forward only those. They generate code that will then be type-checked, not code informed by type-checking. P2996 runs after the type checker has done its work, which means the generated forwarding code can be precisely correct about what exists on the source type, without the programmer specifying it.
The Near-Term Gap
For production C++ code today, Bloomberg has a Clang fork implementing P2996, and EDG has the most complete implementation. Neither is in a shipping compiler release aimed at general use. GCC and Clang trunk work is ongoing.
In the meantime, NamedType remains the pragmatic choice for the strong typedef problem if you want explicit control over which operations a wrapper supports. If you want automatic forwarding and can tolerate a build-step solution, a stage-1 code generator using a reflection-aware Clang fork is achievable today. The clean in-language solution, where make_strong_typedef just works inside a standard compiler without a separate tooling step, waits on both compiler adoption of P2996 and the eventual resolution of P3294.
The design decision to treat reflections as values rather than types is what makes the eventual solution composable and scalable. That choice, pushed through against fifteen years of accumulated metaprogramming idioms pointing the other direction, is the reason the strong typedef problem becomes solvable without external tooling once P2996 is widely available.