There is a class of bug in C++ that the type system has no interest in preventing by default. You define a UserId and an OrderId, both as aliases over int, and the compiler happily lets you pass one where the other is expected. The function signature lies to you, and the compiler considers that acceptable.
using UserId = int;
using OrderId = int;
void process(UserId user, OrderId order);
OrderId oid = 42;
UserId uid = 99;
process(oid, uid); // compiles fine, arguments are silently swapped
This is not a contrived example. It appears in payment systems, database layers, anything that distinguishes between multiple integer-shaped identifiers. The alias gives you a name, not a type.
The Pre-Reflection Workarounds
The C++ community has known about this for years and built libraries to paper over it. Jonathan Boccara’s NamedType is the most widely cited solution: a CRTP-based template that wraps a value type and tags it with a phantom type to prevent accidental conversions. You opt into specific capabilities by inheriting from skill mixins like Addable or Comparable.
using UserId = NamedType<int, struct UserIdTag, Comparable>;
using OrderId = NamedType<int, struct OrderIdTag, Comparable>;
Now they are distinct types. process(oid, uid) no longer compiles. The zero-cost abstraction claim holds up under inspection: in optimized builds, the wrapper collapses entirely and you get the same assembly as a raw int.
The problem is ergonomics. If the wrapped type has methods, you either do not forward them (forcing callers to .get() every time), or you manually write forwarding wrappers for each one. For a simple int, skills cover enough ground. For a richer domain type, you end up writing more boilerplate than the thing you were trying to avoid.
Foonathan’s type_safe library takes a similar approach with more focus on integer semantics. Boost has BOOST_STRONG_TYPEDEF. They all share the same fundamental limitation: the forwarding layer is written by hand or not written at all.
Reflection Changes the Forwarding Equation
The r/cpp thread linked from isocpp.org shows what becomes possible with C++26 static reflection: a make_strong_typedef that generates the wrapper automatically by introspecting the source type at compile time.
The core of P2996, which was voted into C++26 at the Sofia meeting in February 2025, is the ^^ reflection operator. It takes a type, function, variable, or namespace and produces a std::meta::info value representing its compile-time metadata.
constexpr std::meta::info r = ^^Point; // reflects the type Point
The inverse is the splice operator [: r :], which turns a std::meta::info back into a usable language construct. Together, they let you write code that operates on types the way you would operate on data.
The standard library provides a set of consteval functions in the std::meta namespace for interrogating these values:
// All non-static data members of a type
consteval auto nonstatic_data_members_of(std::meta::info)
-> std::vector<std::meta::info>;
// All member functions
consteval auto member_functions_of(std::meta::info)
-> std::vector<std::meta::info>;
// Name of a reflected entity as a string_view
consteval auto identifier_of(std::meta::info) -> std::string_view;
// Type of a reflected entity
consteval auto type_of(std::meta::info) -> std::meta::info;
Combined with P1306’s expansion statements (template for), also accepted for C++26, you can iterate over the members of a type at compile time and do something for each one:
template <typename T>
void print_fields(const T& obj) {
template for (constexpr auto member : std::meta::nonstatic_data_members_of(^^T)) {
std::cout << std::meta::identifier_of(member)
<< ": " << obj.[:member:] << "\n";
}
}
For strong typedefs, the relevant part of the API is define_class and data_member_spec, which let you synthesize a new struct from a list of member descriptors:
consteval std::meta::info make_strong_typedef(std::meta::info underlying) {
std::vector<std::meta::info> members;
for (auto m : std::meta::nonstatic_data_members_of(underlying)) {
members.push_back(std::meta::data_member_spec(
std::meta::type_of(m),
{.name = std::meta::identifier_of(m)}
));
}
// Returns a std::meta::info for the newly defined type
return std::meta::define_class(^^SomeUniqueTag, members);
}
The resulting type has the same fields as the original, but it is a distinct type from it. The compiler sees no relationship between them, so accidental conversions fail at compile time rather than silently succeeding.
Where the Thread Hits a Limit
The r/cpp post notes that it uses queue_injection { ... } from the EDG experimental compiler. This is where things get technically interesting, because queue_injection did not make C++26.
The injection mechanism came from a companion paper, P3294, which covers arbitrary code injection via token sequences. It was deferred for a future standard. What P2996 gives you is read access to type structure and the ability to synthesize new types through define_class, but you cannot inject arbitrary new member function declarations into a type that already exists in the current translation unit.
That distinction matters for the strong typedef use case. Copying data members is straightforward. Forwarding member functions is harder without injection, because you need to emit actual function definitions into the new type, and that is precisely what token-sequence injection was designed for.
The article acknowledges this honestly: without queue_injection, you need two build stages. In the first stage, you run reflection code that emits a generated header. In the second stage, you compile your actual code against that header. This is how tools like Clang’s P2996 experimental branch at Bloomberg approach the gap. It works, but it reintroduces build complexity that the injection approach was meant to eliminate.
Comparing With Rust’s Newtype
Rust handles this at the language level with the newtype pattern, and the ergonomic gap is worth acknowledging:
struct UserId(i32);
struct OrderId(i32);
fn process(user: UserId, order: OrderId) { /* ... */ }
let uid = UserId(99);
let oid = OrderId(42);
// process(oid, uid); // compile error: mismatched types
Rust newtypes are zero-cost, enforce distinctness by default, and require no library support. The tradeoff is that you do not automatically inherit the inner type’s trait implementations; you implement them manually or use the Deref trait for forwarding, which has its own footguns. The forwarding problem exists in Rust too, it just has different contours.
Haskell’s newtype keyword is the cleanest version of this idea: the wrapper is erased at runtime entirely, type safety is total, and derivation (deriving (Show, Eq, Ord)) handles much of the forwarding automatically. C++ is a long way from that, but C++26 reflection is a step in a recognizable direction.
What C++26 Reflection Actually Delivers
The table is clearer than the marketing:
| Feature | In C++26 (P2996) |
|---|---|
^^T reflect operator | Yes |
[:r:] splice operator | Yes |
nonstatic_data_members_of | Yes |
member_functions_of | Yes |
define_class / data_member_spec | Yes |
template for expansion statements | Yes |
queue_injection / token injection | No (P3294, deferred) |
| Injecting declarations into existing types | No |
For the strong typedef use case specifically: you can synthesize a new type with the same data layout as an existing one. You can iterate over member functions of the original type. You can splice calls to those functions from a wrapper. What you cannot yet do in a single compilation pass is automatically emit forwarded method definitions into the new type, which is the part that eliminates the remaining boilerplate.
The r/cpp proof of concept is real and working, but it leans on an experimental EDG extension that has not been standardized. The two-build-stage fallback produces the same result with more infrastructure. Both are better than writing NamedType skill mixins by hand, but neither is quite the seamless make_strong_typedef(^^FoodItem, ^^Item) that the code example implies.
P3294 will get there eventually. The foundational machinery in P2996 is already more powerful than most C++ codebases will use in the first few years after adoption. Strong typedefs from reflection are a compelling demonstration of what compile-time introspection enables, and the core mechanism, reflecting over a type’s members and synthesizing a new struct from that information, works today in the experimental tooling. The full zero-boilerplate version is waiting on the injection paper to clear standardization.
For a language that spent the better part of a decade arguing about whether static reflection was even the right design direction, getting the read side into C++26 is not a small thing.