The strong typedef problem in C++ is old. Using double to represent both meters and kilograms compiles silently. Type aliases make it worse by being transparent synonyms; using Meters = double creates no new type, just a name. This has been a source of bugs in safety-critical code for decades, and the language has offered no built-in remedy.
The standard workarounds all have the same failure mode: they work for scalars but collapse when the base type has a rich interface.
What the Old Approaches Get Wrong
BOOST_STRONG_TYPEDEF solves the scalar case through a macro that wraps a value and generates comparison and arithmetic operators. It is tolerable for double or int. It fails for anything with methods. If your base type is a domain object with a custom interface, the macro gives you nothing useful.
Jonathan Boccara’s NamedType library goes further with a CRTP-based skill system:
using Meters = NamedType<double, struct MetersTag, Addable, Comparable>;
using Kilograms = NamedType<double, struct KilogramsTag, Addable, Comparable>;
Each “skill” is a mixin that adds specific operators. This is more principled than the macro approach, but you still have to manually enumerate which capabilities you want. There is no way to express “has the same interface as the wrapped type” in any compact form. If the base type gains a new method, the strong type silently does not expose it. The mismatch compounds over time.
The plain template approach is more direct:
template <typename T, typename Tag>
class StrongType {
T value_;
public:
explicit StrongType(T v) : value_(v) {}
T& unwrap() { return value_; }
const T& unwrap() const { return value_; }
// ... manually forward every method you care about
};
You write the forwarding by hand. It is correct and zero-cost, but it does not scale and it is not maintainable under change.
How Other Languages Handle It
Rust’s newtype pattern is explicit and built into the language:
struct Meters(f64);
struct Kilograms(f64);
Meters and Kilograms are fully distinct types at zero cost. The problem is interface forwarding. If f64 had ten methods you needed, you would forward them manually, or reach for a crate like derive_more that uses procedural macros to generate delegating implementations. The mechanism works but it is external to the language.
Haskell has had GeneralizedNewtypeDeriving for a long time:
newtype Meters = Meters Double deriving (Show, Eq, Ord, Num)
Typeclass instances from Double are automatically derived for Meters. This is close to the desired behavior, but it is limited to what typeclass derivation can express. There is no general “copy the entire interface of the inner type” primitive.
TypeScript’s branded types work at the type level only:
type Meters = number & { readonly __brand: 'Meters' };
type Kilograms = number & { readonly __brand: 'Kilograms' };
Effective and zero runtime cost, but limited to scalar-like types where operations are defined on the base type rather than through methods.
C++ has had no equivalent of GeneralizedNewtypeDeriving, and no way to enumerate the public interface of an arbitrary type at compile time in order to generate a complete forwarding wrapper. That is exactly what C++26 reflection enables.
The Mechanism
P2996, accepted into the C++26 working draft at the Wrocław WG21 meeting in November 2024, introduces a value-based reflection model. The ^^ operator produces a std::meta::info value representing any C++ entity. Because std::meta::info is a scalar value type rather than a type-level construct, you can store it in std::vector, pass it to functions, and iterate over it with ordinary loops.
The key API functions are consteval-only:
consteval std::vector<std::meta::info> member_functions_of(std::meta::info r);
consteval std::vector<std::meta::info> nonstatic_data_members_of(std::meta::info r);
consteval std::string_view identifier_of(std::meta::info r);
consteval std::meta::info type_of(std::meta::info r);
With P1306 expansion statements (also accepted for C++26), you can iterate over compile-time sequences in a way that generates one instantiation per element:
template for (constexpr auto mem : std::meta::member_functions_of(^^Item)) {
// generate a forwarding declaration for each public method
}
This is what make_strong_typedef depends on. Given a target type and a base type as std::meta::info values, it enumerates the base’s public member functions and generates corresponding declarations in the target. The result is a completely distinct type with a complete forwarded interface.
The usage from the r/cpp thread demonstrates what this looks like at the call site:
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";
}
// display(Item{"hello", 1}); // compile error: no matching overload
FoodItem and BookItem are unrelated types. Passing a BookItem where a FoodItem is expected is a compile error. Adding a method to Item propagates to all three wrappers automatically on the next build, with no manual update required.
What Is and Is Not in the Standard
The example in the original thread uses queue_injection { ... }, an in-place code injection mechanism from EDG’s experimental implementation. This was not integrated into C++26. The committee chose a two-stage approach instead: write reflection code in one pass, compile the generated output in a second pass. For the make_strong_typedef pattern, this means portable standard C++26 code requires two compilation stages, whereas EDG’s experimental build handles it in one.
The Bloomberg Clang fork is the closest available implementation to the actual standard, and both it and EDG are accessible on Compiler Explorer today. Code injection via queue_injection works in EDG’s experimental mode; portable standard C++26 code takes the two-stage path.
Code injection as a general language feature is targeted for C++29 under P3294. That is where Herb Sutter’s metaclass vision from P0707 eventually lands. P2996 gives us read-only enumeration and splice; P3294 will add the ability to programmatically create new member declarations in-place.
Aggregate initialization for reflection-generated types is also called out as incomplete in the proof-of-concept code. Constructors on types whose definitions are synthesized through reflection require extra handling; this is an area where the tooling and specification are still maturing.
Why Previous Attempts Failed
P2996 succeeded after two previous failed attempts, and the failure mode is instructive. The reflexpr proposal from the C++17 and C++20 cycles used a type-based model: metadata was encoded as types, queries went through type traits, and iteration required recursive template specialization. You cannot pass types as function arguments or store them in std::vector.
// reflexpr style (rejected)
using meta_T = reflexpr(MyStruct);
using members = std::reflect::get_data_members_t<meta_T>; // produces a type
// No iteration without recursive template instantiation
P2996’s value-based model changes the fundamental unit. std::meta::info is a first-class value. Functions can accept and return it, standard algorithms can process it, and the entire std::meta API composes with ordinary C++ code.
// P2996 style
constexpr auto r = ^^MyStruct;
auto members = std::meta::nonstatic_data_members_of(r); // std::vector<info>
for (auto mem : members) { /* normal loop */ }
When the author of make_strong_typedef iterates over public methods to generate forwarding declarations, they are writing data-processing code over a std::vector<std::meta::info>, not fighting recursive template instantiation. That shift in how metadata is represented is what makes the pattern possible at all.
The strong typedef is a focused use case, but it captures the broader change well. C++ has needed a way to say “create a distinct type with the same interface as this existing type” for decades. Rust users reach for newtypes; Haskell users reach for GeneralizedNewtypeDeriving; C++ users have been rewriting forwarding code by hand. Reflection closes that gap, and does so without runtime cost or loss of type safety.