One of the most persistent frustrations in C++ is that typedef and using create aliases, not new types. using Meters = double and using Seconds = double are, from the compiler’s perspective, the same type. You can pass a Seconds value to a function expecting Meters and no one complains. For quantities that are semantically distinct, this is exactly the kind of silent error a type system should prevent.
For decades, C++ developers have worked around this limitation with manual struct wrappers, CRTP mixins, and macro expansions, each approach carrying its own friction. A recent r/cpp thread highlighted on isocpp.org demonstrates something more interesting: a short consteval function using C++26 static reflection can generate a fully distinct type with the complete interface of the original, automatically. This is not just a cleaner workaround. It is the feature that closes the gap.
Why Aliases Are Not Types
The problem traces back to C’s typedef, which was explicitly an aliasing mechanism for compatibility and readability, not for semantic distinction. C++11 introduced using declarations with identical semantics. Even enum class, which does create a distinct type, does not help with numeric quantities where you want arithmetic to work.
The gap has real consequences. The Mars Climate Orbiter was lost in 1999 partly due to a unit mismatch between pound-force seconds and newton-seconds, values that would have been the same type in any C codebase of the era. The language gave programmers no way to enforce the distinction without significant boilerplate.
The Workaround Landscape
Three main approaches emerged over the years.
The first is the manual struct wrapper, which has been valid since C++98:
struct Meters { double value; };
struct Seconds { double value; };
This creates distinct types, but you must manually define every operator, comparison, and I/O function you want. For a numeric type, that is a lot of boilerplate, and it is easy to miss something.
The second approach is macro expansion. BOOST_STRONG_TYPEDEF from Boost.Serialization generates a struct with an implicit constructor and an implicit conversion operator back to the underlying type. The implicit conversion undermines the guarantee: a Meters value will silently pass to any function expecting double, which is often exactly what you were trying to prevent.
The third approach, exemplified by Jonathan Boccara’s NamedType library, uses CRTP “skill” mixins to opt in to specific operations:
using Meters = fluent::NamedType<double, struct MetersTag,
fluent::Addable,
fluent::Subtractable,
fluent::Comparable,
fluent::Printable
>;
This is the best available option before C++26, but it requires you to enumerate every capability you want. If the underlying type gains a new method, your strong typedef does not inherit it automatically. And struct MetersTag is a tag declared inline to guarantee a unique phantom type per instantiation, a clever trick that works but exposes implementation details to every call site.
Jonathan Müller’s type_safe library takes a similar approach with explicit operation group inheritance. Both libraries are sound engineering, but neither eliminates the fundamental manual labor.
What C++26 Reflection Provides
The P2996 proposal, authored by Wyatt Childers, Peter Dimov, Dan Katz, Barry Revzin, Andrew Sutton, Faisal Vali, and Daveed Vandevoorde, introduces static reflection into C++26. The core mechanism is the ^^ operator, which takes any C++ entity and returns a std::meta::info value, an opaque scalar handle to compile-time metadata about that entity.
constexpr auto r = ^^int; // reflects the type int
constexpr auto r2 = ^^MyStruct; // reflects a user-defined type
The inverse operation is the splice operator [: :], which converts a std::meta::info back into a usable C++ construct:
using T = [:^^int:]; // T is int
The <meta> header provides a rich API of consteval functions for querying metadata:
std::meta::nonstatic_data_members_of(^^MyStruct) // all data members
std::meta::member_functions_of(^^MyClass) // all member functions
std::meta::identifier_of(r) // name as string_view
std::meta::type_of(r) // type of a member
std::meta::is_public(r) // access predicates
Combined with consteval blocks, which execute arbitrary compile-time code as a statement in otherwise non-consteval contexts, and the template for construct that expands a loop body for each element of a compile-time range, you can write code that inspects an existing type and generates a new one.
The make_strong_typedef Pattern
The approach shown in the r/cpp thread is direct. You call a consteval function with reflections of the target name and the source type, and it generates a complete wrapper:
struct Item { /* name(), price() as methods */ };
struct FoodItem;
struct BookItem;
struct MovieItem;
consteval {
make_strong_typedef(^^FoodItem, ^^Item);
make_strong_typedef(^^BookItem, ^^Item);
make_strong_typedef(^^MovieItem, ^^Item);
}
After this consteval block runs, FoodItem, BookItem, and MovieItem are fully distinct types, each with the complete interface of Item. A function overloaded on FoodItem& and BookItem& will not accept a plain Item, and the two food and book types cannot be mixed.
The implementation of make_strong_typedef uses the metadata API to enumerate all public member functions of the source type and inject forwarding wrappers into the target. The key is std::meta::define_aggregate, which constructs a new struct type from a description at compile time, and template for to iterate over members:
consteval void make_strong_typedef(std::meta::info target, std::meta::info source) {
std::vector<std::meta::nsdm_description> members;
// Copy data members from source
for (auto mem : std::meta::nonstatic_data_members_of(source)) {
members.push_back({
.type = std::meta::type_of(mem),
.name = std::string(std::meta::identifier_of(mem))
});
}
// Member functions are injected via forwarding wrappers
std::meta::define_aggregate(target, members);
// ... inject method wrappers via queue_injection or define_class
}
The thread’s author notes that queue_injection was used with the EDG experimental compiler but was not integrated into the C++26 standard as specified. The EDG implementation, maintained by Daveed Vandevoorde who is a P2996 co-author, has been the primary testbed for these patterns. A Bloomberg-maintained Clang fork also implements the proposal and is accessible on Compiler Explorer under “clang (experimental reflection)”. Both targets let you prototype these ideas today.
How Other Languages Handle This
Looking at how other languages approach the same problem clarifies what C++26 is converging toward.
Haskell has a language keyword for it. newtype Meters = Meters Double creates a distinct type that is identical to Double at runtime, with zero overhead. The GeneralizedNewtypeDeriving extension lets you derive any typeclass the inner type implements automatically. This is the cleanest possible solution: the language was designed with the distinction between aliases and new types as a first-class concept.
Rust uses the newtype pattern, a single-field tuple struct:
struct Meters(f64);
struct Seconds(f64);
This creates a distinct type, but you must implement traits like Add, Display, and From explicitly or via derive. Libraries like derive_more and nutype automate much of this. #[repr(transparent)] guarantees ABI equivalence with the inner type for FFI purposes.
Go has named type declarations that create distinct types with the same underlying representation. type Meters float64 produces a type that inherits the operations of float64 but cannot be mixed with Seconds float64 without an explicit conversion. This is arguably closer to what opaque typedef (the never-adopted WG21 proposal N3741 from 2013) was trying to accomplish.
C++ is arriving at the same destination through a different route: rather than adding a dedicated keyword, reflection gives you the tools to build it yourself. The tradeoff is that it requires more machinery to set up, but the result is more flexible. You can create strong typedefs that wrap only a subset of the original interface, add invariants, or compose multiple sources.
What Still Needs Work
The pattern as demonstrated is a proof of concept. Aggregate initialization is not fully handled, and the method injection mechanism depends on code injection APIs that are still being refined. The distinction between what EDG implements experimentally and what is actually in the C++26 working draft matters here: queue_injection as used in the demo is not the final API.
The code injection story in P2996 went through several revisions. Early drafts used queue_injection with code fragments; later revisions shifted toward define_aggregate and define_class for structured type construction. The version voted into the C++26 draft uses define_aggregate for data member layout and separate mechanisms for injecting methods. The exact API surface for full method generation is still settling.
That said, the fundamental capability is there. The ability to enumerate member functions via member_functions_of, read their signatures via parameters_of and type_of, and inject forwarding wrappers is all supported. The author’s note about needing two build stages without queue_injection is the realistic current constraint: you generate a header in a first pass, then compile against it. Not ideal, but not a fundamental limitation.
The Broader Significance
Strong typedefs are one of the cleaner demonstrations of what static reflection enables because the desired output is well-defined. You want a type that is distinct but interface-compatible. Every other approach in C++ involves either manual enumeration of operations or leaking the implementation through implicit conversions.
Reflection makes this a solved problem in the way Haskell solved it with newtype and Rust solved it with tuple structs plus trait derive, except the C++ solution is entirely library-level. No new keywords, no changes to the type system’s semantics. A single consteval function does it.
For anyone writing C++ that distinguishes between semantically different quantities, types, or identifiers, this pattern deserves attention. The tooling is experimental but functional today via the Clang P2996 fork on Compiler Explorer. By the time compilers ship C++26 support broadly, the API will be stable and the boilerplate that has accumulated in codebases handling units, identifiers, or domain-typed values can start to disappear.