C++26 Reflection and the Strong Typedef Problem Nobody Could Fully Solve
Source: isocpp
Strong types in C++ have a long, frustrating history. The core idea is simple enough: you have an int that represents a user ID and another int that represents an order ID, and you want the compiler to refuse to let you pass one where the other is expected. Every language that takes type safety seriously has some answer to this. C++ has had several partial answers for thirty years, none of them satisfying. C++26 reflection, standardized through P2996, changes the situation in a meaningful way, though not quite as cleanly as the most exciting demos suggest.
The Long History of Not Quite Solving This
The simplest C++ non-solution is using UserId = int;. That creates an alias, not a distinct type. UserId and OrderId are the same type to the compiler; you get nothing.
The macro approach, represented by BOOST_STRONG_TYPEDEF, generates a thin wrapper struct with the basic arithmetic and comparison operations baked in. It works for primitive types, breaks down immediately for anything richer, and carries all the usual macro problems around scoping and composability.
The template approach, best exemplified by Jonathan Boccara’s NamedType library, uses CRTP and phantom tags to create distinct types from a template. You opt into specific capabilities via skill mixins: Comparable, Addable, Printable, and so on. This is the most ergonomic of the pre-reflection solutions, but it has a ceiling. If you want a strong typedef wrapping a struct with ten methods, you either write forwarding wrappers by hand or you give up and accept that your strong type will not behave like its underlying type.
There was a proposal, N3741, for a language-level opaque typedef keyword. It went nowhere. The metaclasses proposal from Herb Sutter gestured at similar capabilities but never reached standardization. The committee knew the problem. The mechanism to solve it cleanly did not exist until reflection.
What P2996 Actually Provides
The core of C++26 reflection is value-based introspection. The ^^ operator takes any C++ entity and produces a std::meta::info value representing it. This is a scalar compile-time constant, not a type, and that distinction matters enormously.
Previous reflection proposals for C++ were type-based: reflexpr(int) gave you a type, and you extracted information through type traits. Composing multiple levels of introspection required nested template instantiations, which compounded the already painful compile-time cost of TMP. P2996’s value-based design collapses that. std::meta::nonstatic_data_members_of(^^MyStruct) returns a std::vector<std::meta::info> at compile time. You iterate over it with a constant-expression loop. No recursive template chains.
The splice operator [: :] goes the other direction, materializing a std::meta::info back into code. [:^^int:] in a type context means int. p.[:member_reflection:] accesses the member that member_reflection refers to. Read and write are symmetric, which makes generated code look structurally like hand-written code.
For type synthesis, std::meta::define_class takes a std::meta::info representing a target type and a span of member descriptors, and fills in that type’s definition at compile time. std::meta::data_member_spec builds those descriptors. Together, these are what make the strong typedef pattern possible.
The Strong Typedef Pattern in Practice
The r/cpp thread that prompted this post shows a make_strong_typedef utility that takes two reflected types and wires them together:
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 this, FoodItem, BookItem, and MovieItem are fully distinct types. You cannot pass a BookItem to a function expecting FoodItem. Each wraps the Item interface, forwarding its members. The implementation uses nonstatic_data_members_of to enumerate Item’s members, data_member_spec to build descriptors for the wrapper, and define_class to fill in the forward-declared target types.
The rough shape of make_strong_typedef looks like this:
consteval void make_strong_typedef(
std::meta::info target,
std::meta::info source)
{
std::vector<std::meta::info> members;
for (auto m : std::meta::nonstatic_data_members_of(source)) {
members.push_back(std::meta::data_member_spec(
std::meta::type_of(m),
{ .name = std::meta::identifier_of(m) }
));
}
std::meta::define_class(target, members);
}
That handles the data layout. Method forwarding is where things get complicated.
The queue_injection Problem
The demo uses queue_injection { ... }, which lets you inject member function declarations directly into a type during the consteval phase. This is expressive and feels like the feature working the way you would want. It is also not in C++26.
Code injection is covered by P3294, which was not standardized alongside P2996. The committee separated the core introspection capability from the code synthesis capability, in part because injection semantics are harder to specify cleanly. P3294 is expected in a future standard, likely C++29.
The EDG compiler, which serves as a reference implementation for P2996, supports queue_injection experimentally, which is why the demo compiles there. The Bloomberg Clang fork with P2996 support is also available on Compiler Explorer and implements the core feature.
Without injection, forwarding member functions in a single compilation pass is not possible through standard C++26. The practical workaround is a two-stage build: the first stage runs reflection code that writes a generated header, the second stage compiles against that header. This is how tools like protobuf and flatbuffers work today, and it is functional, but it reintroduces build infrastructure complexity that the promise of in-language reflection was supposed to eliminate.
This is worth being clear about when reading excited posts about C++26 reflection: the demos often combine P2996 (standardized), P1306 expansion statements (not standardized), and P3294 injection (not standardized) in ways that look seamless. The underlying capability is real and significant. The syntax of the most convenient demos is not yet standard.
How This Compares to Other Languages
Rust’s answer to strong typedefs is the newtype pattern: struct Meters(f64);. That’s it. You get a fully distinct type, zero runtime overhead, and complete type safety. The limitation is that you do not get any of f64’s methods automatically. If you want Meters to support addition, you implement Add. If you want it to print, you implement Display. The derive attribute handles standard traits, and the derive_more crate extends this. But forwarding a rich domain type’s interface still requires either manual implementations or the Deref trait, which introduces its own footguns around coercive subtyping.
Haskell has the cleanest version of this. newtype Meters = Meters Double deriving (Show, Eq, Ord, Num) gives you a distinct type with complete interface forwarding through the typeclass system. GeneralizedNewtypeDeriving can automatically derive any typeclass the inner type supports. The language was designed for this and it shows.
D’s __traits provides string-based reflection at compile time: __traits(allMembers, T) gives you member names as strings, and mixin lets you generate code from those strings. It works, but the string-based representation is weakly typed and non-compositional. You cannot carry the result of one reflection query as meaningful input to another without parsing.
C++26 sits between Haskell and Rust in this comparison. The reflection system is richer than what Rust’s proc macros operate on, because proc macros work on syntax before type-checking while P2996 operates on the semantic model after. C++26 knows the complete, type-checked interface of any type it reflects. The forwarding story is limited today by the injection gap, but once P3294 lands, C++ will be able to do what Haskell’s GeneralizedNewtypeDeriving does, for arbitrary types, including types with complex interfaces that would be painful to enumerate manually.
Why Value-Based Reflection Changes the Cost Model
The compile-time performance difference between value-based and type-based reflection is worth noting. In Boost.Hana, iterating over the members of a struct involves recursive template instantiations: each step creates a new type, the compiler must memoize it, and the total cost is roughly O(N) template instantiations for N members, each of which has its own overhead. For structs with many members, this accumulates noticeably.
With P2996, nonstatic_data_members_of(^^T) is a single compiler intrinsic call. The result is a constexpr vector of std::meta::info values. Iterating over it in a consteval context does not create new types per iteration. The compile-time cost model is closer to procedural code than to template instantiation chains, which is a meaningful improvement for large-scale use of generative patterns.
Runtime overhead is zero. Everything in the std::meta namespace is compile-time only. The generated wrapper types are identical to hand-written wrappers; there is no additional indirection, virtual dispatch, or metadata surviving to the binary.
What You Can Actually Build Today
If you want to experiment with this now, the Bloomberg Clang fork on Compiler Explorer supports the core P2996 features. For full demos including injection, the EDG experimental build is your best option. Neither is ready for production codebases, and MSVC has not yet committed a timeline for P2996 support.
For production use of strong typedefs today, NamedType remains the most practical library option for types with primitive-ish interfaces. For richer types, the two-stage codegen approach (reflection generates headers, headers get compiled) is the path that P2996’s standardized subset enables right now.
The capability that makes the demos exciting, automatically wrapping any arbitrary struct’s full interface in a distinct type through a single make_strong_typedef call, is real and works in experimental compilers. Standardizing the full picture will require C++26 for P2996 plus a future standard for P3294. That is a longer timeline than the demos imply, but the direction is clear and the underlying design is sound.