· 6 min read ·

Making Strong Types Native: What C++26 Reflection Changes About the Typedef Problem

Source: isocpp

The Type Alias Problem

C++ has had a type alias problem for as long as it has had type aliases. typedef int UserId and typedef int ProductId produce two names that refer to the same underlying type. Pass a ProductId where a UserId is expected and the compiler accepts it without complaint. This is occasionally convenient but mostly a footgun, especially in codebases where integer-typed identifiers proliferate.

using aliases in C++11 changed the syntax, not the behavior. A using UserId = int is still transparent to the type system. The compiler sees through it to the underlying type at every point where type identity matters.

The distinction becomes serious in interfaces that carry semantic weight: distances in meters versus feet, monetary amounts in different currencies, user IDs versus session IDs versus product IDs. The type system can enforce these distinctions, but only if the types are genuinely distinct.

What Developers Have Done About It

The community has accumulated several approaches over the years, none of them fully satisfying.

BOOST_STRONG_TYPEDEF is the oldest automated solution, generating a struct that wraps an underlying type and explicitly re-exposes selected operations. It works through macros, which means no visibility into what is happening and limited composability.

Jonathan Boccara’s NamedType library improves on this with a template-based approach. You declare which “skills” a named type should support, choosing from a menu of pre-written policy classes that forward operations from the underlying type.

using Meters = NamedType<double, struct MetersTag, Addable, Comparable>;
using Feet   = NamedType<double, struct FeetTag,   Addable, Comparable>;

This is readable and explicit, but it requires listing every capability you want, it does not handle arbitrary struct member functions cleanly, and the underlying mechanism is still template machinery that approximates what you want rather than expressing it directly.

The third common approach is hand-writing a wrapper struct, forwarding every constructor and every relevant method. Tedious, error-prone when the wrapped type changes, and duplicated across every strong typedef in the codebase.

C++26 Reflection

P2996, “Reflection for C++26,” was voted into the standard after years of work by Wyatt Childers, Peter Dimov, Dan Katz, Barry Revzin, Andrew Sutton, Faisal Vali, and Daveed Vandevoorde. The core mechanism is the ^^ operator, which produces a std::meta::info value representing a declaration or type. This value can be passed to consteval functions that inspect the type’s structure and generate new code from it.

The simplest uses of the operator look like this:

constexpr auto refl  = ^^int;               // reflects on the type int
constexpr auto refl2 = ^^std::vector<int>;  // reflects on a template instantiation

More interesting is what you can do with that reflection value: call std::meta::members_of() to enumerate member functions, inspect their parameter types and return types, and inject new declarations synthesized from that information.

Strong Typedefs via Reflection

The proof of concept in this r/cpp thread applies that mechanism directly to the strong typedef problem. Given a source struct and a forward-declared target struct, make_strong_typedef uses reflection to enumerate the source’s public members and inject forwarding wrappers into the target:

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);
}

void display(FoodItem &i) {
    std::cout << "Food: " << i.name() << ", " << i.price() << "\n";
}
void display(BookItem &i) {
    std::cout << "Book: " << i.name() << ", " << i.price() << "\n";
}

int main() {
    FoodItem fi("apple", 10);
    BookItem bi("the art of war", 20);
    display(fi);
    display(bi);
    // display(Item{"hello", 1}); // won't compile: no display(Item&) overload
}

After the consteval block, FoodItem and BookItem are fully realized types. They expose name() and price(), carry constructors forwarded from Item’s constructors, store a private inner Item value, and have no implicit conversion to each other or to Item. They are distinct types in every sense the type system cares about.

The implementation uses queue_injection { ... }, an EDG experimental extension that allows inline code injection within a single translation unit. This feature was not merged into the C++26 standard proper. Without it, achieving the same result requires separating the reflection phase from the injection phase, typically across a two-stage build: one pass generates the code, a second pass compiles it. The functionality is equivalent; the ergonomics differ.

Limitations of the Current Proof of Concept

Aggregate initialization is not fully handled. FoodItem fi = {"apple", 10} may or may not work depending on how the injected constructors are structured. The interaction between explicit constructors, converting constructors, and the forwarding logic has edge cases that a production-ready library would need to address carefully.

Operator overloading requires a policy decision the inner type’s interface cannot answer automatically. If Item supported operator+, should FoodItem forward it? For a type like Meters wrapping a double, yes. For UserId wrapping an int, probably not, even though the underlying type supports arithmetic. The reflection mechanism provides all the tools needed to make this configurable, but the make_strong_typedef interface has to expose the control point.

Member variable access is a further consideration. If Item has public data members rather than only accessor methods, forwarding them through a stored inner value is slightly more involved than forwarding method calls.

None of these are limitations of reflection itself. They are the expected rough edges of a proof of concept, and each one is solvable with additional implementation work inside make_strong_typedef.

How Other Languages Handle This

Rust’s newtype pattern is the most commonly cited comparison:

struct Meters(f64);
struct Feet(f64);

impl Meters {
    fn value(&self) -> f64 { self.0 }
}

Rust makes you opt into every operation explicitly. Meters has no arithmetic operators, no Display, and no coercion to f64 unless you implement them. The language provides Deref as a deliberate escape hatch when you want to forward all operations from the inner type, but it is not automatic. Zero runtime cost, complete type safety, explicit control over what is exposed.

Haskell’s newtype keyword is more ergonomic for common cases:

newtype UserId    = UserId    Int deriving (Eq, Ord, Show)
newtype ProductId = ProductId Int deriving (Eq, Ord, Show)

The deriving clause generates instances for standard typeclasses. The compiler guarantees identical runtime representation to the inner type. The syntax is minimal, the semantics are precise, and the cost is zero.

D’s alias and mixin facilities offer programmatic type manipulation closer in spirit to what C++26 reflection is doing. D’s compile-time function evaluation and string mixins have long allowed generating struct members from computed strings, though the approach is less structured than P2996’s typed reflection API.

The C++ approach differs from all three in its orientation toward generality. P2996 provides a meta-programming mechanism powerful enough to implement newtype creation as a library, among many other things. That generality is the point: the same tool that implements make_strong_typedef can implement automatic serialization, FFI wrapper generation, enum-to-string conversion, and any other pattern that currently requires macros or external codegen.

The Broader Shift

C++ meta-programming before P2996 worked through template specialization and SFINAE. It is Turing-complete but indirect, expressing programs about types by writing templates that pattern-match on type structure through substitution and overload resolution. It works, but the gap between what you want to express and what you have to write is substantial.

Reflection replaces that indirection with direct inspection. You ask for a type’s members, you get a range to iterate, you construct new declarations from what you find. The strong typedef example from the r/cpp thread demonstrates this at small scale, but the same capability applies to the broader set of domains where C++ developers have reached for macros, external code generators, or elaborate template machinery.

Libraries like reflect-cpp have anticipated this moment, providing reflection-like capabilities on top of existing language features while waiting for P2996 to land. Once compiler support matures past the current EDG experimental stage and into stable Clang and GCC releases, that category of library gets both simpler and more powerful at the same time.

The proof of concept is a proof of concept. The interesting part is what it demonstrates about the design space that opens up once the reflection primitives are in place.

Was this interesting?