· 5 min read ·

C++26 Reflection, Strong Typedefs, and the Code Injection Gap

Source: isocpp

The strong typedef problem in C++ is old and tedious. typedef double Meters; typedef double Seconds; creates no actual type distinction. You can pass Seconds where Meters is expected and the compiler will not object. Multiply them together and you get something dimensionally nonsensical, silently. The type system you thought was protecting you is doing nothing at all.

The traditional remedies range from verbose to fragile. You write a wrapper struct with an explicit constructor, then discover you’ve lost every arithmetic operator, comparison, std::hash support, and stream output you relied on. You add them back manually. By the time you’re done, you have more boilerplate than domain logic, and the next type requires the same effort. Jonathan Boccara’s NamedType library formalized a cleaner approach around CRTP skill policies, where you opt into specific behaviors by composing traits:

#include <NamedType/named_type.hpp>

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

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

This is genuinely good. You name every operation you want. Passing Seconds where Meters is expected now fails to compile. The .get() accessor retrieves the underlying value when you need it. But you still have to list each skill explicitly, and for a type that wraps a class with twenty methods, you’re writing substantial glue code.

A proof-of-concept making rounds on r/cpp shows what reflection could do here. With a few consteval lines, you get fully distinct wrapper types that inherit all public member functions from a base:

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

FoodItem and BookItem compile as completely separate types. No implicit conversion between them. All Item’s public methods are available on each. The author notes upfront that they’re using queue_injection { ... } from the EDG experimental reflection compiler, and that this specific mechanism was not integrated into the C++26 standard. That caveat is doing a lot of work, and it is worth understanding exactly what it means.

What P2996 Actually Shipped

C++26 static reflection is real. P2996 was voted into the working draft at the WG21 meeting in Wrocław in November 2024, which was a significant milestone after years of prior reflection proposals failing to reach consensus. The feature gives you a ^ reflection operator that produces a std::meta::info value representing any C++ entity, and a [:info:] splice operator that reintroduces that entity into a code position. The std::meta namespace provides a comprehensive set of consteval metafunctions:

consteval void inspect_type(std::meta::info t) {
    for (auto mem : std::meta::member_functions_of(t)) {
        if (std::meta::is_public(mem)) {
            auto fn_name = std::meta::name_of(mem);
            // fn_name is available at compile time
        }
    }
}

You can iterate over non-static data members, member functions, base classes, enumerators, and template parameters. You can query names, types, access specifiers, and whether something is a special member function. You can splice reflected types and function pointers back into source. std::meta::define_aggregate lets you synthesize fresh struct types by supplying a list of member descriptors at compile time.

This is the introspection half of reflection, and it is powerful. You can write compile-time code that walks any type’s structure and makes decisions based on it. Enum-to-string conversion, struct serialization, static assertions about class layouts, debug printers for arbitrary aggregates, all of these become straightforward.

What Did Not Ship: Code Injection

The proof-of-concept’s make_strong_typedef function works by iterating over Item’s member functions and injecting forwarding wrappers into FoodItem’s class body. That second step, injecting new member declarations into a class that already exists in source, is code injection, and it is not in C++26.

queue_injection comes from experimental EDG compiler builds that implement features beyond the accepted standard, available for experimentation on Compiler Explorer under their reflection-enabled builds. The injection mechanism was separated from P2996 into P3294, “Code Injection with Token Sequences”, and as of early 2026 it is targeted for C++29.

The exclusion was not accidental. Three competing design models for how injection should work, disagreement on how injected names interact with existing name lookup rules, and real concern from compiler vendors about implementation complexity all contributed. Reflection that reads program structure and reflection that writes new program structure turn out to have very different implementation requirements, and WG21 chose to ship the former cleanly rather than delay both.

The consequence for the strong typedef use case is concrete. C++26 reflection lets you inspect every public member function of Item at compile time. It does not let you retroactively push forwarding wrappers into FoodItem’s member function table using standard mechanisms. The clean two-line make_strong_typedef call only works on compilers running experimental extensions.

What You Can Do in C++26 Today

The picture is not hopeless. std::meta::define_aggregate synthesizes a completely new struct type from a list of member descriptors, which means for simple value-wrapper cases, you can generate a fresh type wholesale rather than modifying a pre-declared one. For types that wrap arithmetic values, quantities, or POD-like structures, this path is viable.

For class wrappers that need to forward richer method sets, you can use reflection to build type lists and drive template machinery that generates non-member forwarding functions or friend declarations. It is more verbose than the proof-of-concept, but it is portable. Barry Revzin’s work on the Clang P2996 branch includes examples of this kind of pattern.

Reflection also changes the diagnostic story for existing strong typedef patterns. You can write static_assert-driven introspection that verifies your manually-composed NamedType wrapper actually exposes all the operations the underlying type defines, catching mismatches at compile time without needing to generate the wrappers from scratch.

The Rust Comparison

Rust handles this through the newtype pattern: a tuple struct with one field:

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

The types are fully distinct with zero runtime overhead. No operations are implicitly inherited. Every trait, arithmetic operator, and interface must be explicitly implemented. This is the same philosophy as NamedType’s opt-in skills, but enforced by the language rather than a library convention.

For common traits like Debug, Clone, PartialEq, and Hash, #[derive] handles the boilerplate. For richer forwarding, crates like nutype add sanitization and validation on top of the newtype pattern with a derive macro API. Rust never promises automatic forwarding because that would defeat the purpose. You are supposed to think about each operation.

C++26’s eventual injection-enabled reflection will offer something Rust does not: automated complete forwarding as a deliberate opt-in, with the ability to selectively exclude operations you do not want. Whether that is better design than Rust’s explicit-per-trait approach depends on how much you trust the developer to know which forwarded operations are semantically safe for a given domain type.

Where This Leaves Things

The r/cpp proof-of-concept is a genuine preview of where C++ is headed, not a description of where it is. The underlying premise, that reflection should eliminate the strong typedef boilerplate entirely, is sound. P2996 provides the vocabulary for it. P3294 provides the mechanism for completing it. The two-build-stage workaround the author mentions for non-injection environments is real but uncomfortable for production use.

For practical work today, NamedType remains the most ergonomic portable option. std::meta::define_aggregate covers value-wrapper cases cleanly in C++26. And for projects running the experimental EDG or Clang reflection builds, the full proof-of-concept pattern works and gives a clear picture of what standardized injection will unlock.

C++26 reflection is a meaningful addition to the language. The strong typedef generation pattern shows both why it matters and where one more standardization cycle is still needed.

Was this interesting?