The strong typedef problem is one of those C++ annoyances that everyone eventually bumps into and then spends years working around. You want FoodItem and BookItem to be genuinely different types even though they share the same internal structure. A plain using alias does not give you that. using FoodItem = Item; creates a synonym, not a distinct type, so the compiler will happily let you pass a BookItem wherever a FoodItem is expected. Type safety evaporates.
A recent thread on r/cpp shows a proof of concept that uses C++26 static reflection to solve this generically, and it is worth unpacking in detail because the approach is qualitatively different from every prior solution.
The Problem, Precisely
The desire is simple: given an existing type Item with some public interface, generate FoodItem, BookItem, and MovieItem as distinct, non-interchangeable types that expose the same interface but cannot be mixed up. The objects should have no extra runtime cost, and you should not have to write out the forwarding code by hand for each new type.
Every C++ developer has encountered this. The canonical examples are physical units (Meters vs Feet), identifier types (UserId vs SessionId), and domain wrappers like the example in the thread.
What We Had Before
The C++ ecosystem has accumulated several solutions, each with real trade-offs.
Macros. BOOST_STRONG_TYPEDEF(int, UserId) is the classic macro approach. It generates a struct with implicit conversion from the underlying type, comparison operators, and not much else. It works for scalars, but it does not generalize to arbitrary class types, and the implicit conversion often defeats the purpose.
The NamedType library. Jonathan Boccara’s NamedType library, described thoroughly on the Fluent C++ blog, takes a more principled approach. You write:
using Meters = NamedType<double, struct MetersTag, Addable, Comparable>;
The “skills” like Addable and Comparable are CRTP mixins that selectively expose operations. This is genuinely useful and sees real production use, but it requires you to enumerate the operations you want explicitly. For a type with a rich existing interface, you are back to writing a lot of boilerplate. You cannot say “give me everything Item already has.”
Manual wrapper structs. The fully explicit approach, where you write:
struct FoodItem {
explicit FoodItem(Item i) : inner(std::move(i)) {}
std::string name() const { return inner.name(); }
int price() const { return inner.price(); }
private:
Item inner;
};
This is type-safe and obvious, but it scales terribly. Every time Item grows a method, every wrapper needs updating.
Phantom types. You can tag a template parameter that never appears in the struct layout:
template<typename Tag>
struct TaggedItem {
Item inner;
};
using FoodItem = TaggedItem<struct FoodTag>;
Distinct types, zero overhead, but the interface is just inner directly. You lose the nice method names unless you add forwarding, and now you are back to the manual wrapper problem.
All of these require either limiting the interface or repeating yourself. The fundamental issue is that C++ has no built-in way to iterate over a type’s members and generate code from that iteration at compile time. Until now.
What C++26 Reflection Gives You
Proposal P2996 introduces compile-time reflection into the language. The key operator is ^^, which lifts a declaration into a std::meta::info value that you can inspect and manipulate inside consteval contexts.
constexpr std::meta::info item_reflection = ^^Item;
From that reflection value, you can enumerate public member functions:
for (auto member : std::meta::members_of(^^Item)) {
if (std::meta::is_public(member) && std::meta::is_function(member)) {
// generate a forwarding wrapper
}
}
That loop runs entirely at compile time. The result is that make_strong_typedef in the r/cpp proof of concept can walk Item’s entire public interface and stamp it into FoodItem, BookItem, and MovieItem without you writing any of it.
struct FoodItem;
struct BookItem;
struct MovieItem;
consteval {
make_strong_typedef(^^FoodItem, ^^Item);
make_strong_typedef(^^BookItem, ^^Item);
make_strong_typedef(^^MovieItem, ^^Item);
}
The consteval { } block syntax is itself notable. It lets you run arbitrary compile-time code as a statement, not just as a template argument or a constant initializer. The function make_strong_typedef receives two std::meta::info values: the incomplete target type and the source type to mirror. It then inspects the source’s public members and injects corresponding declarations and definitions into the target.
The result is that FoodItem, BookItem, and MovieItem are fully distinct types. display(fi) resolves to the FoodItem overload. Passing a BookItem where a FoodItem is expected is a compile error. The inner Item value is stored directly, so there is no vtable, no heap allocation, and no runtime indirection beyond whatever the underlying type already has.
The queue_injection Complication
The author notes that the example uses queue_injection { ... }, an EDG experimental extension that was not actually merged into the C++26 standard draft. This is worth understanding.
The difficulty is ordering. When you call make_strong_typedef(^^FoodItem, ^^Item), the compiler needs to both inspect Item and emit new declarations into FoodItem within the same translation unit, in a single pass. Some reflection operations are inherently forward-dependent: you are asking to generate code that affects a type that is not fully defined yet when the consteval block runs.
queue_injection addresses this by deferring the injection into a queue that gets processed after the current consteval phase completes, allowing single-pass code generation. Without it, you need two build stages: a first pass that generates source code, and a second pass that compiles it. This is not a new idea; it is essentially what tools like Metaclasses (Herb Sutter’s earlier proposal) required.
The fact that this two-stage approach is a viable fallback is important. The proof of concept demonstrates the shape of the solution even if the single-pass path is not standardized yet. Libraries like Circle C++ have been exploring this two-stage model for years.
What the Internal Implementation Looks Like
To make this concrete, the make_strong_typedef function at its core does something like:
consteval void make_strong_typedef(
std::meta::info target,
std::meta::info source
) {
// For each public member function of source...
for (auto member : std::meta::members_of(source)) {
if (!std::meta::is_public(member)) continue;
if (!std::meta::is_member_function(member)) continue;
if (std::meta::is_constructor(member)) {
// Generate constructors that wrap source constructors
} else {
// Generate a forwarding method that calls inner.method(args...)
}
}
// Inject a private `source inner;` data member
// Inject the generated declarations into target
}
The tricky parts are constructors (especially explicit ones, which the example handles by offering both direct and conversion constructors), operators, and any template member functions on the source type. The author notes that aggregate initialization is not fully handled, which makes sense given how much reflection-based reasoning that would require.
Where This Leaves Us
C++26 reflection is not shipping a polished make_strong_typedef in the standard library. What it ships is the machinery that makes writing one possible, in a way that is actually correct and general. That is the right division of labor. The language should provide the introspection primitives; the community should build the abstractions.
For strong typedefs specifically, this approach will eventually obsolete the NamedType pattern for most uses. You will not need to enumerate skills or manually forward methods. You define your base type once, declare the distinct aliases, and let the compiler generate the rest at build time.
The rough edges are real. queue_injection is not in C++26 as standardized. Aggregate initialization requires more work. But as a demonstration of what P2996 makes possible, the proof of concept is compelling. The same mechanism that generates strong typedefs can generate serialization code, implement design-by-contract wrappers, produce enum-to-string conversions, and handle a dozen other metaprogramming tasks that today require either macros, code generators, or careful CRTP gymnastics.
The EDG compiler is the main place to experiment with this today. The P2996 reference implementation on top of Clang is also available for testing. If you work with C++ regularly and have been following the metaclasses and reflection discussions from the past decade, this is worth getting hands-on with before it lands in production toolchains.