· 6 min read ·

Strong Types on Demand: What C++26 Reflection Actually Enables for Opaque Typedefs

Source: isocpp

The problem is old and well-understood. You have a type called Item, representing a product in your catalog. You want FoodItem, BookItem, and MovieItem to behave exactly like Item in every method call, but be completely incompatible with each other at the type system level. Pass a BookItem where a FoodItem is expected and the compiler should refuse, loudly, at compile time.

In most languages, this is called a newtype or an opaque typedef. In C++, it has historically required either a lot of boilerplate or a dependency on a library that itself generates a lot of code behind the scenes. C++26 reflection, as demonstrated in a recent thread on r/cpp, changes that. With consteval blocks and the ^^ reflection operator, you can write a single make_strong_typedef function and have it automatically generate fully distinct wrapper types by inspecting the public interface of your source struct.

This is worth unpacking carefully, because the capability is real and the implications run deeper than the proof-of-concept code suggests.

Why using and typedef Never Solved This

The obvious first instinct is to reach for a type alias:

using FoodItem = Item;
using BookItem = Item;

This creates no new types. FoodItem and BookItem are just another name for Item. Overload resolution treats them identically, template specializations cannot distinguish them, and passing a BookItem to display(FoodItem&) compiles without a warning. The alias is transparent to the type system in every way that matters for safety.

The correct approach has always been wrapping, not aliasing. You define a new struct that stores an Item internally and exposes the same interface:

struct FoodItem {
    Item inner;
    std::string name() const { return inner.name(); }
    int price() const { return inner.price(); }
    // ... every other public method
};

This works. The type system sees FoodItem and BookItem as genuinely different types. But every time Item gains a new method, every wrapper needs to be updated manually. Add a discount calculation method to Item and you have three places to forget. This is the maintenance problem that libraries like NamedType were built to address, using CRTP and tag types:

using FoodItem = NamedType<Item, struct FoodItemTag>;
using BookItem = NamedType<Item, struct BookItemTag>;

NamedType solves the maintenance problem for primitive-like types where the underlying type has no interesting public interface. But for richer types, the CRTP approach requires you to explicitly mix in each behavior you want to expose, using named “skill” types. It does not automatically forward an arbitrary interface. The wrapping is still manual, just at a different layer of abstraction.

What Reflection Enables Here

C++26 reflection, specified in proposal P2996, introduces compile-time introspection through the ^^ operator. Applying ^^ to a type yields a std::meta::info value representing that type, which can be queried at compile time for its members, bases, access specifiers, and more. Critically, this happens inside consteval contexts, where the compiler evaluates expressions fully before generating any object code.

The mechanism behind the strong typedef proof-of-concept is this: given a reflection of Item, you can enumerate every public member function, generate a forwarding wrapper for each one, and inject those declarations into a new struct. The call site looks like:

struct FoodItem;
struct BookItem;
struct MovieItem;

consteval {
    make_strong_typedef(^^FoodItem, ^^Item);
    make_strong_typedef(^^BookItem, ^^Item);
    make_strong_typedef(^^MovieItem, ^^Item);
}

Inside make_strong_typedef, the implementation iterates over the public members of the source type using std::meta::members_of, filters for callable members, and synthesizes equivalent declarations in the destination type. The destination type stores an Item internally and routes each call through it.

The resulting types are fully distinct. FoodItem and BookItem share the same interface but the compiler treats them as unrelated types, which is exactly what strong typing requires:

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

// display(Item{"hello", 1}); -- compile error: no matching function

Add a method to Item and all three wrappers pick it up automatically on the next build, without touching the wrapper declarations.

The queue_injection Caveat

The author notes an important constraint: the example uses queue_injection { ... } from the EDG experimental reflection compiler, which was not incorporated into the C++26 standard. Without it, generating types that are then used within the same translation unit requires a two-stage build, with the first stage performing codegen and the second stage compiling the generated output.

This is not a fatal limitation for production use. Code generation pipelines are common in large C++ projects. CMake and other build systems can orchestrate two-stage builds without too much friction. But it does mean the syntax shown in the proof-of-concept is not exactly what you would write today against the standard C++26 reflection facilities.

The standard approach is closer to what experimental compilers like the P2996 reference implementation on Compiler Explorer expose: consteval functions that return values and splicing through [:...:] syntax to inject reflected entities into new contexts. The ergonomics are rougher, but the underlying capability is the same.

Comparison with Other Languages

Rust’s newtype pattern handles the same problem with considerably less machinery. You write:

struct FoodItem(Item);
struct BookItem(Item);

These are distinct types with a single tuple field. They do not automatically expose any of Item’s methods. To forward a method, you implement a trait or write a forwarding impl block explicitly. The language does not generate forwarding for you, which keeps the mechanism simple but means you still write some boilerplate per type.

Where Rust pulls ahead is in the derive mechanism. If Item derives Debug, Clone, and PartialEq, the newtype can derive the same traits in one line without any code generation framework:

#[derive(Debug, Clone, PartialEq)]
struct FoodItem(Item);

Haskell’s newtype goes further. It is semantically guaranteed to produce a zero-overhead wrapper at the type system level, and deriving is pervasive. The GeneralizedNewtypeDeriving extension allows a newtype to automatically inherit all instances of its underlying type, which is essentially what the C++26 reflection approach generates, but within the language itself rather than a library-implemented reflection mechanism.

C++‘s approach, once reflection stabilizes, will be more powerful in one specific dimension: it works on arbitrary member function sets, not just typeclass or trait implementations. Any public interface can be wrapped and forwarded, including methods with complex signatures, overload sets, and non-trivial return types, without any modifications to the source type or the wrapper declaration.

What This Means for Real Codebases

The strong typedef use case is a clean demonstration of what compile-time reflection unlocks, but the broader pattern is more general. Any scenario where you want one type to structurally conform to another type’s interface, without inheritance, is approachable through this mechanism.

Consider a logging wrapper that records every method call on an underlying service object:

struct LoggedDatabase;

consteval {
    make_logging_wrapper(^^LoggedDatabase, ^^Database);
}

The reflection-generated wrapper intercepts each call, logs it, and forwards to the inner Database instance. This is the proxy pattern, but generated rather than handwritten. Any new method added to Database automatically appears in LoggedDatabase on the next build.

Similarly, mocking frameworks for testing often need to generate wrapper types that record calls and return configured values. The current state of the art involves either heavy macros (gMock’s MOCK_METHOD) or separate code generation tools. Reflection provides the introspection primitives to build this kind of infrastructure as a library, in standard C++.

Practical Status

P2996 was voted into C++26 at the November 2024 WG21 meeting in Wroclaw. The core introspection facilities are in the standard. Compiler support is early; Clang and GCC both have experimental reflection branches, and EDG’s implementation has been the primary testbed for the more aggressive code injection features.

The proof-of-concept in the r/cpp thread is ahead of what the released standard fully enables today, because of the queue_injection dependency. The two-build workaround is functional but adds friction. As compiler implementations mature and the injection facilities either land in a future standard or become available as extensions, the ergonomics will improve.

For teams wanting to use strong typedefs now, NamedType and similar libraries remain the practical choice. For teams willing to target C++26 with experimental compiler features, the reflection-based approach is viable with the two-stage build caveat.

The deeper point is that P2996 gives the language the primitives to implement strong typedefs, proxy types, mocking frameworks, serialization libraries, and ORM-style type mappings as ordinary C++ libraries, without macros, without external code generators, and without modifications to the types being wrapped. That is a meaningful shift in what library authors can offer, and the strong typedef example makes the capability concrete in a way that abstract descriptions of reflection rarely do.

Was this interesting?