Strong Types Without Boilerplate: What C++26 Reflection Finally Changes
Source: isocpp
The problem with type aliases in C++ is not subtle. using Meters = double and using Seconds = double produce exactly one type: double. The compiler has no memory of the alias names. Pass a Seconds value where a Meters is expected, and the code compiles without complaint. Transparent aliasing has been C++‘s weak point relative to other typed languages for decades, and the workarounds have always been painful.
A recent r/cpp thread via isocpp.org demonstrates how C++26 static reflection dissolves most of this pain with a compact consteval function. The approach is worth understanding in context, both for what it achieves and for what parts of it are still experimental.
The Pre-Reflection Landscape
Before C++26, the canonical approaches each came with costs.
BOOST_STRONG_TYPEDEF from Boost.Serialization is the oldest widely-used solution. It emits a struct wrapping the underlying type with an explicit constructor and common operators. It works, but macros do not compose with templates or namespaces cleanly, and the generated type often turns out to be missing operators at inconvenient moments.
Jonathan Boccara’s NamedType library improved on this with a CRTP “skills” system:
using Meters = fluent::NamedType<double, struct MetersTag,
fluent::Addable, fluent::Comparable, fluent::Hashable>;
You enumerate capabilities explicitly. Miss fluent::Hashable and the type fails silently as an unordered_map key. The library is well-designed, but it requires you to know in advance which operations you need, and the skill list grows long for types with rich interfaces.
Hand-rolled phantom type templates have the same problem. Every operator, every std::hash specialisation, every std::formatter instance must be written twice: once for the underlying type and once for the wrapper. For anything more complex than a scalar, the boilerplate surface becomes significant.
enum class solves the integer case. enum class UserId : int {} creates a distinct type with no implicit arithmetic, which is exactly right for opaque identifiers. There is no analogous feature for floating-point, string, or composite types.
What Other Languages Got Right
The comparison with other languages clarifies what C++ has been missing.
Haskell’s newtype has been a first-class language construct since 1990. newtype Meters = Meters Double creates a type with identical runtime representation to Double but entirely distinct at the type-system level. The deriving (Eq, Ord, Num, Show) mechanism generates every required instance from the underlying type automatically. GeneralizedNewtypeDeriving extends this to any typeclass. You get complete operator coverage without enumerating anything.
Rust uses a single-field tuple struct: struct Meters(f64). There is no automatic operator forwarding, but the derive_more crate provides proc macros that generate arithmetic and comparison trait implementations from the inner type. The pattern is idiomatic and widely used, partly because Rust’s orphan rules make newtypes the standard way to implement foreign traits on foreign types.
Go’s named types (type Meters float64) are the closest analog to what C++26 enables. They inherit all operators of the underlying type while remaining distinct for type-checking, with explicit conversions required to mix them. This is clean and requires no boilerplate at all.
The shared pattern across these languages: a zero-cost wrapper should inherit its underlying type’s behaviour without the programmer having to enumerate that behaviour. C++ has never expressed this cleanly.
What C++26 Reflection Provides
P2996, “Reflection for C++26,” adds a compile-time introspection facility built around the ^^ operator and the std::meta::info type. ^^T produces a compile-time handle to T. From that handle you can query the type’s structure: std::meta::member_functions_of(^^T) returns a range of reflected handles to every member function, std::meta::nonstatic_data_members_of(^^T) covers data members, and std::meta::name_of(r) retrieves identifiers as string views.
Splice operators complete the pair: typename [: r :] turns a std::meta::info back into a usable type name, and [: r :] splices a reflected member into an expression. Combined with consteval blocks (P3289), which are anonymous immediate-function bodies executable at class or namespace scope, these tools allow code that iterates over reflected members and generates new declarations for each one at compile time.
The r/cpp post demonstrates this with make_strong_typedef, called inside a consteval { } block:
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);
}
make_strong_typedef receives two std::meta::info values: the target type to define and the source type to reflect on. Inside, it iterates over Item’s public member functions and injects equivalent declarations onto FoodItem, delegating each to a stored inner Item value. The result is three fully distinct types.
FoodItem fi("apple", 10);
BookItem bi("the art of war", 20);
MovieItem mi("interstellar", 25);
display(fi); // resolves to display(FoodItem&)
display(bi); // resolves to display(BookItem&)
// display(Item{"hello", 1}); // error: no matching overload
FoodItem and BookItem have the same methods as Item, they construct from the same arguments, and they overload independently. What they cannot do is masquerade as each other.
The mechanics underneath this are what make it genuinely different from previous C++ approaches. Rather than manually listing operations or writing macro expansions, make_strong_typedef uses std::meta::member_functions_of to discover what Item exposes at compile time and generates wrapper declarations for each one. Add a method to Item and all strong typedefs derived from it automatically gain that method on the next build. This is the property that hand-rolled phantom types and NamedType skills cannot provide without manual updates.
The queue_injection Caveat
The post is transparent about one complication: it uses queue_injection { ... } with the EDG experimental reflection compiler, and explicitly notes this mechanism was not included in the finalised C++26 standard. queue_injection comes from P3294, “Code Injection with Token Sequences,” which was still under revision when C++26 was finalised.
Without queue_injection, the same result requires a two-stage build: a code-generation step reads the source type and emits C++ for the strong typedef, then a second compilation consumes the output. This is how Protobuf, Qt moc, and many IDL compilers have always worked. It is functional but breaks the self-contained build model that makes reflection so appealing.
P2996 does include std::meta::define_class, which constructs a new type programmatically from a list of reflected member descriptors created with data_member_spec. This handles data members well but not arbitrary member function bodies with implementations. For the specific case of wrapping all methods of a source type, you still need either queue_injection or the two-stage approach.
The practical path for experimentation today is bloomberg/clang-p2996 or the EDG experimental build available on Compiler Explorer. Both implement P2996 with varying degrees of P3294 support. Neither is a production compiler, but both are sufficient for proofs of concept like the one in the thread.
What This Changes for Library Authors
The significance of this pattern goes beyond strong typedefs. The same capability, reflecting on a source type and generating declarations mirroring its structure onto a target type, applies to a wide range of currently macro-heavy domains.
Serialisation frameworks that require per-field macro annotations can instead iterate over nonstatic_data_members_of and generate serialisers automatically. Property systems for game engines that currently rely on IDL compilers can derive getters, setters, and change-notification hooks from a plain data struct. Unit-of-measure libraries can generate dimension-checked arithmetic without maintaining a combinatorial table of type pairs. In each case, the existing type definition is the source of truth, and reflection is what lets generated code stay synchronised with it.
Strong typedefs are a clean demonstration because the source type already defines the complete interface the wrapper needs to expose. Reflection lets you read that interface and reproduce it under a new name. Every other boilerplate-heavy pattern in C++ reduces to a similar shape: one type or schema defines the structure, and something else must mirror it without modifying the original.
What C++26 gives library authors is a way to write that mirroring logic once, in ordinary C++, without a separate build step, without a macro with rough edges, and without asking users to enumerate operations they need. That is what languages with newtype, proc macros, or named types have offered for years. The strong typedef example shows the direction clearly, even if queue_injection and full code injection remain in refinement toward a future standard.