Professional Documents
Culture Documents
Reflection for C++26
Reflection for C++26
Reflection for C++26
Contents
1 Revision History
2 Introduction
2.1 Notable Additions to P1240
2.2 Why a single opaque reflection type?
2.3 Implementation Status
3 Examples
3.1 Back-And-Forth
3.2 Selecting Members
3.3 List of Types to List of Sizes
3.4 Implementing make_integer_sequence
3.5 Getting Class Layout
3.6 Enum to String
3.7 Parsing Command-Line Options
3.8 A Simple Tuple Type
3.9 A Simple Variant Type
3.10 Struct to Struct of Arrays
3.11 Parsing Command-Line Options II
3.12 A Universal Formatter
3.13 Implementing member-wise hash_append
3.14 Converting a Struct to a Tuple
3.15 Implementing tuple_cat
3.16 Named Tuple
3.17 Compile-Time Ticket Counter
3.18 Emulating typeful reflection
4 Proposed Features
4.1 The Reflection Operator (^)
4.1.1 Syntax discussion
4.2 Splicers ([:…:])
4.2.1 Addressed Splicing
4.2.2 Limitations
4.2.2.1 Splicing reflections of constructors
4.2.2.2 Splicing namespaces in namespace definitions
4.2.2.3 Splicing namespaces in using-directives and using-enum-declarators
https://isocpp.org/files/papers/P2996R4.html 1/81
7/2/24, 2:29 PM Reflection for C++26
4.2.2.4 Splicing concepts in declarations of template parameters
4.2.2.5 Splicing class members as designators in designated-initializer-lists
4.2.3 Range Splicers
4.2.4 Syntax discussion
4.3 std::meta::info
4.3.1 Comparing reflections
4.3.2 Linkage of reflections and templates specialized by reflections
4.3.3 The associated std::meta namespace
4.4 Metafunctions
4.4.1 Constant evaluation order
4.4.2 Error-Handling in Reflection
4.4.3 Range-Based Metafunctions
4.4.4 Handling Aliases
4.4.5 Reflecting source text
4.4.6 Freestanding implementations
4.4.7 Synopsis
4.4.8 name_of, display_name_of, source_location_of
4.4.9 type_of, parent_of, dealias
4.4.10 object_of, value_of
4.4.11 template_of, template_arguments_of
4.4.12 members_of, static_data_members_of, nonstatic_data_members_of, bases_of, enumerators_of,
subobjects_of
4.4.13 Member Access Reflection
4.4.14 substitute
4.4.15 reflect_invoke
4.4.16 reflect_value, reflect_object, reflect_function
4.4.17 extract<T>
4.4.18 test_trait
4.4.19 data_member_spec, define_class
4.4.20 Data Layout Reflection
4.4.21 Other Type Traits
4.5 ODR Concerns
5 Proposed Wording
5.1 Language
5.2 [lex.phases] Phases of translation
5.4 [lex.pptoken] Preprocessing tokens
5.12 [lex.operators] Operators and punctuators
6.3 [basic.def.odr] One-definition rule
6.5.4 [basic.lookup.argdep] Argument-dependent name lookup
6.5.5.1 [basic.lookup.qual.general] General
[basic.link] Program and Linkage
6.8.1 [basic.types.general] General
6.8.2 [basic.fundamental] Fundamental types
7.5 [expr.prim] Primary expressions
7.5.4.3 [expr.prim.id.qual] Qualified names
7.5.8* [expr.prim.splice] Expression splicing
7.6.1.1 [expr.post.general] General
[expr.ref] Class member access
7.6.2.1 [expr.unary.general] General
7.6.2.10* [expr.reflect] The reflection operator
7.6.10 [expr.eq] Equality Operators
7.7 [expr.const] Constant Expressions
9.2.4 [dcl.typedef] The typedef specifier
9.2.9.3 [dcl.type.simple] Simple type specifiers
9.2.9* [dcl.type.splice] Type splicing
9.4.1 [dcl.init.general] Initializers (General)
9.3.4.6 [dcl.fct] Functions
https://isocpp.org/files/papers/P2996R4.html 2/81
7/2/24, 2:29 PM Reflection for C++26
9.5.3 [dcl.fct.def.delete] Deleted definitions
9.7.2 [enum.udecl] The using enum declaration
9.8.4 [namespace.udir] Using namespace directive
9.12.1 [dcl.attr.grammar] Attribute syntax and semantics
12.5 [over.built] Built-in operators
13.2 [temp.param] Template parameters
13.3 [temp.names] Names of template specializations
13.4.1 [temp.arg.general] General
13.4.2 [temp.arg.type] Template type arguments
13.4.3 [temp.arg.nontype] Template non-type arguments
13.4.4 [temp.arg.template] Template template arguments
13.6 [temp.type] Type equivalence
13.7.9 [temp.concept] Concept definitions
13.8.3.3 [temp.dep.expr] Type-dependent expressions
13.8.3.4 [temp.dep.constexpr] Value-dependent expressions
5.2 Library
5.2.1 16.4.5.2.1 [namespace.std] Namespace std
5.2.2 21.3.3 [meta.type.synop] Header <type_traits> synopsis
5.2.3 21.3.5.2 [meta.unary.cat] Primary type categories
[meta.synop] Header <meta> synopsis
[meta.reflection.names] Reflection names and locations
[meta.reflection.queries] Reflection queries
[meta.reflection.member.queries], Reflection member queries
5.2.4 [meta.reflection.member.access], Reflection member access queries
[meta.reflection.layout] Reflection layout queries
[meta.reflection.extract] Value extraction
[meta.reflection.substitute] Reflection substitution
[meta.reflection.result] Expression result reflection
[meta.reflection.define_class] Reflection class definition generation
[meta.reflection.unary] Unary type traits
[meta.reflection.unary.cat] Primary type categories
[meta.reflection.unary.comp] Composite type categories
[meta.reflection.unary.prop] Type properties
[meta.reflection.unary.prop.query] Type property queries
[meta.reflection.rel], Type relations
[meta.reflection.trans], Transformations between types
[meta.reflection.trans.cv], Const-volatile modifications
[meta.reflection.trans.ref], Reference modifications
[meta.reflection.trans.sign], Sign modifications
[meta.reflection.trans.arr], Array modifications
[meta.reflection.trans.ptr], Pointer modifications
[meta.reflection.trans.other], Other transformations
5.3 Feature-Test Macro
6 References
§1 Revision History
Since [P2996R3]:
— the return of reflect_value: separated reflect_result into three functions: reflect_value, reflect_object,
reflect_function
— more strongly specified comparison and linkage rules for reflections of aliases
— changed is_noexcept to apply to a wider class of entities
https://isocpp.org/files/papers/P2996R4.html 3/81
7/2/24, 2:29 PM Reflection for C++26
— reworked the API for reflecting on accessible class members
— renamed test_type and test_types to test_trait
— added missing has_module_linkage metafunction
— clarified difference between a reflection of a variable and its object; added object_of metafunction
— more wording
Since [P2996R2]:
— added is_noexcept and fixed is_explicit to only apply to member functions, not member function templates
— added section on handling text
— added a section discussing ODR concerns
— some updates to examples, including a new examples which add a named tuple and emulate typeful reflection.
— more discussion of syntax, constant evaluation order, aliases, and freestanding.
— adding lots of wording
Since [P2996R0]:
— added links to Compiler Explorer demonstrating just about all of the examples
— respecified synth_struct to define_class
https://isocpp.org/files/papers/P2996R4.html 4/81
7/2/24, 2:29 PM Reflection for C++26
— collapsed entity_ref and pointer_to_member into value_of
§2 Introduction
This is a proposal for a reduced initial set of features to support static reflection in C++. Specifically, we are mostly proposing
a subset of features suggested in [P1240R2]:
— the representation of program elements via constant-expressions producing reflection values — reflections for short —
of an opaque type std::meta::info,
— a reflection operator (prefix ^) that produces a reflection value for its operand construct,
— a number of consteval metafunctions to work with reflections (including deriving other reflections), and
— constructs called splicers to produce grammatical elements from reflections (e.g., [: refl :]).
(Note that this aims at something a little broader than pure “reflection”. We not only want to observe the structure of the
program: We also want to ease generating code that depends on those observations. That combination is sometimes referred
to as “reflective metaprogramming”, but within WG21 discussion the term “reflection” has often been used informally to
refer to the same general idea.)
This proposal is not intended to be the end-game as far as reflection and compile-time metaprogramming are concerned.
Instead, we expect it will be a useful core around which more powerful features will be added incrementally over time. In
particular, we believe that most or all the remaining features explored in P1240R2 and that code injection (along the lines
described in [P2237R0]) are desirable directions to pursue.
Our choice to start with something smaller is primarily motivated by the belief that that improves the chances of these
facilities making it into the language sooner rather than later.
One addition does stand out, however: We have added metafunctions that permit the synthesis of simple struct and union
types. While it is not nearly as powerful as generalized code injection (see [P2237R0]), it can be remarkably effective in
practice.
We believe that doing so would be a mistake with very serious consequences for the future of C++.
Specifically, it would codify the language design into the type system. We know from experience that it has been quasi-
impossible to change the semantics of standard types once they were standardized, and there is no reason to think that such
evolution would become easier in the future. Suppose for example that we had standardized a reflection type
std::meta::variable in C++03 to represent what the standard called “variables” at the time. In C++11, the term “variable”
was extended to include “references”. Such an change would have been difficult to do given that C++ by then likely would
have had plenty of code that depended on a type arrangement around the more restricted definition of “variable”. That
scenario is clearly backward-looking, but there is no reason to believe that similar changes might not be wanted in the future
and we strongly believe that it behooves us to avoid adding undue constraints on the evolution of the language.
— it makes no assumptions about the representation used within the implementation (e.g., it doesn’t advantage one
compiler over another),
— it is trivially extensible (no types need to be added to represent additional language elements and meta-elements as the
language evolves), and
https://isocpp.org/files/papers/P2996R4.html 5/81
7/2/24, 2:29 PM Reflection for C++26
— it allows convenient collections of heterogeneous constructs without having to surface reference semantics (e.g., a
std::vector<std::meta::info> can easily represent a mixed template argument list — containing types and nontypes
— without fear of slicing values).
EDG has an ongoing implementation of this proposal that is currently available on Compiler Explorer (thank you, Matt
Godbolt).
Additionally, Bloomberg has open sourced a fork of Clang which provides a second implementation of this proposal, also
available on Compiler Explorer (again thank you, Matt Godbolt), which can be found here:
https://github.com/bloomberg/clang-p2996.
Neither implementation is complete, but all significant features proposed by this paper have been implemented by at least one
implementation (including namespace and template splicers). Both implementations have their “quirks” and continue to
evolve alongside this paper.
Nearly all of the examples below have links to Compiler Explorer demonstrating them in both EDG and Clang.
The implementations notably lack some of the other proposed language features that dovetail well with reflection; most
notably, expansion statements are absent. A workaround that will be used in the linked implementations of examples is the
following facility:
namespace __impl {
template<auto... vals>
struct replicator_type {
template<typename F>
constexpr void operator>>(F body) const {
(body.template operator()<vals>(), ...);
}
};
template<auto... vals>
replicator_type<vals...> replicator = {};
}
template<typename R>
consteval auto expand(R range) {
std::vector<std::meta::info> args;
for (auto r : range) {
args.push_back(reflect_value(r));
}
return substitute(^__impl::replicator, args);
}
Used like:
https://isocpp.org/files/papers/P2996R4.html 6/81
7/2/24, 2:29 PM Reflection for C++26
With expansion statements With expand workaround
§3 Examples
We start with a number of examples that show off what is possible with the proposed set of features. It is expected that these
are mostly self-explanatory. Read ahead to the next sections for a more systematic description of each element of this
proposal.
A number of our examples here show a few other language features that we hope to progress at the same time. This facility
does not strictly rely on these features, and it is possible to do without them - but it would greatly help the usability
experience if those could be adopted as well:
§ 3.1 Back-And-Forth
Our first example is not meant to be compelling but to show how to go back and forth between the reflection domain and the
grammatical domain:
The typename prefix can be omitted in the same contexts as with dependent qualified names (i.e., in what the standard calls
type-only contexts). For example:
int main() {
S s{0, 0};
s.[:member_number(1):] = 42; // Same as: s.j = 42;
https://isocpp.org/files/papers/P2996R4.html 7/81
7/2/24, 2:29 PM Reflection for C++26
s.[:member_number(5):] = 0; // Error (member_number(5) is not a constant).
}
This example also illustrates that bit fields are not beyond the reach of this proposal.
Note that a “member access splice” like s.[:member_number(1):] is a more direct member access mechanism than the
traditional syntax. It doesn’t involve member name lookup, access checking, or — if the spliced reflection value denotes a
member function — overload resolution.
This proposal includes a number of consteval “metafunctions” that enable the introspection of various language constructs.
Among those metafunctions is std::meta::nonstatic_data_members_of which returns a vector of reflection values that
describe the non-static members of a given type. We could thus rewrite the above example as:
int main() {
S s{0, 0};
s.[:member_number(1):] = 42; // Same as: s.j = 42;
s.[:member_number(5):] = 0; // Error (member_number(5) is not a constant).
}
This proposal specifies that namespace std::meta is associated with the reflection type (std::meta::info); the
std::meta:: qualification can therefore be omitted in the example above.
int main() {
S s{0, 0};
s.[:member_named("j"):] = 42; // Same as: s.j = 42;
s.[:member_named("x"):] = 0; // Error (member_named("x") is not a constant).
}
https://isocpp.org/files/papers/P2996R4.html 8/81
7/2/24, 2:29 PM Reflection for C++26
Compare this to the following type-based approach, which produces the same array sizes:
#include <utility>
#include <vector>
template<typename T>
consteval std::meta::info make_integer_seq_refl(T N) {
std::vector args{^T};
for (T k = 0; k < N; ++k) {
args.push_back(std::meta::reflect_value(k));
}
return substitute(^std::integer_sequence, args);
}
template<typename T, T N>
using make_integer_sequence = [:make_integer_seq_refl<T>(N):];
Note that the memoization implicit in the template substitution process still applies. So having multiple uses of, e.g.,
make_integer_sequence<int, 20> will only involve one evaluation of make_integer_seq_refl<int>(20).
struct member_descriptor
{
std::size_t offset;
std::size_t size;
};
struct X
{
char a;
int b;
double c;
};
https://isocpp.org/files/papers/P2996R4.html 9/81
7/2/24, 2:29 PM Reflection for C++26
/*
where Xd would be std::array<member_descriptor, 3>{{
{ 0, 1 }, { 4, 4 }, { 8, 8 }
}}
*/
return "<unnamed>";
}
return std::nullopt;
}
But we don’t have to use expansion statements - we can also use algorithms. For instance, enum_to_string can also be
implemented this way (this example relies on non-transient constexpr allocation), which also demonstrates choosing a
different algorithm based on the number of enumerators:
https://isocpp.org/files/papers/P2996R4.html 10/81
7/2/24, 2:29 PM Reflection for C++26
};
if (it == enumerators.end()) {
return std::nullopt;
} else {
return it->second;
}
} else {
// if there are lots of enumerators, use a map with find()
constexpr auto enumerators = get_pairs() | std::ranges::to<std::map>();
auto it = enumerators.find(value);
if (it == enumerators.end()) {
return std::nullopt;
} else {
return it->second;
}
}
};
return get_name(value).value_or("<unnamed>");
}
Note that this last version has lower complexity: While the versions using an expansion statement use an expected O(N)
number of comparisons to find the matching entry, a std::map achieves the same with O(log(N)) complexity (where N is the
number of enumerator constants).
Many many variations of these functions are possible and beneficial depending on the needs of the client code. For example:
— the “<unnamed>” case could instead output a valid cast expression like “E(5)”
— a more sophisticated lookup algorithm could be selected at compile time depending on the length of
enumerators_of(^E)
— a compact two-way persistent data structure could be generated to support both enum_to_string and string_to_enum
with a minimal footprint
— etc.
template<typename Opts>
auto parse_options(std::span<std::string_view const> args) -> Opts {
Opts opts;
template for (constexpr auto dm : nonstatic_data_members_of(^Opts)) {
auto it = std::ranges::find_if(args,
[](std::string_view arg){
return arg.starts_with("--") && arg.substr(2) == name_of(dm);
});
if (it == args.end()) {
// no option provided, use default
continue;
} else if (it + 1 == args.end()) {
std::print(stderr, "Option {} is missing a value\n", *it);
std::exit(EXIT_FAILURE);
}
using T = typename[:type_of(dm):];
auto iss = std::ispanstream(it[1]);
if (iss >> opts.[:dm:]; !iss) {
std::print(stderr, "Failed to parse option {} into a {}\n", *it,
display_name_of(^T));
std::exit(EXIT_FAILURE);
}
}
https://isocpp.org/files/papers/P2996R4.html 11/81
7/2/24, 2:29 PM Reflection for C++26
return opts;
}
struct MyOpts {
std::string file_name = "input.txt"; // Option "--file_name <string>"
int count = 1; // Option "--count <int>"
};
#include <meta>
static_assert(is_type(define_class(^storage, {data_member_spec(^Ts)...})));
storage data;
Tuple(): data{} {}
Tuple(Ts const& ...vs): data{ vs... } {}
};
template<typename... Ts>
struct std::tuple_size<Tuple<Ts...>>: public integral_constant<size_t, sizeof...(Ts)>
{};
This example uses a “magic” std::meta::define_class template along with member reflection through the
nonstatic_data_members_of metafunction to implement a std::tuple-like type without the usual complex and costly
template metaprogramming tricks that that involves when these facilities are not available. define_class takes a reflection
for an incomplete class or union plus a vector of non-static data member descriptions, and completes the give class or union
type to have the described members.
https://isocpp.org/files/papers/P2996R4.html 12/81
7/2/24, 2:29 PM Reflection for C++26
union U1 {
int i;
char c;
};
union U2 {
int i;
std::string s;
};
U1 has a trivial destructor, but U2’s destructor is defined as deleted (because std::string has a non-trivial destructor). This is
a problem because we need to define this thing… somehow. However, for the purposes of define_class, there really is only
one reasonable option to choose here:
If we make define_class for a union have this behavior, then we can implement a variant in a much more straightforward
way than in current implementations. This is not a complete implementation of std::variant (and cheats using libstdc++
internals, and also uses Boost.Mp11’s mp_with_index) but should demonstrate the idea:
static_assert(is_type(define_class(^Storage, {
data_member_spec(^Empty, {.name="empty"}),
data_member_spec(^Ts)...
})));
Storage storage_;
int index_ = -1;
public:
constexpr Variant() requires std::is_default_constructible_v<Ts...[0]>
// should this work: storage_{. [: get_nth_field(0) :]{} }
: storage_{.empty={}}
, index_(0)
{
std::construct_at(&storage_.[: get_nth_field(0) :]);
}
https://isocpp.org/files/papers/P2996R4.html 13/81
7/2/24, 2:29 PM Reflection for C++26
constexpr ~Variant() requires (std::is_trivially_destructible_v<Ts> and ...) =
default;
constexpr ~Variant() {
if (index_ != -1) {
with_index([&](auto I){
std::destroy_at(&storage_.[: get_nth_field(I) :]);
});
}
}
Effectively, Variant<T, U> synthesizes a union type Storage which looks like this:
union Storage {
Empty empty;
T unnamed0;
U unnamed1;
The question here is whether we should be should be able to directly initialize members of a defined union using a splicer, as
in:
https://isocpp.org/files/papers/P2996R4.html 14/81
7/2/24, 2:29 PM Reflection for C++26
Arguably, the answer should be yes - this would be consistent with how other accesses work. This is instead proposed in
[P3293R0].
#include <meta>
#include <array>
Example:
struct point {
float x;
float y;
float z;
};
This is the opening example for clap (Rust’s Command Line Argument Parser):
https://isocpp.org/files/papers/P2996R4.html 15/81
7/2/24, 2:29 PM Reflection for C++26
struct Flags {
bool use_short;
bool use_long;
};
// convert a type (all of whose non-static data members are specializations of Option)
// to a type that is just the appropriate members.
// For example, if type is a reflection of the Args presented above, then this
// function would evaluate to a reflection of the type
// struct {
// std::string name;
// int count;
// }
consteval auto spec_to_opts(std::meta::info opts,
std::meta::info spec) -> std::meta::info {
std::vector<std::meta::info> new_members;
for (std::meta::info member : nonstatic_data_members_of(spec)) {
auto type_new = template_arguments_of(type_of(member))[0];
new_members.push_back(data_member_spec(type_new, {.name=name_of(member)}));
}
return define_class(opts, new_members);
}
struct Clap {
template <typename Spec>
auto parse(this Spec const& spec, int argc, char** argv) {
std::vector<std::string_view> cmdline(argv+1, argv+argc)
struct Opts;
static_assert(is_type(spec_to_opts(^Opts, ^Spec)));
Opts opts;
nonstatic_data_members_of(^Opts))) {
auto const& cur = spec.[:sm:];
constexpr auto type = type_of(om);
// no such argument
if (it == cmdline.end()) {
if constexpr (has_template_arguments(type) and template_of(type) ==
^std::optional) {
// the type is optional, so the argument is too
continue;
https://isocpp.org/files/papers/P2996R4.html 16/81
7/2/24, 2:29 PM Reflection for C++26
} else if (cur.initializer) {
// the type isn't optional, but an initializer is provided, use that
opts.[:om:] = *cur.initializer;
continue;
} else {
std::print(stderr, "Missing required option {}\n", name_of(sm));
std::exit(EXIT_FAILURE);
}
} else if (it + 1 == cmdline.end()) {
std::print(stderr, "Option {} for {} is missing a value\n", *it, name_of(sm));
std::exit(EXIT_FAILURE);
}
struct universal_formatter {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
*out++ = '}';
return out;
}
};
struct B { int m0 = 0; };
struct X { int m1 = 1; };
struct Y { int m2 = 2; };
class Z : public X, private Y { int m3 = 3; int m4 = 4; };
https://isocpp.org/files/papers/P2996R4.html 17/81
7/2/24, 2:29 PM Reflection for C++26
template <> struct std::formatter<Z> : universal_formatter { };
int main() {
std::println("{}", Z());
// Z{X{B{.m0=0}, .m1 = 1}, Y{{.m0=0}, .m2 = 2}, .m3 = 3, .m4 = 4}
}
Note that currently, we do not have the ability to access a base class subobject using the t.[: base :] syntax - which means
that the only way to get at the base is to use a cast:
Both have to explicitly specify the const-ness of the type in the cast. The static_cast additionally has to check access. The
C-style cast is one many people find unsavory, though in this case it avoids checking access - but requires writing typename
since this isn’t a type-only context.
template<typename From>
consteval auto get_struct_to_tuple_helper() {
using To = [: type_struct_to_tuple(^From): ];
/*
Alternatively, with Ranges:
args.append_range(
nonstatic_data_members_of(^From)
| std::views::transform(std::meta::reflect_value)
);
*/
Here, type_struct_to_tuple takes a reflection of a type like struct { T t; U const& u; V v; } and returns a reflection
of the type std::tuple<T, U, V>. That gives us the return type. Then, struct_to_tuple_helper is a function template that
does the actual conversion — which it can do by having all the reflections of the members as a non-type template parameter
pack. This is a constexpr function and not a consteval function because in the general case the conversion is a run-time
operation. However, determining the instance of struct_to_tuple_helper that is needed is a compile-time operation and
has to be performed with a consteval function (because the function invokes nonstatic_data_members_of), hence the
separate function template get_struct_to_tuple_helper().
Everything is put together by using substitute to create the instantiation of struct_to_tuple_helper that we need, and a
compile-time reference to that instance is obtained with extract. Thus f is a function reference to the correct specialization
of struct_to_tuple_helper, which we can simply invoke.
On Compiler Explorer (with a different implementation than either of the above): EDG, Clang.
https://isocpp.org/files/papers/P2996R4.html 19/81
7/2/24, 2:29 PM Reflection for C++26
for (T x : args) {
a2.push_back(std::meta::reflect_value(x));
}
template<typename... Tuples>
auto my_tuple_cat(Tuples&&... tuples) {
constexpr typename [: make_indexer({type_tuple_size(type_remove_cvref(^Tuples))...})
:] indexer;
return indexer(std::forward_as_tuple(std::forward<Tuples>(tuples)...));
}
1. Can introduce a pair type so that we can write make_named_tuple<pair<int, "x">, pair<double, "y">>(), or
2. Can just do reflections all the way down so that we can write
make_named_tuple<^int, std::meta::reflect_value("x"),
^double, std::meta::reflect_value("y")>()
We do not currently support splicing string literals, and the pair approach follows the similar pattern already shown with
define_class (given a suitable fixed_string type):
};
(f(tags), ...);
return define_class(type, nsdms);
}
struct R;
static_assert(is_type(make_named_tuple(^R, pair<int, "x">{}, pair<double, "y">{})));
static_assert(type_of(nonstatic_data_members_of(^R)[0]) == ^int);
static_assert(type_of(nonstatic_data_members_of(^R)[1]) == ^double);
https://isocpp.org/files/papers/P2996R4.html 20/81
7/2/24, 2:29 PM Reflection for C++26
int main() {
[[maybe_unused]] auto r = R{.x=1, .y=2.0};
}
Alternatively, can side-step the question of non-type template parameters entirely by keeping everything in the value domain:
struct R;
static_assert(is_type(make_named_tuple(^R, {{^int, "x"}, {^double, "y"}})));
static_assert(type_of(nonstatic_data_members_of(^R)[0]) == ^int);
static_assert(type_of(nonstatic_data_members_of(^R)[1]) == ^double);
int main() {
[[maybe_unused]] auto r = R{.x=1, .y=2.0};
}
On Compiler Explorer: EDG and Clang (the EDG and Clang implementations differ only in Clang having the updated
data_member_spec API that returns an info).
class TU_Ticket {
template<int N> struct Helper;
public:
static consteval int next() {
int k = 0;
https://isocpp.org/files/papers/P2996R4.html 21/81
7/2/24, 2:29 PM Reflection for C++26
§ 3.18 Emulating typeful reflection
Although we believe a single opaque std::meta::info type to be the best and most scalable foundation for reflection, we
acknowledge the desire expressed by SG7 for future support for “typeful reflection”. The following demonstrates one
possible means of assembling a typeful reflection library, in which different classes of reflections are represented by distinct
types, on top of the facilities proposed here.
std::unreachable();
}
We can leverage this machinery to select different function overloads based on the “type” of reflection provided as an
argument.
int main() {
// Classifies any reflection as one of: Type, Function, or Unmatched.
auto enrich = [](std::meta::info r) { return ::enrich<type_t,
template_t>(r); };
https://isocpp.org/files/papers/P2996R4.html 22/81
7/2/24, 2:29 PM Reflection for C++26
PrintKind([:enrich(std::meta::reflect_value(3):]); // "unknown kind"
}
Note that the metatype class can be generalized to wrap values of any literal type, or to wrap multiple values of possibly
different types. This has been used, for instance, to select compile-time overloads based on: whether two integers share the
same parity, the presence or absence of a value in an optional, the type of the value held by a variant or an any, or the
syntactic form of a compile-time string.
Achieving the same in C++23, with the same generality, would require spelling the argument(s) twice: first to obtain a
“classification tag” to use as a template argument, and again to call the function, i.e.,
Printer::PrintKind<classify(^int)>(^int).
// or worse...
fn<classify(Arg1, Arg2, Arg3)>(Arg1, Arg2, Arg3).
§4 Proposed Features
§ 4.1 The Reflection Operator (^)
The reflection operator produces a reflection value from a grammatical construct (its operand):
unary-expression:
…
^ ::
^ namespace-name
^ type-id
^ id-expression
The expression ^:: evaluates to a reflection of the global namespace. When the operand is a namespace-name or type-id, the
resulting value is a reflection of the designated namespace or type.
When the operand is an id-expression, the resulting value is a reflection of the designated entity found by lookup. This might
be any of:
For all other operands, the expression is ill-formed. In a SFINAE context, a failure to substitute the operand of a reflection
operator construct causes that construct to not evaluate to constant.
Earlier revisions of this paper allowed for taking the reflection of any cast-expression that could be evaluated as a constant
expression, as we believed that a constant expression could be internally “represented” by just capturing the value to which it
evaluated. However, the possibility of side effects from constant evaluation (introduced by this very paper) renders this
approach infeasible: even a constant expression would have to be evaluated every time it’s spliced. It was ultimately decided
to defer all support for expression reflection, but we intend to introduce it through a future paper using the syntax ^(expr).
This paper does, however, support reflections of values and of objects (including subobjects). Such reflections arise naturally
when iterating over template arguments.
https://isocpp.org/files/papers/P2996R4.html 23/81
7/2/24, 2:29 PM Reflection for C++26
static_assert(is_value(template_arguments_of(spec)[0]));
static_assert(is_object(template_arguments_of(spec)[1]));
static_assert(!is_variable(template_arguments_of(spec)[1]));
static_assert([:template_arguments_of(spec)[0]:] == 1);
static_assert(&[:template_arguments_of(spec)[1]:] == &p[1]);
Such reflections cannot generally be obtained using the ^-operator, but the std::meta::reflect_value and
std::meta::reflect_object functions make it easy to reflect particular values or objects. The std::meta::value_of
metafunction can also be used to map a reflection of an object to a reflection of its value.
The caret already has a meaning as a binary operator in C++ (“exclusive OR”), but that is clearly not conflicting with a prefix
operator. In C++/CLI (a Microsoft C++ dialect) the caret is also used as a new kind of ptr-operator (9.3.1
[dcl.decl.general]) to declare “handles”. That is also not conflicting with the use of the caret as a unary operator because
C++/CLI uses the usual prefix * operator to dereference handles.
Apple also uses the caret in syntax “blocks” and unfortunately we believe that does conflict with our proposed use of the
caret.
Since the syntax discussions in SG7 landed on the use of the caret, new basic source characters have become available: @, `,
and $. While we have since discussed some alternatives (e.g., @ for lifting, \ and / for “raising” and “lowering”), we have
grown quite fond of the existing syntax.
— [: r :] produces an expression evaluating to the entity represented by r in grammatical contexts that permit
expressions. In type-only contexts (13.8.1 [temp.res.general]/4), [: r :] produces a type (and r must be the reflection
of a type). In contexts that only permit a namespace name, [: r :] produces a namespace (and r must be the reflection
of a namespace or alias thereof).
— typename[: r :] produces a simple-type-specifier corresponding to the type represented by r.
— template[: r :] produces a template-name corresponding to the template represented by r.
— [:r:]:: produces a nested-name-specifier corresponding to the namespace, enumeration type, or class type represented
by r.
The operand of a splicer is implicitly converted to a std::meta::info prvalue (i.e., if the operand expression has a class type
that with a conversion function to convert to std::meta::info, splicing can still work).
Attempting to splice a reflection value that does not meet the requirement of the splice is ill-formed. For example:
https://isocpp.org/files/papers/P2996R4.html 24/81
7/2/24, 2:29 PM Reflection for C++26
— Otherwise, if r is a reflection of a static member function, a function, or a non-static member function with an explicit
object parameter, &[:r:] is a pointer to function
— Otherwise, if r is a reflection of a non-static member function with an implicit object parameter, &[:r:] is a pointer to
member function.
— Otherwise, if r is a reflection of a function template or member function template, &[:r:] is the address of that overload
set - which would then require external context to resolve as usual.
For most members, this doesn’t even require any additional wording since that’s just what you get when you take the address
of the splice based on the current rules we have today.
Now, there are a couple interesting cases to point out when &[:r:] isn’t just the same as &X::f.
When r is a reflection of a function or function template that is part of an overload set, overload resolution will not consider
the whole overload set, just the specific function or function template that r reflects:
struct C {
template <class T> void f(T); // #1
void f(int); // #2
};
Another interesting question is what does this mean when r is the reflection of a constructor or destructor? Consider the type:
struct X {
X(int, int);
};
And let rc be a reflection of the constructor and rd be a reflection of the destructor. The sensible syntax and semantics for
how you would use rc and rd should be as follows:
That is, splicing a constructor behaves like a free function that produces an object of that type, so &[: rc :] has type X(*)
(int, int). On the other hand, splicing a destructor behaves like a regular member function, so &[: rd :] has type void
(X::*)().
§ 4.2.2 Limitations
Splicers can appear in many contexts, but our implementation experience has uncovered a small set of circumstances in
which a splicer must be disallowed. Mostly these are because any entity designated by a splicer can be dependent on a
template argument, so any context in which the language already disallows a dependent name must also disallow a dependent
splicer. It also becomes possible for the first time to have the “name” of a namespace or concept become dependent on a
https://isocpp.org/files/papers/P2996R4.html 25/81
7/2/24, 2:29 PM Reflection for C++26
template argument. Our implementation experience has helped to sort through which uses of these dependent names pose no
difficulties, and which must be disallowed.
Iterating over the members of a class (e.g., using std::meta::members_of) allows one, for the first time, to obtain “handles”
representing constructors. An immediate question arises of whether it’s possible to reify these constructors to construct
objects, or even to take their address. While we are very interested in exploring these ideas, we defer their discussion to a
future paper; this proposal disallows splicing a reflection of a constructor (or constructor template) in any context.
namespace A {}
constexpr std::meta::info NS_A = ^A;
namespace B {
namespace [:NS_A:] {
void fn(); // Is this '::A::fn' or '::B::A::fn' ?
}
}
We found no satisfying answer as to how to interpret examples like the one given above. Neither did we find motivating use
cases: many of the “interesting” uses for reflections of namespaces are either to introspect their members, or to pass them as
template arguments - but the above example does nothing to help with introspection, and neither can namespaces be reopened
within any dependent context. Rather than choose between unintuitive options for a syntax without a motivating use case, we
are disallowing splicers from appearing in the opening of a namespace.
C++20 already disallowed dependent enumeration types from appearing in using-enum-declarators (as in #1), as it would
otherwise force the parser to consider every subsequent identifier as possibly a member of the substituted enumeration type.
We extend this limitation to splices of dependent reflections of enumeration types, and further disallow the use of dependent
reflections of namespaces in using-directives (as in #2) following the same principle.
What kind of parameter is S? If R reflects a class template, then it is a non-type template parameter of deduced type, but if R
reflects a concept, it is a type template parameter. There is no other circumstance in the language for which it is not possible
to decide at parse time whether a template parameter is a type or a non-type, and we don’t wish to introduce one for this use
case.
The most obvious solution would be to introduce a concept [:R:] syntax that requires that R reflect a concept, and while this
could be added going forward, we weren’t convinced of its value at this time - especially since the above can easily be
rewritten:
https://isocpp.org/files/papers/P2996R4.html 26/81
7/2/24, 2:29 PM Reflection for C++26
We are resolving this ambiguity by simply disallowing a reflection of a concept, whether dependent or otherwise, from being
spliced in the declaration of a template parameter (thus in the above example, the parser can assume that S is a non-type
parameter).
struct S { int a; };
Although we would like for splices of class members to be usable as designators in an initializer-list, we lack implementation
experience with the syntax and would first like to verify that there are no issues with dependent reflections. We are very likely
to propose this as an extension in a future paper.
The splicers described above all take a single object of type std::meta::info (described in more detail below). However,
there are many cases where we don’t have a single reflection, we have a range of reflections - and we want to splice them all
in one go. For that, the predecessor to this paper, [P1240R0], proposed an additional form of splicer: a range splicer.
Construct the struct-to-tuple example from above. It was demonstrated using a single splice, but it would be simpler if we had
a range splice:
A range splice, [: ... r :], would accept as its argument a constant range of meta::info, r, and would behave as an
unexpanded pack of splices. So the above expression
would evaluate as
However, range splicing of dependent arguments is at least an order of magnitude harder to implement than ordinary splicing.
We think that not including range splicing gives us a better chance of having reflection in C++26. Especially since, as this
paper’s examples demonstrate, a lot can be done without them.
Another way to work around a lack of range splicing would be to implement with_size<N>(f), which would behave like
f(integral_constant<size_t, 0>{}, integral_constant<size_t, 1>{}, ..., integral_constant<size_t, N-1>
{}). Which is enough for a tolerable implementation:
https://isocpp.org/files/papers/P2996R4.html 27/81
7/2/24, 2:29 PM Reflection for C++26
We propose [: and :] be single tokens rather than combinations of [, ], and :. Among others, it simplifies the handling of
expressions like arr[[:refl():]]. On the flip side, it requires a special rule like the one that was made to handle <:: to
leave the meaning of arr[::N] unchanged and another one to avoid breaking a (somewhat useless) attribute specifier of the
form [[using ns:]].
A syntax that is delimited on the left and right is useful here because spliced expressions may involve lower-precedence
operators. Additionally, it’s important that the left- and right-hand delimiters are different so as to allow nested splices when
that comes up.
However, there are other possibilities. For example, now that $ or @ are available in the basic source character set, we might
consider those. One option that was recently brought up was @ primary-expression which would allow writing @e for the
simple identifier splices but for the more complex operations still require parenthesizing for readability. $<expr> is
somewhat natural to those of us that have used systems where $ is used to expand placeholders in document templates:
There are two other pieces of functionality that we will probably need syntax for in the future:
The prefixes typename and template are only strictly needed in some cases where the operand of the splice is a dependent
expression. In our proposal, however, we only make typename optional in the same contexts where it would be optional for
qualified names with dependent name qualifiers. That has the advantage to catch unfortunate errors while keeping a single
rule and helping human readers parse the intended meaning of otherwise ambiguous constructs.
§ 4.3 std::meta::info
The type std::meta::info can be defined as follows:
namespace std {
namespace meta {
using info = decltype(^::);
}
}
— any template
— any namespace (including the global namespace) or namespace alias
— any object that is a permitted result of a constant expression
— any value with structural type that is a permitted result of a constant expression
— the null reflection (when default-constructed)
We for now restrict the space of reflectable values to those of structural type in order to meet two requirements:
1. The compiler must know how to mangle any reflectable value (i.e., when a reflection thereof is used as a template
argument).
2. The compiler must know how to compare any two reflectable values, ideally without interpreting user-defined
comparison operators (i.e., to implement comparison between reflections).
Values of structural types can already be used as template arguments (so implementations must already know how to mangle
them), and the notion of template-argument-equivalent values defined on the class of structural types helps guarantee that
&fn<^value1> == &fn<^value2> if and only if &fn<value1> == &fn<value2>.
Notably absent at this time are reflections of expressions. For example, one might wish to walk over the subexpressions of a
function call:
void g() {
constexpr auto call = ^(fn(42));
static_assert(
template_arguments_of(function_of(call))[0] ==
^int);
}
Previous revisions of this proposal suggested limited support for reflections of constant expressions. The introduction of side
effects from constant evaluations (by this very paper), however, renders this roughly as difficult for constant expressions as it
is for non-constant expressions. We instead defer all expression reflection to a future paper, and only present value and object
reflection in the present proposal.
static_assert(^int == ^int);
static_assert(^int != ^const int);
static_assert(^int != ^int &);
When the ^ operator is followed by an id-expression, the resulting std::meta::info reflects the entity named by the
expression. Such reflections are equivalent only if they reflect the same entity.
https://isocpp.org/files/papers/P2996R4.html 29/81
7/2/24, 2:29 PM Reflection for C++26
int x;
struct S { static int y; };
static_assert(^x == ^x);
static_assert(^x != ^S::y);
static_assert(^S::y == static_data_members_of(^S)[0]);
Special rules apply when comparing certain kinds of reflections. A reflection of an alias compares equal to another reflection
if and only if they are both aliases, alias the same type, and share the same name and scope. In particular, these rules allow
e.g., fn<^std::string> to refer to the same instantiation across translation units.
A reflection of an object (including variables) does not compare equally to a reflection of its value. Two values of different
types never compare equally.
Nontype template arguments of type std::meta::info are permitted (and frequently useful!), but since reflections represent
internal compiler state while processing a single translation unit, they cannot be allowed to leak across TUs. Therefore both
variables of consteval-only type, and entities specialized by a non-type template argument of consteval-only type, cannot have
module or external linkage (i.e., they must have either internal or no linkage). While this can lead to some code bloat, we
aren’t aware of any organic use cases for reflection that are harmed by this limitation.
A corollary of this rule is that static data members of a class cannot have consteval-only types - such members always have
external linkage, and to do otherwise would be an ODR violation. Again, we aren’t aware of any affected use-cases that
absolutely require this.
The namespace std::meta is an associated type of std::meta::info, which allows standard library meta functions to be
invoked without explicit qualification. For example:
https://isocpp.org/files/papers/P2996R4.html 30/81
7/2/24, 2:29 PM Reflection for C++26
#include <meta>
struct S {};
std::string name2 = std::meta::name_of(^S); // Okay.
std::string name1 = name_of(^S); // Also okay.
Default constructing or value-initializing an object of type std::meta::info gives it a null reflection value. A null reflection
value is equal to any other null reflection value and is different from any other reflection that refers to one of the mentioned
entities. For example:
#include <meta>
struct S {};
static_assert(std::meta::info() == std::meta::info());
static_assert(std::meta::info() != ^S);
§ 4.4 Metafunctions
We propose a number of metafunctions declared in namespace std::meta to operator on reflection values. Adding
metafunctions to an implementation is expected to be relatively “easy” compared to implementing the core language features
described previously. However, despite offering a normal consteval C++ function interface, each on of these relies on
“compiler magic” to a significant extent.
In C++23, “constant evaluation” produces pure values without observable side-effects and thus the order in which constant-
evaluation occurs is immaterial. In fact, while the language is designed to permit constant evaluation to happen at compile
time, an implementation is not strictly required to take advantage of that possibility.
Some of the proposed metafunctions, however, have side-effects that have an effect on the remainder of the program. For
example, we provide a define_class metafunction that provides a definition for a given class. Clearly, we want the effect of
calling that metafunction to be “prompt” in a lexical-order sense. For example:
#include <meta>
struct S;
void g() {
static_assert(is_type(define_class(^S, {})));
S s; // S should be defined at this point.
}
First, we identify a subset of manifestly constant-evaluated expressions and conversions characterized by the fact that their
evaluation must occur and must succeed in a valid C++ program: We call these plainly constant-evaluated. We require that a
programmer can count on those evaluations occurring exactly once and completing at translation time.
Second, we sequence plainly constant-evaluated expressions and conversions within the lexical order. Specifically, we require
that the evaluation of a plainly constant-evaluated expression or conversion occurs before the implementation checks the
validity of source constructs lexically following that expression or conversion.
Those constraints are mostly intuitive, but they are a significant change to the underlying principles of the current standard in
this respect.
[P2758R1] (“Emitting messages at compile time”) also has to deal with side effects during constant evaluation. However,
those effects (“output”) are of a slightly different nature in the sense that they can be buffered until a manifestly constant-
evaluated expression/conversion has completed. “Buffering” a class type completion is not practical (e.g., because other
metafunctions may well depend on the completed class type). Still, we are not aware of incompatibilities between our
proposal and [P2758R1].
https://isocpp.org/files/papers/P2996R4.html 31/81
7/2/24, 2:29 PM Reflection for C++26
Earlier revisions of this proposal suggested several possible approaches to handling errors in reflection metafunctions. This
question arises naturally when considering, for instance, examples like template_of(^int): the argument is a reflection of a
type, but that type is not a specialization of a template, so there is no valid reflected template for us to return.
1. Returning an invalid reflection (similar to NaN for floating point) which carries source location info and some useful
message (i.e., the approach suggested by P1240)
2. Returning a std::expected<std::meta::info, E> for some reflection-specific error type E, which carries source
location info and some useful message
3. Failing to be a constant expression
4. Throwing an exception of type E, which requires a language extension for such exceptions to be catchable during
constexpr evaluation
We found that we disliked (1) since there is no satisfying value that can be returned for a call like
template_arguments_of(^int): We could return a std::vector<std::meta::info> having a single invalid reflection, but
this makes for awkward error handling. The experience offered by (3) is at least consistent, but provides no immediate means
for a user to “recover” from an error.
Either std::expected or constexpr exceptions would allow for a consistent and straightforward interface. Deciding between
the two, we noticed that many of usual concerns about exceptions do not apply during translation:
— concerns about runtime performance, object file size, etc. do not exist, and
— concerns about code evolving to add new uncaught exception types do not apply
An interesting example illustrates one reason for our preference for exceptions over std::expected:
— If template_of returns an expected<info, E>, then foo<int> is a substitution failure — expected<T, E> is equality-
comparable to T, that comparison would evaluate to false but still be a constant expression.
— If template_of returns info but throws an exception, then foo<int> would cause that exception to be uncaught, which
would make the comparison not a constant expression. This actually makes the constraint ill-formed - not a substitution
failure. In order to have foo<int> be a substitution failure, either the constraint would have to first check that T is a
template or we would have to change the language rule that requires constraints to be constant expressions (we would of
course still keep the requirement that the constraint is a bool).
Since the R2 revision of this paper, [P3068R1] has proposed the introduction of constexpr exceptions. The proposal addresses
hurdles like compiler modes that disable exception support, and a Clang-based implementation is underway. We believe this
to be the most desirable error-handling mechanism for reflection metafunctions.
Because constexpr exceptions have not yet been adopted into the working draft, we do not specify any functions in this paper
that throw exceptions. Rather, we propose that they fail to be constant expressions (i.e., case 3 above), and note that this
approach will allow us to forward-compatibly add exceptions at a later time. In the interim period, implementations should
have all of the information needed to issue helpful diagnostics (e.g., “note: R does not reflect a template specialization”) to
improve the experience of writing reflection code.
For example:
— template_arguments_of(^std::tuple<int>) is {^int}
— substitute(^std::tuple, {^int}) is ^std::tuple<int>
https://isocpp.org/files/papers/P2996R4.html 32/81
7/2/24, 2:29 PM Reflection for C++26
This requires us to answer the question: how do we accept a range parameter and how do we provide a range return.
For return, we intend on returning std::vector<std::meta::info> from all such APIs. This is by far the easiest for users to
deal with. We definitely don’t want to return a std::span<std::meta::info const>, since this requires keeping all the
information in the compiler memory forever (unlike std::vector which could free its allocation). The only other option
would be a custom container type which is optimized for compile-time by being able to produce elements lazily on demand -
i.e. so that nonstatic_data_members_of(^T)[3] wouldn’t have to populate all the data members, just do enough work to be
able to return the 4th one. But that adds a lot of complexity that’s probably not worth the effort.
1. Accept std::span<std::meta::info const>, which now accepts braced-init-list arguments so it’s pretty convenient
in this regard.
2. Accept std::vector<std::meta::info>
3. Accept any range whose type_value is std::meta::info.
Now, for compiler efficiency reasons, it’s definitely better to have all the arguments contiguously. So the compiler wants
span. There’s really no reason to prefer vector over span. Accepting any range would look something like this:
namespace std::meta {
template <typename R>
concept reflection_range = ranges::input_range<R>
&& same_as<ranges::range_value_t<R>, info>;
This API is more user friendly than accepting span<info const> by virtue of simply accepting more kinds of ranges. The
default template argument allows for braced-init-lists to still work. Example.
Specifically, if the user is doing anything with range adaptors, they will either end up with a non-contiguous or non-sized
range, which will no longer be convertible to span - so they will have to manually convert their range to a vector<info> in
order to pass it to the algorithm. Because the implementation wants contiguity anyway, that conversion to vector will happen
either way - so it’s just a matter of whether every call needs to do it manually or the implementation can just do it once.
consteval auto type_struct_to_tuple(info type) -> consteval auto type_struct_to_tuple(info type) ->
meta::info { meta::info {
return substitute( return substitute(
^tuple, ^tuple,
nonstatic_data_members_of(type) nonstatic_data_members_of(type)
| views::transform(meta::type_of) | views::transform(meta::type_of)
| |
views::transform(meta::type_remove_cvref) views::transform(meta::type_remove_cvref)
| ranges::to<vector>()); );
} }
This shouldn’t cause much compilation overhead. Checking convertibility to span already uses Ranges machinery. And
implementations can just do the right thing interally:
consteval auto __builtin_substitute(info tmpl, info const* arg, size_t num_args) ->
info;
https://isocpp.org/files/papers/P2996R4.html 33/81
7/2/24, 2:29 PM Reflection for C++26
} else {
auto as_vector = ranges::to<vector<info>>((R&&)args);
return __builtin_substitute(tmpl, as_vector.data(), as_vector.size());
}
}
As such, we propose that all the range-accepting algorithms accept any range.
using A = int;
In C++ today, A and int can be used interchangeably and there is no distinction between the two types. With reflection as
proposed in this paper, that will no longer be the case. ^A yields a reflection of an alias to int, while ^int yields a reflection
of int. ^A == ^int evaluates to false, but there will be a way to strip aliases - so dealias(^A) == ^int evaluates to true.
This opens up the question of how various other metafunctions handle aliases and it is worth going over a few examples:
using A = int;
using B = std::unique_ptr<int>;
template <class T> using C = std::unique_ptr<T>;
— is_type(^A) is true. ^A is an alias, but it’s an alias to a type, and if this evaluated as false then everyone would have
to dealias everything all the time.
— has_template_arguments(^B) is false while has_template_arguments(^C<int>) is true. Even though B is an alias
to a type that itself has template arguments (unique_ptr<int>), B itself is simply a type alias and does not. This reflects
the actual usage.
— Meanwhile, template_arguments_of(^C<int>) yields {^int} while
template_arguments_of(^std::unique_ptr<int>) yields {^int, ^std::default_deleter<int>}. This is because C
has its own template arguments that can be reflected on.
Thanks to recent work originating in SG16 (the “Unicode” study group) we can assume that all source code is ultimately
representable as Unicode code points. C++ now also has types to represent UTF-8-encoded text (incl. char8_t, u8string,
and u8string_view) and corresponding literals like u8"Hi". Unfortunately, what can be done with those types is still limited
at the time of this writing. For example,
#include <iostream>
int main() {
std::cout << u8"こんにちは世界\n";
}
is not standard C++ because the standard output stream does not have support for UTF-8 literals.
In practice ordinary strings encoded in the “ordinary string literal encoding” (which may or may not be UTF-8) are often
used. We therefore need mechanisms to produce the corresponding ordinary string types as well.
Orthogonal to the character representation is the data structure used to traffic in source text. An implementation can easily
have at least three potential representations of reflected source text:
a. the internal representation used, e.g., in the compiler front end’s AST-like structures (persistent)
https://isocpp.org/files/papers/P2996R4.html 34/81
7/2/24, 2:29 PM Reflection for C++26
b. the representation of string literals in the AST (persistent)
(some compilers might share some of those representations). For transient text during constant evaluation we’d like to use
string/u8string values, but because of the limitations on non-transient allocation during constant evaluation we cannot
easily transfer such types to the non-constant (i.e., run-time) environment. E.g., if name_of were a (consteval) metafunction
returning a std::string value, the following simple example would not work:
#include <iostream>
#include <meta>
int main() {
int hello_world = 42;
std::cout << name_of(^hello_world) << "\n"; // Doesn't work if name_of produces a
std::string.
}
We can instead return a std::string_view or std::u8string_view, but that has the downside that it effectively makes all
results of querying source text persistent for the compilation.
For now, however, we propose that queries like name_of do produce “string view” results. For example:
An alternative strategy that we considered is the introduction of a “proxy type” for source text:
namespace std::meta {
struct source_text_info {
...
template<typename T>
requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view)
||
^T == dealias(^std::string) || ^T == dealias(^std::u8string))
consteval T as();
...
};
}
where the as<...>() member function produces a string-like type as desired. That idea was dropped, however, because it
became unwieldy in actual use cases.
With a source text query like name_of(refl) it is possible that the some source characters of the result are not representable.
We can then consider multiple options, including:
2. any unrepresentable source characters are translated to a different presentation, such as universal-character-names of
the form \u{ hex-number },
3. any source characters not in the basic source character set are translated to a different presentation (as in (2)).
Following much discussion with SG16, we propose #1: The query fails to evaluate if the identifier cannot be represented in
the ordinary string literal encoding.
https://isocpp.org/files/papers/P2996R4.html 35/81
7/2/24, 2:29 PM Reflection for C++26
§ 4.4.7 Synopsis
Here is a synopsis for the proposed library API. The functions will be explained below.
namespace std::meta {
using info = decltype(^::);
// type queries
consteval auto type_of(info r) -> info;
consteval auto parent_of(info r) -> info;
consteval auto dealias(info r) -> info;
// template queries
consteval auto template_of(info r) -> info;
consteval auto template_arguments_of(info r) -> vector<info>;
// member queries
template<typename ...Fs>
consteval auto members_of(info type_class, Fs ...filters) -> vector<info>;
template<typename ...Fs>
consteval auto bases_of(info type_class, Fs ...filters) -> vector<info>;
consteval auto static_data_members_of(info type_class) -> vector<info>;
consteval auto nonstatic_data_members_of(info type_class) -> vector<info>;
consteval auto subobjects_of(info type_class) -> vector<info>;
consteval auto enumerators_of(info type_enum) -> vector<info>;
// member access
consteval auto access_context() -> info;
struct access_pair {
consteval access_pair(info target, info from = access_context());
};
// substitute
template <reflection_range R = span<info const>>
consteval auto can_substitute(info templ, R&& args) -> bool;
template <reflection_range R = span<info const>>
consteval auto substitute(info templ, R&& args) -> info;
// reflect_invoke
template <reflection_range R = span<info const>>
consteval auto reflect_invoke(info target, R&& args) -> info;
template <reflection_range R1 = span<info const>, reflection_range R2 = span<info
const>>
consteval auto reflect_invoke(info target, R1&& tmpl_args, R2&& args) -> info;
// extract
template <typename T>
consteval auto extract(info) -> T;
// test_trait
consteval auto test_trait(info templ, info type) -> bool;
template <reflection_range R = span<info const>>
consteval auto test_trait(info templ, R&& arguments) -> bool;
https://isocpp.org/files/papers/P2996R4.html 37/81
7/2/24, 2:29 PM Reflection for C++26
consteval auto is_nonstatic_data_member(info entity) -> bool;
consteval auto is_static_member(info entity) -> bool;
consteval auto is_base(info entity) -> bool;
consteval auto is_namespace(info entity) -> bool;
consteval auto is_function(info entity) -> bool;
consteval auto is_variable(info entity) -> bool;
consteval auto is_type(info entity) -> bool;
consteval auto is_alias(info entity) -> bool;
consteval auto is_incomplete_type(info entity) -> bool;
consteval auto is_template(info entity) -> bool;
consteval auto is_function_template(info entity) -> bool;
consteval auto is_variable_template(info entity) -> bool;
consteval auto is_class_template(info entity) -> bool;
consteval auto is_alias_template(info entity) -> bool;
consteval auto is_concept(info entity) -> bool;
consteval auto is_structured_binding(info entity) -> bool;
consteval auto is_value(info entity) -> bool;
consteval auto is_object(info entity) -> bool;
consteval auto has_template_arguments(info r) -> bool;
consteval auto is_constructor(info r) -> bool;
consteval auto is_destructor(info r) -> bool;
consteval auto is_special_member(info r) -> bool;
consteval auto is_user_provided(info r) -> bool;
// define_class
struct data_member_options_t;
consteval auto data_member_spec(info type_class,
data_member_options_t options = {}) -> info;
template <reflection_range R = span<info const>>
consteval auto define_class(info type_class, R&&) -> info;
// data layout
consteval auto offset_of(info entity) -> size_t;
consteval auto size_of(info entity) -> size_t;
consteval auto alignment_of(info entity) -> size_t;
consteval auto bit_offset_of(info entity) -> size_t;
consteval auto bit_size_of(info entity) -> size_t;
namespace std::meta {
consteval auto name_of(info) -> string_view;
consteval auto qualified_name_of(info) -> string_view;
consteval auto display_name_of(info) -> string_view;
If a string_view is returned, its contents consist of characters representable by the ordinary string literal encoding only; if
any character cannot be represented, it is not a constant expression.
Given a reflection r that designates a declared entity X, name_of(r) and qualified_name_of(r) return a string_view
holding the unqualified and qualified name of X, respectively. u8name_of(r) and qualified_name_of(r) return the same,
respectively, as a u8string_view. For all other reflections, an empty string view is produced. For template instances, the
name does not include the template argument list.
Given a reflection r, display_name_of(r) and u8display_name_of(r) return an unspecified non-empty string_view and
u8string_view, respectively. Implementations are encouraged to produce text that is helpful in identifying the reflected
construct.
https://isocpp.org/files/papers/P2996R4.html 38/81
7/2/24, 2:29 PM Reflection for C++26
Given a reflection r, source_location_of(r) returns an unspecified source_location. Implementations are encouraged to
produce the correct source location of the item designated by the reflection.
namespace std::meta {
consteval auto type_of(info r) -> info;
consteval auto parent_of(info r) -> info;
consteval auto dealias(info r) -> info;
}
If r is a reflection designating a typed entity, type_of(r) is a reflection designating its type. If r is already a type,
type_of(r) is not a constant expression. This can be used to implement the C typeof feature (which works on both types
and expressions and strips qualifiers):
If r designates a member of a class or namespace, parent_of(r) is a reflection designating its immediately enclosing class or
(possibly inline or anonymous) namespace.
If r designates an alias, dealias(r) designates the underlying entity. Otherwise, dealias(r) produces r. dealias is
recursive - it strips all aliases:
using X = int;
using Y = X;
static_assert(dealias(^int) == ^int);
static_assert(dealias(^X) == ^int);
static_assert(dealias(^Y) == ^int);
namespace std::meta {
consteval auto object_of(info r) -> info;
consteval auto value_of(info r) -> info;
}
If r is a reflection of a variable denoting an object with static storage duration, then object_of(r) is a reflection of the object
designated by the variable. If r is already a reflection of an object, object_of(r) is r. For all other inputs, object_of(r) is
not a constant expression.
int x;
int &y = x;
static_assert(^x != ^y);
static_assert(object_of(^x) == object_of(^y));
If r is a reflection of an enumerator, then value_of(r) is a reflection of the value of the enumerator. Otherwise, if r is a
reflection of an object usable in constant expressions, then value_of(r) is a reflection of the value of the object. For all other
inputs, value_of(r) is not a constant expression.
namespace std::meta {
consteval auto template_of(info r) -> info;
https://isocpp.org/files/papers/P2996R4.html 39/81
7/2/24, 2:29 PM Reflection for C++26
consteval auto template_arguments_of(info r) -> vector<info>;
}
If r is a reflection designating a specialization of some template, then template_of(r) is a reflection of that template and
template_arguments_of(r) is a vector of the reflections of the template arguments. In other words, the preconditions on
both is that has_template_arguments(r) is true.
For example:
namespace std::meta {
template<typename ...Fs>
consteval auto members_of(info type_class, Fs ...filters) -> vector<info>;
template<typename ...Fs>
consteval auto bases_of(info type_class, Fs ...filters) -> vector<info>;
The template members_of returns a vector of reflections representing the direct members of the class type represented by its
first argument. Any non-static data members appear in declaration order within that vector. Anonymous unions appear as a
non-static data member of corresponding union type. Reflections of structured bindings shall not appear in the returned
vector. If any Filters... argument is specified, a member is dropped from the result if any filter applied to that members
reflection returns false. E.g., members_of(^C, std::meta::is_type) will only return types nested in the definition of C
and members_of(^C, std::meta::is_type, std::meta::is_variable) will return an empty vector since a member
cannot be both a type and a variable.
The template bases_of returns the direct base classes of the class type represented by its first argument, in declaration order.
subobjects_of returns the base class subobjects and the non-static data members of a type, in declaration order. Note that the
term subobject also includes array elements, which we are excluding here. Such reflections would currently be of minimal
use since you could not splice them with access (e.g. arr.[:elem:] is not supported), so would need some more thought
first.
enumerators_of returns the enumerator constants of the indicated enumeration type in declaration order.
https://isocpp.org/files/papers/P2996R4.html 40/81
7/2/24, 2:29 PM Reflection for C++26
namespace std::meta {
consteval auto access_context() -> info;
struct access_pair {
consteval access_pair(info target, info from = access_context());
};
template<typename ...Fs>
consteval auto accessible_members_of(access_pair p, Fs ...filters) -> vector<info>;
template<typename ...Fs>
consteval auto accessible_members_of(info target, info from, Fs ...filters) ->
vector<info>;
template<typename ...Fs>
consteval auto accessible_bases_of(access_pair p, Fs ...filters) -> vector<info>;
template<typename ...Fs>
consteval auto accessible_bases_of(info target, info from, Fs ...filters) ->
vector<info>;
The access_context() function returns a reflection of the function, class, or namespace whose scope encloses the function
call.
The type access_pair represents the operands of a check for access to target from the scope introduced by the function,
class, or namespace reflected by from. If from is not specified, the access_pair constructor captures the current access
context of the caller via the default argument. Each function also provides an overload whereby target and from may be
specified as distinct arguments.
Each function named accessible_meow_of returns the result of meow_of filtered on is_accessible.
For example:
class C {
int k;
static_assert(is_accessible(^C::k)); // ok: context is 'C'.
static_assert(accessible_subobjects_of(^C).size() == 0);
static_assert(accessible_subobjects_of(^C, ^fn).size() == 1);
§ 4.4.14 substitute
namespace std::meta {
template <reflection_range R = span<info const>>
consteval auto can_substitute(info templ, R&& args) -> bool;
template <reflection_range R = span<info const>>
consteval auto substitute(info templ, R&& args) -> info;
}
https://isocpp.org/files/papers/P2996R4.html 41/81
7/2/24, 2:29 PM Reflection for C++26
Given a reflection for a template and reflections for template arguments that match that template, substitute returns a
reflection for the entity obtained by substituting the given arguments in the template. If the template is a concept template, the
result is a reflection of a constant of type bool.
For example:
This process might kick off instantiations outside the immediate context, which can lead to the program being ill-formed.
Note that the template is only substituted, not instantiated. For example:
can_substitute(templ, args) simply checks if the substitution can succeed (with the same caveat about instantiations
outside of the immediate context). If can_substitute(templ, args) is false, then substitute(templ, args) will be ill-
formed.
§ 4.4.15 reflect_invoke
namespace std::meta {
template <reflection_range R = span<info const>>
consteval auto reflect_invoke(info target, R&& args) -> info;
template <reflection_range R1 = span<info const>, reflection_range R2 = span<info
const>>
consteval auto reflect_invoke(info target, R1&& tmpl_args, R2&& args) -> info;
}
For the first overload: Letting F be the entity reflected by target, and A0, A1, ..., AN be the sequence of entities reflected
by the values held by args: if the expression F(A0, A1, ..., AN) is a well-formed constant expression evaluating to a
structural type that is not void, and if every value in args is a reflection of a value or object usable in constant expressions,
then reflect_invoke(target, args) evaluates to a reflection of the result of F(A0, A1, ..., AN). For all other
invocations, reflect_invoke(target, args) is not a constant expression.
The second overload behaves the same as the first overload, except instead of evaluating F(A0, A1, ..., AN), we require
that F be a reflection of a template and evaluate F<T0, T1, ..., TM>(A0, A1, ..., AN). This allows evaluating
reflect_invoke(^std::get, {std::meta::reflect_value(0)}, {e}) to evaluate to, approximately, ^std::get<0>([:
e :]).
If the returned reflection is of a value (rather than an object), the type of the reflected value is the cv-qualified (de-aliased)
type of what’s returned by the function.
A few possible extensions for reflect_invoke have been discussed among the authors. Given the advent of constant
evaluations with side-effects, it may be worth allowing void-returning functions, but this would require some representation
of “a returned value of type void”. Construction of runtime call expressions is another exciting possibility. Both extensions
require more thought and implementation experience, and we are not proposing either at this time.
namespace std::meta {
template<typename T> consteval auto reflect_value(T expr) -> info;
template<typename T> consteval auto reflect_object(T& expr) -> info;
https://isocpp.org/files/papers/P2996R4.html 42/81
7/2/24, 2:29 PM Reflection for C++26
template<typename T> consteval auto reflect_function(T& expr) -> info;
}
These metafunctions produce a reflection of the result from evaluating the provided expression. One of the most common
use-cases for such reflections is to specify the template arguments with which to build a specialization using
std::meta::substitute.
reflect_value(expr) produces a reflection of the value computed by an lvalue-to-rvalue conversion on expr. The type of
the reflected value is the cv-unqualified (de-aliased) type of expr. The result needs to be a permitted result of a constant
expression, and T cannot be of reference type.
reflect_object(expr) produces a reflection of the object designated by expr. This is frequently used to obtain a reflection
of a subobject, which might then be used as a template argument for a non-type template parameter of reference type.
int p[2];
constexpr auto r = substitute(^fn, {std::meta::reflect_object(p[1])});
reflect_function(expr) produces a reflection of the function designated by expr. It can be useful for reflecting on the
properties of a function for which only a reference is available.
§ 4.4.17 extract<T>
namespace std::meta {
template<typename T> consteval auto extract(info) -> T;
}
If r is a reflection for a value of type T, extract<T>(r) is a prvalue whose evaluation computes the reflected value.
If r is a reflection for an object of non-reference type T, extract<T&>(r) and extract<T const&>(r) are lvalues referring to
that object. If the object is usable in constant expressions [expr.const], extract<T>(r) evaluates to its value.
If r is a reflection for an object of reference type T usable in constant-expressions, extract<T>(r) evaluates to that reference.
If r is a reflection for a non-static member function and T is the type for a pointer to the reflected member function,
extract<T>(r) evaluates to a pointer to the member function.
If r is a reflection for an enumerator constant of type E, extract<E>(r) evaluates to the value of that enumerator.
If r is a reflection for a non-bit-field non-reference non-static member of type M in a class C, extract<M C::*>(r) is the
pointer-to-member value for that non-static member.
The function template extract may feel similar to splicers, but unlike splicers it does not require its operand to be a constant-
expression itself. Also unlike splicers, it requires knowledge of the type associated with the entity reflected by its operand.
§ 4.4.18 test_trait
namespace std::meta {
consteval auto test_trait(info templ, info type) -> bool {
return test_trait(templ, {type});
https://isocpp.org/files/papers/P2996R4.html 43/81
7/2/24, 2:29 PM Reflection for C++26
}
This utility translates existing metaprogramming predicates (expressed as constexpr variable templates or concept templates)
to the reflection domain. For example:
struct S {};
static_assert(test_trait(^std::is_class_v, ^S));
static_assert(test_trait(^std::is_same_v, {^S, ^S})
An implementation is permitted to recognize standard predicate templates and implement test_trait without actually
instantiating the predicate template. In fact, that is recommended practice.
namespace std::meta {
struct data_member_options_t {
struct name_type {
template <typename T> requires constructible_from<u8string, T>
consteval name_type(T &&);
optional<name_type> name;
optional<int> alignment;
optional<int> width;
bool no_unique_address = false;
};
consteval auto data_member_spec(info type,
data_member_options_t options = {}) -> info;
template <reflection_range R = span<info const>>
consteval auto define_class(info type_class, R&&) -> info;
}
data_member_spec returns a reflection of a description of a data member of given type. Optional alignment, bit-field-width,
static-ness, and name can be provided as well. An inner class name_type, which may be implicitly constructed from any of
several “string-like” types (e.g., string_view, u8string_view, char8_t[], char_t[]), is used to represent the name. If a
name is provided, it must be a valid identifier when interpreted as a sequence of UTF-8 code-units (after converting any
contained UCNs to UTF-8). Otherwise, the name of the data member is unspecified.
define_class takes the reflection of an incomplete class/struct/union type and a range of reflections of data member
descriptions and completes the given class type with data members as described (in the given order). The given reflection is
returned. For now, only data member reflections are supported (via data_member_spec) but the API takes in a range of info
anticipating expanding this in the near future.
For example:
union U;
static_assert(is_type(define_class(^U, {
data_member_spec(^int),
data_member_spec(^char),
data_member_spec(^double),
})));
https://isocpp.org/files/papers/P2996R4.html 44/81
7/2/24, 2:29 PM Reflection for C++26
// int _0;
// char _1;
// double _2;
// };
When defining a union, if one of the alternatives has a non-trivial destructor, the defined union will still have a destructor
provided - that simply does nothing. This allows implementing variant without having to further extend support in
define_class for member functions.
If type_class is a reflection of a type that already has a definition, or which is in the process of being defined, the call to
define_class is not a constant expression.
namespace std::meta {
consteval auto offset_of(info entity) -> size_t;
consteval auto size_of(info entity) -> size_t;
consteval auto alignment_of(info entity) -> size_t;
These are generalized versions of some facilities we already have in the language.
— offset_of takes a reflection of a non-static data member or a base class subobject and returns the offset of it.
— size_of takes the reflection of a type, object, variable, non-static data member, or base class subobject and returns its
size.
— alignment_of takes the reflection of a type, non-static data member, or base class subobject and returns its alignment.
— bit_size_of and bit_offset_of give the size and offset of a base class subobject or non-static data member, except in
bits. Note that the bit_offset_of is a value between 0 and 7, inclusive:
struct Msg {
uint64_t a : 10;
uint64_t b : 8;
uint64_t c : 25;
uint64_t d : 21;
};
static_assert(bit_offset_of(^Msg::a) == 0);
static_assert(bit_offset_of(^Msg::b) == 2);
static_assert(bit_offset_of(^Msg::c) == 2);
static_assert(bit_offset_of(^Msg::d) == 3);
static_assert(bit_size_of(^Msg::a) == 10);
static_assert(bit_size_of(^Msg::b) == 8);
static_assert(bit_size_of(^Msg::c) == 25);
static_assert(bit_size_of(^Msg::d) == 21);
https://isocpp.org/files/papers/P2996R4.html 45/81
7/2/24, 2:29 PM Reflection for C++26
consteval auto total_bit_offset_of(std::meta::info m) -> size_t {
return offset_of(m) * 8 + bit_offset_of(m);
}
static_assert(total_bit_offset_of(^Msg::a) == 0);
static_assert(total_bit_offset_of(^Msg::b) == 10);
static_assert(total_bit_offset_of(^Msg::c) == 18);
static_assert(total_bit_offset_of(^Msg::d) == 43);
There is a question of whether all the type traits should be provided in std::meta. For instance, a few examples in this paper
use std::meta::type_remove_cvref(t) as if that exists. Technically, the functionality isn’t strictly necessary - since it can
be provided indirectly:
Direct Indirect
std::meta::type_is_const(type) std::meta::extract<bool>
(std::meta::substitute(^std::is_const_v, {type}))
std::meta::test_trait(^std::is_const_v, type)
Having std::meta::meow for every trait std::meow is more straightforward and will likely be faster to compile, though
means we will have a much larger library API. There are quite a few traits in 21 [meta] - but it should be easy enough to
specify all of them. So we’re doing it.
Now, one thing that came up is that the straightforward thing we want to do is to simply add a std::meta::meow for every
trait std::meow and word it appropriately. That’s what the current wording in this revision does. However, we’ve run into a
conflict. The standard library type traits are all type traits - they only accept types. As such, their names are simply things like
std::is_pointer, std::is_const, std::is_lvalue_reference, and so forth. Renaming it to std::type_is_pointer, for
instance, would be a waste of characters since there’s nothing else the argument could be save for a type. But this is no longer
the case. Consider std::meta::is_function(e), which is currently actually specified twice in our wording having two
different meanings:
1. A consteval function equivalent of the type trait std::is_function<T>, such that std::meta::is_function(e)
mandates that e reflect a type and checks if that type is a function type. This is the same category of type trait as the
ones mentioned above.
2. A new kind of reflection query std::meta::is_function(e) which asks if e is the reflection of a function (as opposed
to a type or a namespace or a template, etc.). This is the same category of query as std::meta::is_template or
std::meta::is_concept or std::meta::is_namespace.
Both of these are useful, yet they mean different things entirely - the first is ill-formed when passed a reflection of a function
(as opposed to a function type), and the second would simply answer false for the reflection of any type (function type or
otherwise). So what do we do?
Probably the most straightforward choice would be to either prefix or suffix all of the type traits with _type. We think prefix
is a little bit better because it groups all the type traits together and perhaps make it clearer that the argument(s) must be
types. That is: std::is_pointer<T> because std::meta::type_is_pointer(^T), std::is_arithmetic<T> becomes
std::meta::type_is_arithmetic(^T), and so forth. The advantage of this approach is that it very likely just works, also
opening the door to making a more general std::meta::is_const(e) that checks not just if e is a const-qualified type but
also if it’s a const-qualified object or a const-qualified member, etc. The disadvantage is that the suffixed names would not
be familiar - we’re much more familiar with the name is_copy_constructible than we would be with
type_is_copy_constructible.
That said, it’s not too much added mental overhead to remember type_is_copy_constructible and this avoids have to
remember which type traits have the suffix and which don’t. Not to mention that many of the type traits read as if they would
accept objects just fine (e.g. is_trivially_copyable). So we propose that simply all the type traits be suffixed with *_type.
// File 'cls.h'
struct Cls {
void odr_violator() {
if constexpr (members_of(parent_of(^std::size_t)).size() % 2 == 0)
branch_1();
else
branch_2();
}
};
Two translation units including cls.h can generate different definitions of Cls::odr_violator() based on whether an odd
or even number of declarations have been imported from std. Branching on the members of a namespace is dangerous
because namespaces may be redeclared and reopened: the set of contained declarations can differ between program points.
The creative programmer will find no difficulty coming up with other predicates which would be similarly dangerous if
substituted into the same if constexpr condition: for instance, given a branch on is_incomplete_type(^T), if one
translation unit #includes a forward declaration of T, another #includes a complete definition of T, and they both afterwards
#include "cls.h", the result will be an ODR violation.
Additional papers are already in flight proposing additional metafunctions that pose similar dangers. For instance, [P3096R1]
proposes the parameters_of metafunction. This feature is important for generating language bindings (e.g., Python,
JavaScript), but since parameter names can differ between declarations, it would be dangerous for a member function defined
in a header file to branch on the name of a parameter.
These cases are not difficult to identify: Given an entity E and two program points P1 and P2 from which a reflection of E may
be optained, it is unsafe to branch runtime code generation on any property of E (e.g., namespace members, parameter names,
completeness of a class) that can be modified between P1 and P2. Worth noting as well, these sharp edges are not unique (or
new) to reflection: It is already possible to build an ODR trap based on the completeness of a class using C++23.
Education and training are important to help C++ users avoid such sharp edges, but we do not find them sufficiently
concerning to give pause to our enthusiasm for the features proposed by this paper.
§5 Proposed Wording
§ 5.1 Language
§ 5.2 [lex.phases] Phases of translation
7 Whitespace characters separating tokens are no longer significant. Each preprocessing token is converted into
a token (5.6). The resulting tokens constitute a translation unit and are syntactically and semantically analyzed
and translated. Plainly constant-evaluated expressions ([expr.const]) appearing outside template declarations
are evaluated in lexical order. Diagnosable rules (4.1.1 [intro.compliance.general]) that apply to constructs
whose syntactic end point occurs lexically after the syntactic end point of a plainly constant-evaluated
expression X are considered in a context where X has been evaluated. […]
8 […] All the required instantiations are performed to produce instantiation units. Plainly constant-evaluated
expressions ([expr.const]) appearing in those instantiation units are evaluated in lexical order as part of the
instantiation process. Diagnosable rules (4.1.1 [intro.compliance.general]) that apply to constructs whose
syntactic end point occurs lexically after the syntactic end point of a plainly constant-evaluated expression X
are considered in a context where X has been evaluated. […]
https://isocpp.org/files/papers/P2996R4.html 47/81
7/2/24, 2:29 PM Reflection for C++26
— Otherwise, if the next three characters are <:: and the subsequent character is neither : nor >, the < is
treated as a preprocessing token by itself and not as the first character of the alternative token <:.
— Otherwise, if the next three characters are [:: and the subsequent character is not : or if the next three
characters are [:>, the [ is treated as a preprocessing token by itself and not as the first character of the
preprocessing token [:.
Change the grammar for operator-or-punctuator in paragraph 1 of 5.12 [lex.operators] to include splicer delimiters:
operator-or-punctuator: one of
{ } [ ] ( ) [: :]
<: :> <% %> ; : ...
? :: . .* -> ->* ~
! + - * / % ^ & |
= += -= *= /= %= ^= &= |=
== != < > <= >= <=> && ||
<< >> <<= >>= ++ -- ,
and or xor not bitand bitor compl
and_eq or_eq xor_eq not_eq
(4.1) — A function is named by an expression or conversion if it is the selected member of an overload set
([basic.lookup], [over.match], [over.over]) in an overload resolution performed as part of forming that
expression or conversion, or if it is denoted by a splice-expression ([expr.prim.splice]), unless it is a pure
virtual function and either the expression is not an id-expression naming the function with an explicitly
qualified name or the expression forms a pointer to member ([expr.unary.op]).
15pre If a class C is defined in a translation unit with a call to std::meta::define_class, every definition of that
class shall be the result of a call to std::meta::define_class such that its respective members are equal in
number and have respectively the same types, alignments, [[no_unique_address]] attributes (if any), bit-
field widths (if any), and specified names (if any).
https://isocpp.org/files/papers/P2996R4.html 48/81
7/2/24, 2:29 PM Reflection for C++26
3 … Any typedef-names and using-declarations used to specify the types do not contribute to this set. The
set of entities is determined in the following way:
(3.0) — If T is std::meta::info, its associated set of entities is the singleton containing the function
std::meta::is_type.
(3.1) — If T is a fundamental type, its associated set of entities is empty.
(3.2) — If T is a class type …
1 Lookup of an identifier followed by a:: scope resolution operator considers only namespaces, types, and
templates whose specializations are types. If a name, template-id, or computed-type-specifier, or
splice-name-qualifier is followed by a::, it shall designate a namespace, class, enumeration, or dependent
type, and the ::is never interpreted as a complete nested-name-specifier.
4 An unnamed namespace or a namespace declared directly or indirectly within an unnamed namespace has
internal linkage. All other namespaces have external linkage. The name of an entity that belongs to a
(4.1)
namespace scope that has not been given internal linkage above and that is the name of * a variable; or
(4.7) — if the enclosing namespace has internal linkage, the name has internal linkage;
(4.8) — otherwise, if the declaration of the name is attached to a named module ([module.unit]) and is not
exported ([module.interface]), the name has module linkage;
(4.9) — otherwise, if the declaration is a variable having consteval-only type ([basic.types.general]), or is of a
class template specialization type having a consteval-only type as a non-type template argument, the
name has internal linkage.
(4.10) — otherwise, the name has external linkage.
9 Arithmetic types (6.8.2), enumeration types, pointer types, pointer-to-member types (6.8.4),
std::meta::info, std::nullptr_t, and cv-qualified (6.8.5) versions of these types are collectively called
scalar types.
https://isocpp.org/files/papers/P2996R4.html 49/81
7/2/24, 2:29 PM Reflection for C++26
— std::meta::info, or
An object of consteval-only type shall either end its lifetime during the evaluation of a manifestly constant-
evaluated expression or conversion (7.7 [expr.const]), or be a constexpr variable that is not odr-used (6.3
[basic.def.odr]).
* Consteval-only types may not be used to declare a static data member of a class having module or external
linkage. Furthermore, specializations of a class template having a non-type template argument of consteval-
only type may not be used to declare a static data member of a class having module or external linkage.
* A value of type std::meta::info is called a reflection and represents a language element such as a type, a
value, an object, a non-static data member, etc. An expression convertible to std::meta::info is said to
reflect the language element represented by the resulting value; the language element is said to be reflected by
the expression. sizeof(std::meta::info) shall be equal to sizeof(void*). [ Note 1: Reflections are only
meaningful during translation. The notion of consteval-only types (see 6.8.1 [basic.types.general]) exists to
diagnose attempts at using such values outside the translation process. — end note ]
primary-expression:
literal
this
( expression )
id-expression
lambda-expression
fold-expression
requires-expression
+ splice-expression
+
+ splice-expression
+ [: constant-expression :]
+ template[: constant-expression :] < template-argument-listopt >
nested-name-specifier:
::
type-name ::
namespace-name ::
computed-type-specifier ::
+ splice-name-qualifier ::
nested-name-specifier identifier ::
https://isocpp.org/files/papers/P2996R4.html 50/81
7/2/24, 2:29 PM Reflection for C++26
nested-name-specifier templateopt simple-template-id ::
+
+ splice-name-qualifier:
+ [: constant-expression :]
1 The component names of a qualified-id are those of its nested-name-specifier and unqualified-id.
The component names of a nested-name-specifier are its identifier (if any) and those of its type-name,
namespace-name, simple-template-id, and/or nested-name-specifier, and/or the type-name or
namespace-name of the entity reflected by the constant-expression of its splice-name-qualifier. For a
nested-name-specifier having a splice-name-qualifier with a constant-expression that reflects the
global namespace, the component names are the same as for ::. The constant-expression of a splice-
name-qualifier shall be a reflection of either a type-name, namespace-name, or the global namespace.
https://isocpp.org/files/papers/P2996R4.html 51/81
7/2/24, 2:29 PM Reflection for C++26
postfix-expression -> templateopt id-expression
+ postfix-expression -> templateopt splice-expression
1 A postfix expression followed by a dot . or an arrow ->, optionally followed by the keyword template, and
then followed by an id-expression or a splice-expression, is a postfix expression. [ Note 1: If the keyword
template is used, the following unqualified name is considered to refer to a template ([temp.names]). If a
simple-template-id results and is followed by a ::, the id-expression or splice-expression is a qualified-id.
— end note ]
2 For the first option, if the dot is followed by an id-expression names or splice-expression naming a static
member or an enumerator, the first expression is a discarded-value expression ([expr.context]); if the id-
expression or splice-expression names a non-static data member, the first expression shall be a glvalue.
For the second option (arrow), the first expression shall be a prvalue having pointer type. The expression E1-
>E2 is converted to the equivalent form (*(E1)).E2; the remainder of [expr.ref] will address only the first
option (dot).
3 The postfix expression before the dot is evaluated the result of that evaluation, together with the id-
expression or splice-expression, determines the result of the entire postfix expression.
unary-expression:
...
delete-expression
+ reflect-expression
+ reflect-expression:
+ ^ ::
+ ^ namespace-name
+ ^ nested-name-specifieropt template-name
+ ^ nested-name-specifieropt concept-name
+ ^ type-id
+ ^ id-expression
https://isocpp.org/files/papers/P2996R4.html 52/81
7/2/24, 2:29 PM Reflection for C++26
1 The unary ^ operator (called the reflection operator) produces a prvalue — called a reflection — whose type is
the reflection type (i.e., std::meta::info). That reflection represents its operand.
2 Every value of type std::meta::info is either a reflection of some entity (or description thereof) or a null
reflection value.
3 A reflect-expression is parsed as the longest possible sequence of tokens that could syntactically form a reflect-
expression.
4 [ Example 1:
— end example ]
5 When applied to ::, the reflection operator produces a reflection for the global namespace. When applied to a
namespace-name, the reflection operator produces a reflection for the indicated namespace or namespace alias.
6 When applied to a template-name, the reflection operator produces a reflection for the indicated template.
7 When applied to a concept-name, the reflection operator produces a reflection for the indicated concept.
8 When applied to a type-id, the reflection operator produces a reflection for the indicated type or type alias.
9 When applied to an lvalue id-expression (7.5.4 [expr.prim.id]), the reflection operator produces a reflection
of the variable, function, enumerator constant, or non-static member designated by the operand. The id-
expression is not evaluated.
(9.1) — If this id-expression names an overload set S, and if the assignment of S to an invented variable of type
const auto (9.2.9.7.2 [dcl.type.auto.deduct]) would select a unique candidate function F from S, the
result is a reflection of F. Otherwise, the expression ^S is ill-formed.
10 When applied to a prvalue id-expression, the reflection operator produces a reflection of the value computed
by the operand [ Note 1: An id-expression naming a non-type template parameter of non-class and non-
reference type is a prvalue. — end note ]
[ Example 2:
— end example ]
https://isocpp.org/files/papers/P2996R4.html 53/81
7/2/24, 2:29 PM Reflection for C++26
2 The converted operands shall have arithmetic, enumeration, pointer, or pointer-to-member type, or type types
std::meta::info or std ::
nullptr_t. The operators == and != both yield true or false, i.e., a result of type
bool. In each case below, the operands shall have the same type after the specified conversions have been
applied.
5 Two operands of type std::nullptr_t or one operand of type std::nullptr_t and the other a null pointer
constant compare equal.
6 If two operands compare equal, the result is true for the == operator and false for the != operator. If two
operands compare unequal, the result is false for the == operator and true for the != operator. Otherwise, the
result of each of the operators is unspecified.
Add a new paragraph after the definition of manifestly constant-evaluated 7.7 [expr.const]/20:
(21.3) — the initializer of a constexpr (9.2.6 [dcl.constexpr]) or constinit (9.2.7 [dcl.constinit]) variable, or
https://isocpp.org/files/papers/P2996R4.html 54/81
7/2/24, 2:29 PM Reflection for C++26
1 […] A name declared with the typedef specifier becomes a typedef-name. A typedef-name names the type
associated with the identifier ([dcl.decl]) or simple-template-id ([temp.pre]); a typedef-name is thus a
synonym for another type. A typedef-name does not introduce a new type the way a class declaration
([class.name]) or enum declaration ([dcl.enum]) does.
2 A typedef-name can also be introduced by an alias-declaration. The identifier following the using keyword is
not looked up; it becomes a typedef-name and the optional attribute-specifier-seq following the identifier
appertains to that typedef-name. Such a typedef-name has the same semantics as if it were introduced by the
typedef specifier. In particular, it does not define a new type.
* A type alias is either a name declared with the typedef specifier or a name introduced by an alias-
declaration.
computed-type-specifier:
decltype-specifier
pack-index-specifier
+ splice-type-specifier
+ splice-enum-name:
+ [: constant-expression :]
+
using-enum-declarator:
nested-name-specifieropt identifier
nested-name-specifieropt simple-template-id
+ splice-enum-name
+ splice-type-specifier
+ typename [: constant-expression :]
+ [: constant-expression :]
3 The constant-expression shall evaluate to a reflection of a type, and the type designated by the splice-
type-specifier is the same as the type reflected by the constant-expression.
https://isocpp.org/files/papers/P2996R4.html 55/81
7/2/24, 2:29 PM Reflection for C++26
(6.2) — […]
7 To default-initialize an object of type T means:
(8.1) — […]
9 To value-initialize an object of type T means: […]
2 A program that refers to a deleted function implicitly or explicitly, other than to declare it or to use as the
operand of the reflection operator, is ill-formed.
using-enum-declaration:
using enum using-enum-declarator ;
+ splice-enum-name:
+ [: constant-expression :]
+
using-enum-declarator:
nested-name-specifieropt identifier
nested-name-specifieropt simple-template-id
+ splice-enum-name
https://isocpp.org/files/papers/P2996R4.html 56/81
7/2/24, 2:29 PM Reflection for C++26
§ 9.8.4 [namespace.udir] Using namespace directive
Modify the grammar for using-directive as follows:
+ splice-namespace-name:
+ [: constant-expression :]
+
+ namespace-declarator:
+ nested-name-specifieropt namespace-name
+ splice-namespace-name
+
using-directive:
- attribute-specifier-seqopt using namespace $nested-name-specifieropt namespace-name
+ attribute-specifier-seqopt using namespace namespace-declarator
1 A using-directive shall not appear in class scope, but may appear in namespace scope or in block scope. A
namespace-declarator not consisting of a splice-namespace-name nominates the namespace found by
lookup (6.5.3 [basic.lookup.unqual], 6.5.5 [basic.lookup.qual]) and shall not contain a dependent nested-
name-specifier. A namespace-declarator consisting of a splice-namespace-name shall contain a non-
dependent constant-expression that reflects a namespace or namespace alias, and nominates the entity
reflected by the constant-expression.
attribute-specifier:
[ [ attribute-using-prefixopt attribute-list ] ]
+ [ [ using attribute-namespace :] ]
alignment-specifier
balanced-token :
( balanced-token-seqopt )
[ balanced-token-seqopt ]
{ balanced-token-seqopt }
- any token other than a parenthesis, a bracket, or a brace
+ [: balanced-token-seqopt :]
+ any token other than (, ), [, ], {, }, [:, or :]
https://isocpp.org/files/papers/P2996R4.html 57/81
7/2/24, 2:29 PM Reflection for C++26
bool operator==(T, T);
bool operator!=(T, T);
4 … The concept designated by a type-constraint shall be a type concept ([temp.concept]) that does not consist
of a splice-template-name.
+ splice-template-name:
+ template [: constant-expression :]
+
+ splice-template-argument:
+ [: constant-expression :]
+
template-name:
identifier
+ splice-template-name
template-argument:
constant-expression
type-id
id-expression
braced-init-list
+ splice-template-argument
The component name of a simple-template-id, template-id, or template-name is the first name in it. If
the template-name is a splice-template-name, the converted constant-expression shall evaluate to a
reflection for a concept, variable template, class template, alias template, or function template which is not a
constructor template or destructor template; the splice-template-name names the entity reflected by the
constant-expression.
https://isocpp.org/files/papers/P2996R4.html 58/81
7/2/24, 2:29 PM Reflection for C++26
2 The value of a non-type template-parameter P of (possibly deduced) type T is determined from its template
argument A as follows. If T is not a class type and A is notneither a braced-init-list nor a splice-
template-argument, A shall be a converted constant expression ([expr.const]) of type T; the value of P is A
(as converted).
1 A template-argument for a template template-parameter shall be the name of a class template or an alias
template, expressed as id-expression, or a splice-template-argument. A template-argument for a
template template-parameter having a splice-template-argument is treated as an id-expression
nominating the class template or alias template reflected by the constant-expression of the splice-
template-argument.
2 Two values are template-argument-equivalent if they are of the same type and
(2.1) — they are of integral type and their values are the same, or
(2.2) — they are of floating-point type and their values are identical, or
(2.3) — they are of type std::nullptr_t, or
(2.*) — they are of type std::meta::info and they compare equal, or
(2.4) — they are of enumeration type and their values are the same, or
(2.5) — […]
concept-name:
identifier
+ splice-template-name
A concept-definition declares a concept. Its concept-name shall consist of an identifier, and the
identifier becomes a concept-name referring to that concept within its scope. The optional attribute-
specifier-seq appertains to the concept.
https://isocpp.org/files/papers/P2996R4.html 59/81
7/2/24, 2:29 PM Reflection for C++26
§ 13.8.3.3 [temp.dep.expr] Type-dependent expressions
Add to the list of never-type-dependent expression forms in 13.8.3.3 [temp.dep.expr]/4:
literal
sizeof unary-expression
sizeof ( type-id )
sizeof ... ( identifier )
alignof ( type-id )
typeid ( expression )
typeid ( type-id )
::opt delete cast-expression
::opt delete [ ] cast-expression
throw assignment-expressionopt
noexcept ( expression )
requires-expression
+ reflect-expression
(2.1) — […]
Expressions of the following form are value-dependent if the unary-expression or expression is type-
dependent or the type-id is dependent:
sizeof unary-expression
sizeof ( type-id )
typeid ( expression )
typeid ( type-id )
alignof ( type-id )
noexcept ( expression )
§ 5.2 Library
§ 5.2.1 16.4.5.2.1 [namespace.std] Namespace std
Insert before paragraph 7:
https://isocpp.org/files/papers/P2996R4.html 60/81
7/2/24, 2:29 PM Reflection for C++26
6 Let F denote a standard library function ([global.functions]), a standard library static member function, or an
instantiation of a standard library function template. Unless F is designated an addressable function, the
behavior of a C++ program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a
pointer to F. […]
7pre Let F denote a standard library function, member function, or function template. If F does not designate an
addressable function, it is unspecified if or how a reflection value designating the associated entity can be
formed. [ Note 1: E.g., std::meta::members_of might not produce reflections of standard functions that an
implementation handles through an extra-linguistic mechanism. — end note ]
7 A translation unit shall not declare namespace std to be an inline namespace ([namespace.def]).
… … …
#include <span>
#include <string_view>
#include <vector>
namespace std::meta {
using info = decltype(^::);
https://isocpp.org/files/papers/P2996R4.html 61/81
7/2/24, 2:29 PM Reflection for C++26
// [meta.reflection.names], reflection names and locations
consteval string_view name_of(info r);
consteval string_view qualified_name_of(info r);
consteval string_view display_name_of(info r);
https://isocpp.org/files/papers/P2996R4.html 62/81
7/2/24, 2:29 PM Reflection for C++26
consteval vector<info> nonstatic_data_members_of(info type);
consteval vector<info> subobjects_of(info type);
consteval vector<info> enumerators_of(info type_enum);
struct access_pair {
consteval access_pair(info target, info from = access_context());
};
https://isocpp.org/files/papers/P2996R4.html 63/81
7/2/24, 2:29 PM Reflection for C++26
struct name_type {
template <typename T> requires constructible_from<u8string, T>
consteval name_type(T &&);
optional<name_type> name;
optional<int> alignment;
optional<int> width;
bool no_unique_address = false;
};
consteval info data_member_spec(info type,
data_member_options_t options = {});
template <reflection_range R = span<info const>>
consteval info define_class(info type_class, R&&);
https://isocpp.org/files/papers/P2996R4.html 64/81
7/2/24, 2:29 PM Reflection for C++26
consteval bool type_is_swappable(info type);
https://isocpp.org/files/papers/P2996R4.html 65/81
7/2/24, 2:29 PM Reflection for C++26
// [meta.reflection.trans.ref], reference modifications
consteval info type_remove_reference(info type);
consteval info type_add_lvalue_reference(info type);
consteval info type_add_rvalue_reference(info type);
1 Mandates: If returning string_view, the unqualified name is representable using the ordinary string literal
encoding.
2 Returns: If r designates a declared entity X, then the unqualified name of X. Otherwise, an empty string_view
or u8string_view, respectively.
3 Mandates: If returning string_view, the qualified name is representable using the ordinary string literal
encoding.
4 Returns: If r designates a declared entity X, then the qualified name of X. Otherwise, an empty string_view or
u8string_view, respectively.
5 Mandates: If returning string_view, the implementation-defined name is representable using the ordinary
string literal encoding.
https://isocpp.org/files/papers/P2996R4.html 66/81
7/2/24, 2:29 PM Reflection for C++26
1 Returns: true if r designates a class member or base class that is public, protected, or private, respectively.
Otherwise, false.
2 Returns: true if r designates a either a virtual member function or a virtual base class. Otherwise, false.
3 Returns: true if r designates a member function that is pure virtual or overrides another member function,
respectively. Otherwise, false.
6 Returns: true if r designates a member function that is declared explicit. Otherwise, false.
7 Returns: true if r designates a noexcept function type, a pointer to noexcept function or member function
type, a closure type of a non-generic lambda whose call operator is declared noexcept, a value of any of the
previously mentioned types, or a function that is declared noexcept. Otherwise, false.
9 Returns: true if r designates a const or volatile type (respectively), a const- or volatile-qualified function type
(respectively), or an object, variable, non-static data member, or function with such a type. Otherwise, false.
10 Returns: true if r designates a final class or a final member function. Otherwise, false.
11 Returns: true if r designates an object or variable that has static storage duration. Otherwise, false.
12 Returns: true if r designates an entity that has internal linkage, module linkage, external linkage, or any
linkage, respectively ([basic.link]). Otherwise, false.
https://isocpp.org/files/papers/P2996R4.html 67/81
7/2/24, 2:29 PM Reflection for C++26
consteval bool is_variable(info r);
17 Returns: true if r designates a type alias, alias template, or namespace alias. Otherwise, false.
19 Effects: If dealias(r) designates a class template specialization with a reachable definition, the specialization
is instantiated.
20 Returns: true if the type designated by dealias(r) is an incomplete type ([basic.types]). Otherwise, false.
21 Returns: true if r designates a function template, class template, variable template, or alias template.
Otherwise, false.
23 Returns: true if r designates a function template, class template, variable template, alias template, concept,
structured binding, or value respectively. Otherwise, false.
25 Returns: true if r designates an instantiation of a function template, variable template, class template, or an
alias template. Otherwise, false.
26 Returns: true if r designates a class member, namespace member, non-static data member, static member,
base class member, constructor, destructor, or special member, respectively. Otherwise, false.
https://isocpp.org/files/papers/P2996R4.html 68/81
7/2/24, 2:29 PM Reflection for C++26
29 Mandates: r designates a typed entity. r does not designate a constructor, destructor, or structured binding.
30 Returns: A reflection of the type of that entity. If every declaration of that entity was declared with the same
type alias (but not a template parameter substituted by a type alias), the reflection returned is for that alias.
Otherwise, if some declaration of that entity was declared with an alias it is unspecified whether the reflection
returned is for that alias or for the type underlying that alias. Otherwise, the reflection returned shall not be a
type alias reflection.
31 Mandates: r is a reflection designating either an object or a variable denoting an object with static storage
duration ([expr.const]).
32 Returns: If r is a reflection of a variable, then a reflection of the object denoted by the variable. Otherwise, r.
37 Returns: If r designates a type alias or a namespace alias, a reflection designating the underlying entity.
Otherwise, r.
38 [ Example 1:
using X = int;
using Y = X;
static_assert(dealias(^int) == ^int);
static_assert(dealias(^X) == ^int);
static_assert(dealias(^Y) == ^int);
— end example ]
40 Returns: A reflection of the template of r, and the reflections of the template arguments of the specialization
designated by r, respectively.
41 [ Example 2:
static_assert(template_of(^Pair<int>) == ^Pair);
static_assert(template_arguments_of(^Pair<int>).size() == 2);
static_assert(template_of(^PairPtr<int>) == ^PairPtr);
static_assert(template_arguments_of(^PairPtr<int>).size() == 1);
— end example ]
template<class... Fs>
consteval vector<info> members_of(info r, Fs... filters);
2 Effects: If dealias(r) designates a class template specialization with a reachable definition, the specialization
is instantiated.
3 Returns: A vector containing the reflections of all the direct members m of the entity, excluding any structured
bindings, designated by r such that (filters(m) && ...) is true. Non-static data members are indexed in
the order in which they are declared, but the order of other kinds of members is unspecified. [ Note 1: Base
classes are not members. — end note ]
template<class... Fs>
consteval vector<info> bases_of(info type, Fs... filters);
4 Mandates: type is a reflection designating a complete class type and (std::predicate<Fs, info> && ...)
is true.
5 Effects: If dealias(type) designates a class template specialization with a reachable definition, the
specialization is instantiated.
6 Returns: Let C be the type designated by type. A vector containing the reflections of all the direct base
classes b, if any, of C such that (filters(b) && ...) is true. The base classes are indexed in the order in
which they appear in the base-specifier-list of C.
10 Effects: If dealias(type) designates a class template specialization with a reachable definition, the
specialization is instantiated.
11 Returns: A vector containing all the reflections in bases_of(type) followed by all the reflections in
nonstatic_data_members_of(type).
13 Returns: A vector containing the reflections of each enumerator of the enumeration designated by type_enum,
in the order in which they are declared.
1 Returns: A reflection of the function, class, or namespace scope most nearly enclosing the function call.
2 Mandates: p.target is a reflection designating a member of a class. p.from designates a function, class, or
namespace.
https://isocpp.org/files/papers/P2996R4.html 70/81
7/2/24, 2:29 PM Reflection for C++26
3 Returns: true if the class member designated by p.target can be named within the scope of p.from.
Otherwise, false.
5 Mandates: p.target is a reflection designating a complete class type. p.from designates a function, class, or
namespace.
return members_of(p.target,
[&](info r) { return is_accessible({r, p.from}); },
preds...);
8 Mandates: p.target is a reflection designating a complete class type. p.from designates a function, class, or
namespace.
return bases_of(p.target,
[&](info r) { return is_accessible({r, p.from}); },
preds...);
return accessible_members_of(p.target,
std::meta::is_nonstatic_data_member,
preds...);
return accessible_members_of(p.target,
std::meta::is_static_data_member);
15 Returns: A vector containing all the reflections in accessible_bases_of(p) followed by all the reflections in
accessible_nonstatic_data_members_of(p).
2 Returns: The offset in bytes from the beginning of an object of type parent_of(r) to the subobject associated
with the entity reflected by r.
3 Mandates: r is a reflection of a type, non-static data member, base class, object, value, or variable.
5 Mandates: r is a reflection designating an object, variable, type, non-static data member, or base class.
6 Returns: If r designates a type, object, or variable, then the alignment requirement of the entity. Otherwise, if
r designates a base class, then alignment_of(type_of(r)). Otherwise, the alignment requirement of the
subobject associated with the reflected non-static data member within any object of type parent_of(r).
8 Let V be the offset in bits from the beginning of an object of type parent_of(r) to the subobject associated
with the entity reflected by r.
9 Returns: V - offset_of(r).
10 Mandates: r is a reflection of a type, non-static data member, base class, object, value, or variable.
11 Returns If r designates a type, then the size in bits of any object having the reflected type. Otherwise, if r
reflects a non-static data member that is a bit-field, then the width of the reflected bit-field. Otherwise,
bit_size_of(type_of(r)).
1 Mandates: r is a reflection designating a value, object, variable, function, enumerator, or non-static data
member that is not a bit-field. If r reflects a value or enumerator, then T is not a reference type. If r reflects a
value or enumerator of type U, or if r reflects a variable or object of non-reference type U, then the cv-
unqualified types of T and U are the same. If r reflects a variable, object, or function with type U, and T is a
reference type, then the cv-unqualified types of T and U are the same, and T is either U or more cv-qualified
than U. If r reflects a non-static data member, or if r reflects a function and T is a reference type, then the
statement T v = &expr, where expr is an lvalue naming the entity designated by r, is well-formed.
2 Returns: If r designates a value or enumerator, then the entity reflected by r. Otherwise, if r reflects an object,
variable, or enumerator and T is not a reference type, then the result of an lvalue-to-rvalue conversion applied
https://isocpp.org/files/papers/P2996R4.html 72/81
7/2/24, 2:29 PM Reflection for C++26
to an expression naming the entity reflected by r. Otherwise, if r reflects an object, variable, or function and T
is a reference type, then the result of an lvalue naming the entity reflected by r. Otherwise, if r reflects a
function or non-static data member, then a pointer value designating the entity reflected by r.
2 Let Z be the template designated by templ and let Args... be the sequence of entities or aliases designated by
the elements of arguments.
4 Remarks: If attempting to substitute leads to a failure outside of the immediate context, the program is ill-
formed.
6 Let Z be the template designated by templ and let Args... be the sequence of entities or aliases designated by
the elements of arguments.
7 Returns: ^Z<Args...>.
1 Mandates: T is a structural type that is not a reference type. Any subobject of the value computed by expr
having reference or pointer type designates an entity that is a permitted result of a constant expression
([expr.const]).
2 Returns: A reflection of the value computed by an lvalue-to-rvalue conversion applied to expr. The type of the
reflected value is the cv-unqualified version of T.
3 Mandates: T is not a function type. expr designates an entity that is a permitted result of a constant expression.
https://isocpp.org/files/papers/P2996R4.html 73/81
7/2/24, 2:29 PM Reflection for C++26
5 Mandates: T is a function type.
7 Let F be the entity reflected by target, let Arg0 be the entity reflected by the first element of args (if any), let
Args... be the sequence of entities reflected by the elements of args excluding the first, and let TArgs... be
the sequence of entities or aliases designated by the elements of tmpl_args.
8 If F is a non-member function, a value of pointer to function type, a value of pointer to member type, or a
value of closure type, then let INVOKE-EXPR be the expression INVOKE(F, Arg0, Args...). Otherwise, if F is
a member function, then let INVOKE-EXPR be the expression Arg0.F(Args...). Otherwise, if F is a constructor
for a class C, then let INVOKE-EXPR be the expression C(Arg0, Args...) for which only the constructor F is
considered by overload resolution. Otherwise, if F is a non-member function template or a member function
template, then let INVOKE-EXPR be the expression F<TArgs...>(Arg0, Args...) or Arg0.template
F<TArgs...>(Args...) respectively. Otherwise, if F is a constructor template, then let INVOKE-EXPR be the
expression C(Arg0, Args...) for which only the constructor F is considered by overload resolution, and
TArgs... are inferred as explicit template arguments for F.
1 Mandates: type designates a type. If options.name contains a value, the string or u8string value that was
used to initialize options.name contains a valid identifier (5.10 [lex.name]).
2 Returns: A reflection of a description of the declaration of non-static data member with a type designated by
type and optional characteristics designated by options.
3 Remarks: The reflection value being returned is only useful for consumption by define_class. No other
function in std::meta recognizes such a value.
4 Let d1, d2, …, dN denote the reflection values of the range mdescrs obtained by calling data_member_spec
with type values t1, t2, … tN and option values o1, o2, … oN respectively.
5 Mandates: class_type designates an incomplete class type. mdescrs is a (possibly empty) range of reflection
values obtained by calls to data_member_spec. [ Note 1: For example, class_type could be a specialization
of a class template that has not been instantiated or explicitly specialized. — end note ] Each ti designates a
type that is valid types for data members. If oK.width (for some K) contains a value w, the corresponding type
tK is a valid type for bit field of width w. If oK.alignment (for some K) contains a value a, alignas(a) is a
valid alignment-specifier for a non-static data member of type tK.
https://isocpp.org/files/papers/P2996R4.html 74/81
7/2/24, 2:29 PM Reflection for C++26
(6.3) — The type of the respective members are the types denoted by the reflection values t1, t2, … tN.
(6.4) — If oK.no_unique_address (for some K) is true, the corresponding member is declared with attribute
[[no_unique_address]].
(6.5) — If oK.width (for some K) contains a value, the corresponding member is declared as a bit field with that
value as its width.
(6.6) — If oK.alignment (for some K) contains a value a, the corresponding member is aligned as if declared with
alignas(a).
(6.7) — If oK.name (for some K) does not contain a value, the corresponding member is declared with an
implementation-defined name. Otherwise, the corresponding member is declared with a name
corresponding to the string or u8string value that was used to initialize oK.name.
(6.8) — If class_type is a union type and any of its members is not trivially default constructible, then it has a
default constructor that is user-provided and has no effect. If class_type is a union type and any of its
members is not trivially default destructible, then it has a default destructor that is user-provided and has
no effect.
7 Returns: class_type.
1 Subclause [meta.reflection.unary] contains consteval functions that may be used to query the properties of a
type at compile time.
2 For each function taking an argument of type meta::info whose name contains type, a call to the function is
a non-constant library call (3.34 [defns.nonconst.libcall]) if that argument is not a reflection of a type or type
alias. For each function taking an argument named type_args, a call to the function is a non-constant library
call if any meta::info in that range is not a reflection of a type or a type alias.
1 For any type or type alias T, for each function std::meta::type_TRAIT defined in this clause,
std::meta::type_TRAIT(^T) equals the value of the corresponding unary type trait std::TRAIT_v<T> as
specified in 21.3.5.2 [meta.unary.cat].
2 [ Example 1:
namespace std::meta {
consteval bool type_is_void(info type) {
// one example implementation
return extract<bool>(substitute(^is_void_v, {type}));
https://isocpp.org/files/papers/P2996R4.html 75/81
7/2/24, 2:29 PM Reflection for C++26
|| type == ^volatile void
|| type == ^const volatile void;
}
}
— end example ]
1 For any type or type alias T, for each function std::meta::type_TRAIT defined in this clause,
std::meta::type_TRAIT(^T) equals the value of the corresponding unary type trait std::TRAIT_v<T> as
specified in 21.3.5.3 [meta.unary.comp].
1 For any type or type alias T, for each function std::meta::type_UNARY-TRAIT defined in this clause with
signature bool(std::meta::info), std::meta::type_UNARY-TRAIT(^T) equals the value of the
corresponding type property std::UNARY-TRAIT_v<T> as specified in 21.3.5.4 [meta.unary.prop].
2 For any types or type aliases T and U, for each function std::meta::type_BINARY-TRAIT defined in this
clause with signature bool(std::meta::info, std::meta::info), std::meta::type_BINARY-TRAIT(^T,
^U) equals the value of the corresponding type property std::BINARY-TRAIT_v<T, U> as specified in 21.3.5.4
[meta.unary.prop].
3 For any type or type alias T, pack of types or type aliases U..., and range r such that ranges::to<vector>
(r) == vector{^U...} is true, for each function template std::meta::type_VARIADIC-TRAIT defined in
this clause, std::meta::type_VARIADIC-TRAIT(^T, r) equals the value of the corresponding type property
std::VARIADIC-TRAIT_v<T, U...> as specified in 21.3.5.4 [meta.unary.prop].
https://isocpp.org/files/papers/P2996R4.html 76/81
7/2/24, 2:29 PM Reflection for C++26
1 For any type or type alias T, for each function std::meta::type_PROP defined in this clause with signature
size_t(std::meta::info), std::meta::type_PROP(^T) equals the value of the corresponding type
property std::PROP_v<T> as specified in 21.3.6 [meta.unary.prop.query].
2 For any type or type alias T and unsigned integer value I, std::meta::type_extent(^T, I) equals
std::extent_v<T, I> ([meta.unary.prop.query]).
1 The consteval functions specified in this clause may be used to query relationships between types at compile
time.
2 For any types or type aliases T and U, for each function std::meta::type_REL defined in this clause with
signature bool(std::meta::info, std::meta::info), std::meta::type_REL(^T, ^U) equals the value of
the corresponding type relation std::REL_v<T, U> as specified in 21.3.7 [meta.rel].
3 For any type or type alias T, pack of types or type aliases U..., and range r such that ranges::to<vector>
(r) == vector{^U...} is true, for each binary function template std::meta::type_VARIADIC-REL,
std::meta::type_VARIADIC-REL(^T, r) equals the value of the corresponding type relation
std::VARIADIC-REL_v<T, U...> as specified in 21.3.7 [meta.rel].
https://isocpp.org/files/papers/P2996R4.html 77/81
7/2/24, 2:29 PM Reflection for C++26
4 For any types or type aliases T and R, pack of types or type aliases U..., and range r such that
ranges::to<vector>(r) == vector{^U...} is true, for each ternary function template
std::meta::type_VARIADIC-REL-R defined in this clause, std::meta::type_VARIADIC-REL-R(^R, ^T, r)
equals the value of the corresponding type relation std::VARIADIC-REL-R_v<R, T, U...> as specified in
21.3.7 [meta.rel].
5 [ Note 1: If t is a reflection of the type int and u is a reflection of an alias to the type int, then t == u is
false but type_is_same(t, u) is true. t == dealias(u) is also true. — end note ].
1 Subclause [meta.reflection.trans] contains consteval functions that may be used to transform one type to
another following some predefined rule.
1 For any type or type alias T, for each function std::meta::type_MOD defined in this clause,
std::meta::type_MOD(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in
21.3.8.2 [meta.trans.cv].
1 For any type or type alias T, for each function std::meta::type_MOD defined in this clause,
std::meta::type_MOD(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in
21.3.8.3 [meta.trans.ref].
1 For any type or type alias T, for each function std::meta::type_MOD defined in this clause,
std::meta::type_MOD(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in
21.3.8.4 [meta.trans.sign].
https://isocpp.org/files/papers/P2996R4.html 78/81
7/2/24, 2:29 PM Reflection for C++26
consteval info type_make_signed(info type);
consteval info type_make_unsigned(info type);
1 For any type or type alias T, for each function std::meta::type_MOD defined in this clause,
std::meta::type_MOD(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in
21.3.8.5 [meta.trans.arr].
1 For any type or type alias T, for each function std::meta::type_MOD defined in this clause,
std::meta::type_MOD(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in
21.3.8.6 [meta.trans.ptr].
[ Editor's note: There are four transformations that are deliberately omitted here. type_identity and enable_if are not
useful, conditional(cond, t, f) would just be a long way of writing cond ? t : f, and basic_common_reference is a
class template intended to be specialized and not directly invoked. ]
1 For any type or type alias T, for each function std::meta::type_MOD defined in this clause with signature
std::meta::info(std::meta::info), std::meta::type_MOD(^T) returns the reflection of the
corresponding type std::MOD_t<T> as specified in 21.3.8.7 [meta.trans.other].
2 For any pack of types or type aliases T... and range r such that ranges::to<vector>(r) ==
vector{^T...} is true, for each unary function template std::meta::type_VARIADIC-MOD defined in this
clause, std::meta::type_VARIADIC-MOD(r) returns the reflection of the corresponding type std::VARIADIC-
MOD_t<T...> as specified in 21.3.8.7 [meta.trans.other].
3 For any type or type alias T, pack of types or type aliases U..., and range r such that ranges::to<vector>
(r) == vector{^U...} is true, std::meta::type_invoke_result(^T, r) returns the reflection of the
corresponding type std::invoke_result_t<T, U...> (21.3.8.7 [meta.trans.other]).
4 [ Example 1:
// example implementation
consteval info type_unwrap_reference(info type) {
type = dealias(type);
if (has_template_arguments(type) && template_of(type) == ^reference_wrapper) {
return type_add_lvalue_reference(template_arguments_of(type)[0]);
} else {
return type;
https://isocpp.org/files/papers/P2996R4.html 79/81
7/2/24, 2:29 PM Reflection for C++26
}
}
— end example ]
To 15.11 [cpp.predefined]:
__cpp_impl_coroutine 201902L
__cpp_impl_destroying_delete 201806L
__cpp_impl_three_way_comparison 201907L
+ __cpp_impl_reflection 2024XXL
§6 References
[N3980] H. Hinnant, V. Falco, J. Byteway. 2014-05-24. Types don’t know #.
https://wg21.link/n3980
[P0784R7] Daveed Vandevoorde, Peter Dimov,Louis Dionne, Nina Ranns, Richard Smith, Daveed Vandevoorde. 2019-07-22.
More constexpr containers.
https://wg21.link/p0784r7
[P1061R5] Barry Revzin, Jonathan Wakely. 2023-05-18. Structured Bindings can introduce a Pack.
https://wg21.link/p1061r5
[P1240R0] Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2018-10-08. Scalable Reflection in C++.
https://wg21.link/p1240r0
[P1240R2] Daveed Vandevoorde, Wyatt Childers, Andrew Sutton, Faisal Vali. 2022-01-14. Scalable Reflection.
https://wg21.link/p1240r2
[P1306R2] Andrew Sutton, Sam Goodrick, Daveed Vandevoorde, and Dan Katz. 2024-05-07. Expansion statements.
https://wg21.link/p1306r2
[P1887R1] Corentin Jabot. 2020-01-13. Reflection on attributes.
https://wg21.link/p1887r1
[P1974R0] Jeff Snyder, Louis Dionne, Daveed Vandevoorde. 2020-05-15. Non-transient constexpr allocation using
propconst.
https://wg21.link/p1974r0
[P2237R0] Andrew Sutton. 2020-10-15. Metaprogramming.
https://wg21.link/p2237r0
[P2670R1] Barry Revzin. 2023-02-03. Non-transient constexpr allocation.
https://wg21.link/p2670r1
[P2758R1] Barry Revzin. 2023-12-09. Emitting messages at compile time.
https://wg21.link/p2758r1
[P2996R0] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2023-10-15.
Reflection for C++26.
https://wg21.link/p2996r0
[P2996R1] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2023-12-18.
Reflection for C++26.
https://wg21.link/p2996r1
[P2996R2] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-
02-15. Reflection for C++26.
https://isocpp.org/files/papers/P2996R4.html 80/81
7/2/24, 2:29 PM Reflection for C++26
https://wg21.link/p2996r2
[P2996R3] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-
05-22. Reflection for C++26.
https://wg21.link/p2996r3
[P3068R1] Hana Dusíková. 2024-03-18. Allowing exception throwing in constant-evaluation.
https://wg21.link/p3293r0
[P3096R1] Adam Lach and Walter Genovese. 2024-04-29. Function Parameter Reflection in Reflection for C++26.
https://wg21.link/p3096r1
[P3293R0] Peter Dimov, Dan Katz, Barry Revzin, and Daveed Vandevoorde. 2024-05-19. Splicing a base class subobject.
https://wg21.link/p3293r0
[P3295R0] Ben Craig. 2024-05-18. Freestanding constexpr containers and constexpr exception types.
https://wg21.link/p3295r0
https://isocpp.org/files/papers/P2996R4.html 81/81