Strong Typedefs Without the Boilerplate: What C++26 Reflection Changes
Source: isocpp
C++ has had a type aliasing problem since the beginning. typedef int UserId and typedef int ProductId look like distinct types, but the compiler treats them as identical. Pass a ProductId where a UserId is expected and nothing complains. This is not a theoretical concern; it causes real bugs in systems where IDs, indices, handles, and measurements all share the same underlying integer type.
A recent thread on r/cpp demonstrates how C++26’s compile-time reflection addresses this directly, showing a make_strong_typedef function that synthesizes fully distinct wrapper types by reflecting over a source type at compile time. The result is clean enough that it is worth understanding what is happening under the hood, how it compares to what came before, and where the current gaps still are.
The Decades of Workarounds
The simplest approach to strong typedefs in C++ has always been wrapping the type in a struct:
struct UserId {
int value;
};
UserId is genuinely distinct from ProductId even if both hold an int. The problem emerges when wrapping something more complex. If you wrap std::string, you need to forward size(), substr(), find(), operator[], and everything else you want to use. The wrapper’s interface must be written by hand, method by method.
Boost’s BOOST_STRONG_TYPEDEF macro generates some of this forwarding, but it dates to an era of very limited metaprogramming and has always felt like infrastructure that predates the problem being fully understood. Jonathan Boccara’s NamedType library is a more principled approach: you construct a strong type by composing template parameters that opt into specific behaviors, such as Addable, Comparable, or Printable. The wrapper stays distinct, and you control exactly which operations are permitted. The cost is that you must enumerate every capability you want, which converts the original problem of implicit permissiveness into a different problem of exhaustive annotation.
enum class was briefly popular as a workaround for wrapping integer types. It produces genuinely distinct types, plays well with overload resolution, and prevents implicit conversions. But arithmetic on enum class values requires casting, which puts you back into unsafe territory and undercuts the safety you were building.
The using keyword in C++11 added nothing to this situation. using UserId = int is transparent to the type system in exactly the same way typedef int UserId is.
External code generators have always been an escape hatch. If you run a script that reads a type definition and outputs a full wrapper, you get what you want, but the build system gets more complicated and the generated code is no longer collocated with the type it describes.
What P2996 Actually Provides
P2996, the static reflection proposal that entered C++26, gives the language a way to treat program entities as values. The ^^ operator produces a std::meta::info value that represents a type, function, variable, or data member. From a consteval context, you can query that reflected entity: enumerate its public members, inspect their names and signatures, and synthesize new code based on what you find.
The make_strong_typedef pattern from the thread works at this level:
struct Item {
std::string name() const;
int price() const;
};
struct FoodItem;
struct BookItem;
struct MovieItem;
consteval {
make_strong_typedef(^^FoodItem, ^^Item);
make_strong_typedef(^^BookItem, ^^Item);
make_strong_typedef(^^MovieItem, ^^Item);
}
After the consteval block runs, each of the three types has name() and price() methods that delegate to an internal Item value. They are fully distinct types. Passing a FoodItem where a BookItem is expected is a compile error, even though both expose identical interfaces.
Inside make_strong_typedef, the implementation uses std::meta::members_of() to iterate over the source type’s public interface, then generates forwarding methods on the new type for each one found. The new type stores an Item internally and each forwarding function calls through to it. All of this happens at compile time; the resulting methods are ordinary inline forwarders that the optimizer handles the same way it handles any other inlined call.
This is genuinely new ground for C++. Before P2996, producing a new type from compile-time introspection required elaborate template metaprogramming, macro abuse, or a separate code generation step. None of those options scale gracefully when the source type is complex or when you want multiple distinct wrappers over the same type.
The queue_injection Caveat
The thread author is careful about one limitation. The queue_injection { ... } syntax used with EDG’s experimental reflection compiler was not actually standardized in C++26. It appears in some experimental implementations as a mechanism for injecting code into a type that is still being defined within a single compilation pass, but the finalized standard requires a two-stage build for this kind of synthesis.
In practice, two-stage compilation means one pass emits generated source code and a second pass compiles it. Build systems can orchestrate this with enough configuration, but the developer experience is rougher than a single-pass workflow, and the generated code lives outside the normal source tree unless you build tooling to manage it.
The gap between what EDG’s experimental frontend supports and what the ratified standard mandates is a recurring issue with cutting-edge C++ features. Proposals often explore a richer design space than what ends up standardized, and implementations that follow proposals closely can look more capable than the final language spec supports.
Comparison With Rust and Haskell
Rust solves the strong typedef problem with the newtype pattern, which is a language convention rather than a feature. struct UserId(i32) creates a tuple struct wrapping a single i32, and the type system treats it as completely distinct from i32 or any other wrapper. The tradeoff is that UserId starts with no methods; you implement traits to add them. Delegation via the Deref trait is technically available but discouraged for this pattern because it leaks through the abstraction in subtle ways. Libraries like the delegate crate can forward method calls more selectively, which parallels the NamedType approach in C++.
Haskell’s newtype is the cleanest solution in any mainstream language. newtype UserId = UserId Int costs nothing at runtime because the wrapper is erased during compilation. The GeneralizedNewtypeDeriving extension lets you automatically inherit typeclass instances from the wrapped type, so if Int is Ord and Show, UserId can be too without writing any instances by hand. The abstraction is zero-cost, delegation is automatic when you want it, and the type system enforces distinctness with no annotation burden.
C++26 reflection lands somewhere between these two. Like Haskell’s GeneralizedNewtypeDeriving, it can synthesize a complete interface automatically. Unlike Haskell, it requires a consteval function to specify what forwarding policy to apply; the language does not have a fixed newtype semantics. That extra step is also a source of flexibility: the same machinery can implement forwarding that filters certain methods, transforms signatures, or adds precondition checking on top of the delegated call.
What Is Still Missing
The thread author notes that aggregate initialization is not fully handled in the proof-of-concept. If Item were a plain aggregate with no user-provided constructor, FoodItem fi{"apple", 10} should in principle work through the wrapper, but synthesizing aggregate initialization correctly across a reflection-generated type boundary is a non-trivial design problem. The current implementation sidesteps it.
Beyond aggregates, questions remain about how reflection-generated types interact with concepts, CTAD (class template argument deduction), and structured bindings. None of these are fundamental blockers for the strong typedef use case, but they represent the kind of edge cases that separate a proof of concept from production-ready infrastructure.
There are also tooling gaps. Debuggers, static analyzers, and IDEs that understand reflection-generated types are still catching up. A type whose methods were synthesized at compile time by a consteval function is harder to introspect in a debugger than one whose methods are written in source. This improves as toolchain support matures, but it is a real cost today.
The Broader Picture
The strong typedef case is a compact demonstration of what P2996 makes possible, but it points toward a much larger class of problems. The same mechanism that enumerates and forwards public methods can generate serialization code, build proxy objects for network interfaces, create instrumented wrappers for profiling, or synthesize mock types for testing. Any pattern that currently requires external codegen or deep CRTP machinery becomes a candidate.
C++ has historically reached for macros and templates in these situations. Both impose cognitive overhead on the reader and hard limits on the implementor. Reflection moves the generation step inside the language itself, where it has access to the full type system and can produce idiomatic, verifiable output.
The thread’s example is small enough to understand in one sitting and mechanically clear enough to reveal the reflection model without obscuring it in complexity. For anyone evaluating what C++26 metaprogramming actually looks like in practice, this is a better entry point than most of the theory-heavy proposals.