· 7 min read ·

Writing the Wrapper Generator C++ Never Had: Strong Typedefs via C++26 Reflection

Source: isocpp

The strong typedef problem in C++ is not subtle. using Meters = double; and using Seconds = double; are both double, and the compiler treats them identically, accepts one where you meant the other, and says nothing. This is not a niche concern. Dimensional analysis bugs, identifier confusion, and accidental implicit conversions are among the most common sources of latent defects in systems code. The fix has been obvious since at least the 1990s: the language should support declaring that two types, though sharing a representation, are distinct for the purposes of overload resolution and type checking.

WG21 has considered this many times. Bjarne Stroustrup co-authored N3741 in 2013, which proposed using Meters = distinct double;. Later papers including N4041 and N4107 proposed variations on opaque typedef syntax. None made it into a standard. The committee kept concluding that getting the semantics right, especially around which operations should be inherited, how constructors should work, and how standard library templates should interact, was too hard to specify cleanly as a language feature.

The community responded by building workarounds.

Jonathan Boccara’s NamedType library is the most ergonomic of these. It uses phantom tag types and CRTP mixin “skills” to let you opt into specific operations:

using Meters = fluent::NamedType<double, struct MetersTag,
    fluent::Addable, fluent::Comparable>;

using Seconds = fluent::NamedType<double, struct SecondsTag,
    fluent::Divisible>;

Meters and Seconds are now distinct types. Passing a Seconds where Meters is expected is a compile error. The skill system means you pay only for the operations you explicitly allow. Jonathan Müller’s type_safe takes a similar approach. BOOST_STRONG_TYPEDEF exists but is intentionally permissive, preserving most implicit conversion behaviour and therefore offering weaker guarantees.

These libraries work. They are header-only, zero-overhead at runtime, and well-maintained. The cost is the CRTP machinery: every new type requires explicitly listing which operations it supports, and the underlying implementation requires understanding CRTP mixin patterns to extend. It is a reasonable solution to a problem that should have a language answer, and C++26 reflection is starting to provide it.

What Reflection Enables

P2996, “Reflection for C++26,” introduces the ^^ operator to produce compile-time metadata about program entities, and the [: :] splice operator to use that metadata as a concrete syntactic element. The type of all reflected entities is std::meta::info, an opaque handle usable only in consteval contexts.

The relevant piece for strong typedefs is std::meta::define_class. Given a list of member descriptors, it constructs a complete struct type at compile time and returns a std::meta::info representing it. A data member descriptor is created with std::meta::data_member_spec:

consteval auto make_strong_typedef(std::meta::info underlying)
    -> std::meta::info
{
    auto storage = std::meta::data_member_spec(underlying, {.name = "value_"});
    return std::meta::define_class(^^struct, {storage});
}

using Kilometers = [:make_strong_typedef(^^double):];
using Miles      = [:make_strong_typedef(^^double):];

Kilometers and Miles are now fully distinct types, not aliases. The splice [:make_strong_typedef(^^double):] takes the std::meta::info returned by define_class and inserts it as a type-id into the using declaration. The compiler sees two separate struct definitions, each containing a single double member. Their layouts are identical; their types are not, and there is no runtime overhead.

The r/cpp thread that inspired this post goes further and wraps an existing Item struct by reflecting over its public member functions and forwarding them into the generated type. That is where things get more involved.

Forwarding Methods: The Harder Part

Generating a struct with a stored value is straightforward. Forwarding all public member functions of the source type into the wrapper is what makes this approach equivalent to the pre-existing library solutions, and it requires a different mechanism.

P3294, “Code Injection for C++26,” provides std::meta::queue_injection. This function accepts a compile-time token sequence and schedules it for injection into the current declaration context. Combined with iteration over std::meta::members_of, you can loop over the source type’s methods and inject forwarding wrappers:

consteval void make_strong_typedef(std::meta::info new_type, std::meta::info source) {
    // Store the wrapped value
    queue_injection(data_member_spec(source, {.name = "value_"}));

    // Forward each public member function
    for (auto mem : std::meta::members_of(source)) {
        if (!std::meta::is_function(mem) || !std::meta::is_public(mem)) continue;
        // Inject a forwarding wrapper for mem
    }
}

struct FoodItem;
struct BookItem;

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

The consteval { } block at namespace scope, proposed in P3289, executes compile-time code as a top-level statement. The author of the original post uses queue_injection { }, which is the EDG experimental syntax for this construct; it was not finalized in the C++26 standard in that form, which is why the post notes that without it you would need a two-stage build to achieve the same result.

The injection API represents the roughest edge in the current design. P3294 was not fully merged into C++26 in the same form as P2996. The code-injection mechanism for injecting function member bodies is still being stabilized across the proposal and the EDG implementation. define_class and data_member_spec for pure data structures are well-specified; injecting functions with full bodies is where the experimental status actually bites.

Comparison with Other Languages

Haskell’s newtype has been solving this problem since 1990. The syntax is minimal:

newtype Meters  = Meters  Double
newtype Seconds = Seconds Double

GHC’s GeneralizedNewtypeDeriving extension lets you derive any type class instance that the underlying type has, which is equivalent to NamedType’s “inherit all operations” mode, and it is one word: deriving. The C++26 reflection approach, once make_strong_typedef is written, reaches a similar ergonomic level for users of the utility, while giving the author of that utility full control over what gets forwarded and how.

Rust’s newtype pattern uses tuple structs:

struct Meters(f64);
struct Seconds(f64);

No operators are inherited by default. You implement std::ops::Add for Meters explicitly, or use the derive_more crate to derive forwarding implementations automatically via proc macros. The orphan rule means you cannot implement standard traits for a newtype wrapping a foreign type unless the trait is local, which creates friction for some patterns. The C++26 approach sidesteps this entirely because there is no orphan rule; injected methods live inside the generated struct and operate freely on its internals.

Swift does not have a newtype equivalent. typealias creates transparent aliases with no type safety, and the only alternative is a full struct definition with manual forwarding. That is exactly where C++ was before the library solutions appeared, and where Swift remains today despite several Swift Evolution pitches for nominal type aliases.

A rough comparison of the design space:

LanguageMechanismOperator ForwardingZero Cost
Haskellnewtype keywordderiving / GeneralizedNewtypeDerivingGuaranteed by spec
RustTuple structimpl Trait / derive_moreGuaranteed by spec
SwiftFull structManualYes
C++ pre-26NamedType / type_safeCRTP skill mixinsYes
C++26define_class + reflectionInjected via queue_injectionYes

The C++26 approach is not as terse as Haskell’s newtype at the call site, but it is close in terms of what a user of the utility has to write. The difference is that instead of a language keyword, you are using a utility function someone wrote in standard C++.

Current State

The EDG experimental compiler on Compiler Explorer is the primary way to test P2996 code today. Search for the EDG experimental entry in the compiler list and include <meta> to access the reflection API. A Clang branch tracking the proposal exists in the llvm/llvm-project repository and is being actively developed by Wyatt Childers and collaborators. GCC has exploratory work. MSVC has not announced a branch.

For the strong typedef use case, the define_class plus data_member_spec path works in the EDG experimental build for pure data wrappers. The method-forwarding case using queue_injection requires the EDG-specific injection syntax and should be treated as a proof-of-concept. Aggregate initialization with the generated types is noted in the original post as incomplete.

This pattern is not production-ready in any shipping compiler. What it demonstrates is that the language infrastructure now exists to express this in standard C++, without macros or external libraries, and with full control over exactly which operations get forwarded. When compilers ship C++26 support, NamedType and type_safe may eventually become unnecessary for this use case.

What Remains

A few things are still unresolved. The queue_injection API for function bodies needs to stabilize across compilers. Aggregate initialization for define_class-generated types needs to be worked through. The interaction with concepts, standard library containers, and std::hash specializations requires careful handling; NamedType has a Hashable skill precisely because getting this right takes thought, and the reflection-generated type needs equivalent treatment.

These are solvable problems, and the reflection machinery makes them approachable in a way that template metaprogramming never quite managed. The strong typedef problem has been waiting for a language answer for three decades. C++26 is not the tidy using Meters = distinct double; syntax Stroustrup proposed in 2013, but the infrastructure it provides lets you build that yourself in a way that is composable, inspectable, and extensible. That is a more honest fit for how C++ has always worked.

Was this interesting?