Document #: | P2996R7 [Latest] [Status] |
Date: | 2024-10-12 |
Project: | Programming Language C++ |
Audience: |
CWG, LEWG |
Reply-to: |
Wyatt Childers <wcc@edg.com> Peter Dimov <pdimov@gmail.com> Dan Katz <dkatz85@bloomberg.net> Barry Revzin <barry.revzin@gmail.com> Andrew Sutton <andrew.n.sutton@gmail.com> Faisal Vali <faisalv@gmail.com> Daveed Vandevoorde <daveed@edg.com> |
make_integer_sequence
hash_append
tuple_cat
^
)
[:
…:]
)
std::meta::info
identifier_of
,
display_string_of
,
source_location_of
type_of
,
parent_of
,
dealias
object_of
,
value_of
template_of
,
template_arguments_of
members_of
,
static_data_members_of
,
nonstatic_data_members_of
,
bases_of
,
enumerators_of
substitute
reflect_invoke
reflect_value
,
reflect_object
,
reflect_function
extract<T>
data_member_spec
,
define_class
define_static_string
,
define_static_array
using enum
declaration<type_traits>
synopsis<meta>
synopsisbit_cast
Since [P2996R6]:
accessible_members
family of functionsget_public
family of
functionstuple
and
variant
traitsis_mutable_member
function(u8)operator_symbol_of
functions, tweaked enumerator names in std::meta::operators
members_of
is_user_declared
for
completeness with
is_user_provided
Since [P2996R5]:
members_of
and
define_class
. An informal
elaboration on this is included in a new section on “Reachability and
injected declarations”.type_of
no longer returns
reflections of
typedef-names
; added
elaboration of reasoning to the “Handling
Aliases” section.define_static_array
,
has_complete_definition
.subobjects_of
and
accessible_subobjects_of
(will be
reintroduced by [P3293R1]).enumerators_of
in terms of
has_complete_definition
.reflect_{value, object, function}
are expressed as mandates.is_special_member
to
is_special_member_function
to align
with core language terminology.(u8)identifier_of
,
has_identifier
,
extract
,
data_member_spec
,
define_class
,
reflect_invoke
,
source_location_of
).typedef-name
” over “alias
of a type” in formal wording.Since [P2996R4]:
access_pair
type, and redid API to
be based on an access_context
is_noexcept
span<info const>
to initializer_list<info>
test_trait
(u8)name_of
and (u8)qualified_name_of
;
added (u8)identifier_of
,
operator_of
,
define_static_string
.display_name_of
to
display_string_of
is_enumerator
,
is_copy_constructor
,
is_move_constructor
,
is_assignment
,
is_move_assignment
,
is_copy_assignment
,
is_default_constructor
,
has_default_member_initializer
,
is_lvalue_reference_qualified
,
is_rvalue_reference_qualified
, is_literal_operator(_template)
,
is_conversion_function(_template)
,
is_operator(_template)
,
is_data_member_spec
, has_(thread|automatic)_storage_duration
data_member_spec
, and defined
comparison among reflections returned by it.is_alias
to is_(type|namespace)_alias
is_incomplete_type
to
is_complete_type
Since [P2996R3]:
u8name_of
,
u8qualified_name_of
,
u8display_name_of
.reflect_value
:
separated reflect_result
into three
functions: reflect_value
,
reflect_object
,
reflect_function
is_noexcept
to apply to
a wider class of entitiestest_type
and
test_types
to
test_trait
has_module_linkage
metafunctionobject_of
metafunctionSince [P2996R2]:
accessible_members_of
variants to restore a TS-era agreementvalue_of
to
extract
, and expanded it to operate
on functionscan_substitute
,
is_value
,
is_object
, and (new)
value_of
meta::info
yield a null reflectionreflect_invoke
to support template
argumentstype_
to avoid name clashes. added
more generalized is_const
,
is_final
, and
is_volatile
is_noexcept
and fixed
is_explicit
to only apply to member
functions, not member function templatesSince [P2996R1], several changes to the overall library API:
qualified_name_of
(to
partner with name_of
)is_static
for being
ambiguous, added
has_internal_linkage
(and
has_linkage
and
has_external_linkage
) and
is_static_member
insteadis_class_member
,
is_namespace_member
, and
is_concept
reflect_invoke
Other paper changes:
Since [P2996R0]:
synth_struct
to
define_class
entity_ref
and
pointer_to_member
into
value_of
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]:
std::meta::info
,^
) that
produces a reflection value for its operand construct,consteval
metafunctions to work with reflections (including deriving
other reflections), and[: 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.
While we tried to select a useful subset of the P1240 features, we
also made a few additions and changes. Most of those changes are minor.
For example, we added a std::meta::test_trait
interface that makes it convenient to use existing standard type
predicates (such as is_class_v
) in
reflection computations.
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.
Perhaps the most common suggestion made regarding the framework
outlined in P1240 is to switch from the single std::meta::info
type to a family of types covering various language elements (e.g.,
std::meta::variable
,
std::meta::type
,
etc.).
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.
Other advantages of a single opaque type include:
std::vector<std::meta::info>
can easily represent a mixed template argument list — containing types
and nontypes — without fear of slicing values).Lock3 implemented the equivalent of much that is proposed here in a fork of Clang (specifically, it worked with the P1240 proposal, but also included several other capabilities including a first-class injection mechanism).
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> <vals...> replicator = {}; replicator_type} template<typename R> consteval auto expand(R range) { ::vector<std::meta::info> args; stdfor (auto r : range) { .push_back(reflect_value(r)); args} return substitute(^__impl::replicator, args); }
Used like:
With expansion statements
|
With expand
workaround
|
---|---|
|
|
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:
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:
constexpr auto r = ^int; typename[:r:] x = 42; // Same as: int x = 42; typename[:^char:] c = '*'; // Same as: char c = '*';
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:
using MyType = [:sizeof(int)<sizeof(long)? ^long : ^int:]; // Implicit "typename" prefix.
On Compiler Explorer: EDG, Clang.
Our second example enables selecting a member “by number” for a specific type:
struct S { unsigned i:2, j:6; }; consteval auto member_number(int n) { if (n == 0) return ^S::i; else if (n == 1) return ^S::j; } int main() { {0, 0}; S s.[:member_number(1):] = 42; // Same as: s.j = 42; s.[:member_number(5):] = 0; // Error (member_number(5) is not a constant). s}
This example also illustrates that bit fields are not beyond the reach of this proposal.
On Compiler Explorer: EDG, Clang.
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 represents 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:
struct S { unsigned i:2, j:6; }; consteval auto member_number(int n) { return std::meta::nonstatic_data_members_of(^S)[n]; } int main() { {0, 0}; S s.[:member_number(1):] = 42; // Same as: s.j = 42; s.[:member_number(5):] = 0; // Error (member_number(5) is not a constant). s}
On Compiler Explorer: EDG, Clang.
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.
Another frequently-useful metafunction is std::meta::identifier_of
,
which returns a std::string_view
describing the identifier with which an entity represented by a given
reflection value was declared. With such a facility, we could
conceivably access non-static data members “by string”:
struct S { unsigned i:2, j:6; }; consteval auto member_named(std::string_view name) { for (std::meta::info field : nonstatic_data_members_of(^S)) { if (has_identifier(field) && identifier_of(field) == name) return field; } } int main() { {0, 0}; S s.[:member_named("j"):] = 42; // Same as: s.j = 42; s.[:member_named("x"):] = 0; // Error (member_named("x") is not a constant). s}
On Compiler Explorer: EDG, Clang.
Here, sizes
will be a std::array<std::size_t, 3>
initialized with {sizeof(int), sizeof(float), sizeof(double)}
:
constexpr std::array types = {^int, ^float, ^double}; constexpr std::array sizes = []{ ::array<std::size_t, types.size()> r; std::views::transform(types, r.begin(), std::meta::size_of); stdreturn r; }();
Compare this to the following type-based approach, which produces the
same array sizes
:
template<class...> struct list {}; using types = list<int, float, double>; constexpr auto sizes = []<template<class...> class L, class... T>(L<T...>) { return std::array<std::size_t, sizeof...(T)>{{ sizeof(T)... }}; }(types{});
On Compiler Explorer: EDG, Clang.
make_integer_sequence
We can provide a better implementation of
make_integer_sequence
than a
hand-rolled approach using regular template metaprogramming (although
standard libraries today rely on an intrinsic for this):
#include <utility> #include <vector> template<typename T> consteval std::meta::info make_integer_seq_refl(T N) { ::vector args{^T}; stdfor (T k = 0; k < N; ++k) { .push_back(std::meta::reflect_value(k)); args} return substitute(^std::integer_sequence, args); } template<typename T, T N> using make_integer_sequence = [:make_integer_seq_refl<T>(N):];
On Compiler Explorer: EDG, Clang.
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 { ::size_t offset; std::size_t size; std}; // returns std::array<member_descriptor, N> template <typename S> consteval auto get_layout() { constexpr auto members = nonstatic_data_members_of(^S); ::array<member_descriptor, members.size()> layout; stdfor (int i = 0; i < members.size(); ++i) { [i] = {.offset=offset_of(members[i]).bytes, .size=size_of(members[i])}; layout} return layout; } struct X { char a; int b; double c; }; /*constexpr*/ auto Xd = get_layout<X>(); /* where Xd would be std::array<member_descriptor, 3>{{ { 0, 1 }, { 4, 4 }, { 8, 8 } }} */
On Compiler Explorer: EDG, Clang.
One of the most commonly requested facilities is to convert an enum value to a string (this example relies on expansion statements):
template <typename E> requires std::is_enum_v<E> constexpr std::string enum_to_string(E value) { template for (constexpr auto e : std::meta::enumerators_of(^E)) { if (value == [:e:]) { return std::string(std::meta::identifier_of(e)); } } return "<unnamed>"; } enum Color { red, green, blue }; static_assert(enum_to_string(Color::red) == "red"); static_assert(enum_to_string(Color(42)) == "<unnamed>");
We can also do the reverse in pretty much the same way:
template <typename E> requires std::is_enum_v<E> constexpr std::optional<E> string_to_enum(std::string_view name) { template for (constexpr auto e : std::meta::enumerators_of(^E)) { if (name == std::meta::identifier_of(e)) { return [:e:]; } } 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:
template <typename E> requires std::is_enum_v<E> constexpr std::string enum_to_string(E value) { constexpr auto get_pairs = []{ return std::meta::enumerators_of(^E) | std::views::transform([](std::meta::info e){ return std::pair<E, std::string>(std::meta::extract<E>(e), std::meta::identifier_of(e)); }) }; constexpr auto get_name = [](E value) -> std::optional<std::string> { if constexpr (enumerators_of(^E).size() <= 7) { // if there aren't many enumerators, use a vector with find_if() constexpr auto enumerators = get_pairs() | std::ranges::to<std::vector>(); auto it = std::ranges::find_if(enumerators, [value](auto const& pr){ return pr.first == value; }; 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).
On Compiler Explorer: EDG, Clang.
Many many variations of these functions are possible and beneficial depending on the needs of the client code. For example:
enumerators_of(^E)
enum_to_string
and
string_to_enum
with a minimal
footprintOur next example shows how a command-line option parser could work by automatically inferring flags based on member names. A real command-line parser would of course be more complex, this is just the beginning.
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) == identifier_of(dm); }); if (it == args.end()) { // no option provided, use default continue; } else if (it + 1 == args.end()) { ::print(stderr, "Option {} is missing a value\n", *it); std::exit(EXIT_FAILURE); std} using T = typename[:type_of(dm):]; auto iss = std::ispanstream(it[1]); if (iss >> opts.[:dm:]; !iss) { ::print(stderr, "Failed to parse option {} into a {}\n", *it, display_string_of(^T)); std::exit(EXIT_FAILURE); std} } return opts; } struct MyOpts { ::string file_name = "input.txt"; // Option "--file_name <string>" stdint count = 1; // Option "--count <int>" }; int main(int argc, char *argv[]) { = parse_options<MyOpts>(std::vector<std::string_view>(argv+1, argv+argc)); MyOpts opts // ... }
This example is based on a presentation by Matúš Chochlík.
On Compiler Explorer: EDG, Clang.
#include <meta> template<typename... Ts> struct Tuple { struct storage; static_assert(is_type(define_class(^storage, {data_member_spec(^Ts)...}))); storage data; (): data{} {} Tuple(Ts const& ...vs): data{ vs... } {} Tuple}; template<typename... Ts> struct std::tuple_size<Tuple<Ts...>>: public integral_constant<size_t, sizeof...(Ts)> {}; template<std::size_t I, typename... Ts> struct std::tuple_element<I, Tuple<Ts...>> { static constexpr std::array types = {^Ts...}; using type = [: types[I] :]; }; consteval std::meta::info get_nth_field(std::meta::info r, std::size_t n) { return nonstatic_data_members_of(r)[n]; } template<std::size_t I, typename... Ts> constexpr auto get(Tuple<Ts...> &t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>>& { return t.data.[:get_nth_field(^decltype(t.data), I):]; } // Similarly for other value categories...
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.
On Compiler Explorer: EDG, Clang.
Similarly to how we can implement a tuple using
define_class
to create on the fly a
type with one member for each
Ts...
, we
can implement a variant that simply defines a
union
instead of a
struct
. One
difference here is how the destructor of a
union
is
currently defined:
union U1 { int i; char c; }; union U2 { int i; ::string s; std};
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:
template <class... Ts> union U { // all of our members ... members; Ts // a defaulted destructor if all of the types are trivially destructible constexpr ~U() requires (std::is_trivially_destructible_v<Ts> && ...) = default; // ... otherwise a destructor that does nothing constexpr ~U() { } };
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:
template <typename... Ts> class Variant { union Storage; struct Empty { }; static_assert(is_type(define_class(^Storage, { (^Empty, {.name="empty"}), data_member_spec(^Ts)... data_member_spec}))); static consteval std::meta::info get_nth_field(std::size_t n) { return nonstatic_data_members_of(^Storage)[n+1]; } Storage storage_;int index_ = -1; // cheat: use libstdc++'s implementation template <typename T> static constexpr size_t accepted_index = std::__detail::__variant::__accepted_index<T, std::variant<Ts...>>; template <class F> constexpr auto with_index(F&& f) const -> decltype(auto) { return mp_with_index<sizeof...(Ts)>(index_, (F&&)f); } public: constexpr Variant() requires std::is_default_constructible_v<Ts...[0]> // should this work: storage_{. [: get_nth_field(0) :]{} } : storage_{.empty={}} (0) , index_{ ::construct_at(&storage_.[: get_nth_field(0) :]); std} constexpr ~Variant() requires (std::is_trivially_destructible_v<Ts> and ...) = default; constexpr ~Variant() { if (index_ != -1) { ([&](auto I){ with_index::destroy_at(&storage_.[: get_nth_field(I) :]); std}); } } template <typename T, size_t I = accepted_index<T&&>> requires (!std::is_base_of_v<Variant, std::decay_t<T>>) constexpr Variant(T&& t) : storage_{.empty={}} (-1) , index_{ ::construct_at(&storage_.[: get_nth_field(I) :], (T&&)t); std= (int)I; index_ } // you can't actually express this constraint nicely until P2963 constexpr Variant(Variant const&) requires (std::is_trivially_copyable_v<Ts> and ...) = default; constexpr Variant(Variant const& rhs) requires ((std::is_copy_constructible_v<Ts> and ...) and not (std::is_trivially_copyable_v<Ts> and ...)) : storage_{.empty={}} (-1) , index_{ .with_index([&](auto I){ rhsconstexpr auto field = get_nth_field(I); ::construct_at(&storage_.[: field :], rhs.storage_.[: field :]); std= I; index_ }); } constexpr auto index() const -> int { return index_; } template <class F> constexpr auto visit(F&& f) const -> decltype(auto) { if (index_ == -1) { throw std::bad_variant_access(); } return mp_with_index<sizeof...(Ts)>(index_, [&](auto I) -> decltype(auto) { return std::invoke((F&&)f, 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; ~Storage() requires std::is_trivially_destructible_v<T> && std::is_trivially_destructible_v<U> = default; ~Storage() { } }
The question here is whether we should be should be able to directly initialize members of a defined union using a splicer, as in:
: storage{.[: get_nth_field(0) :]={}}
Arguably, the answer should be yes - this would be consistent with how other accesses work. This is instead proposed in [P3293R1].
On Compiler Explorer: EDG, Clang.
#include <meta> #include <array> template <typename T, std::size_t N> struct struct_of_arrays_impl; consteval auto make_struct_of_arrays(std::meta::info type, ::meta::info N) -> std::meta::info { std::vector<std::meta::info> old_members = nonstatic_data_members_of(type); std::vector<std::meta::info> new_members = {}; stdfor (std::meta::info member : old_members) { auto type_array = substitute(^std::array, {type_of(member), N }); auto mem_descr = data_member_spec(type_array, {.name = identifier_of(member)}); .push_back(mem_descr); new_members} return std::meta::define_class( (^struct_of_arrays_impl, {type, N}), substitute); new_members} template <typename T, size_t N> using struct_of_arrays = [: make_struct_of_arrays(^T, ^N) :];
Example:
struct point { float x; float y; float z; }; using points = struct_of_arrays<point, 30>; // equivalent to: // struct points { // std::array<float, 30> x; // std::array<float, 30> y; // std::array<float, 30> z; // };
Again, the combination of
nonstatic_data_members_of
and
define_class
is put to good use.
On Compiler Explorer: EDG, Clang.
Now that we’ve seen a couple examples of using std::meta::define_class
to create a type, we can create a more sophisticated command-line parser
example.
This is the opening example for clap (Rust’s Command Line Argument Parser):
struct Args : Clap { <std::string, {.use_short=true, .use_long=true}> name; Option<int, {.use_short=true, .use_long=true}> count = 1; Option}; int main(int argc, char** argv) { auto opts = Args{}.parse(argc, argv); for (int i = 0; i < opts.count; ++i) { // opts.count has type int ::print("Hello {}!", opts.name); // opts.name has type std::string std} }
Which we can implement like this:
struct Flags { bool use_short; bool use_long; }; template <typename T, Flags flags> struct Option { ::optional<T> initializer = {}; std // some suitable constructors and accessors for flags }; // 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, ::meta::info spec) -> std::meta::info { std::vector<std::meta::info> new_members; stdfor (std::meta::info member : nonstatic_data_members_of(spec)) { auto type_new = template_arguments_of(type_of(member))[0]; .push_back(data_member_spec(type_new, {.name=identifier_of(member)})); new_members} return define_class(opts, new_members); } struct Clap { template <typename Spec> auto parse(this Spec const& spec, int argc, char** argv) { ::vector<std::string_view> cmdline(argv+1, argv+argc) std // check if cmdline contains --help, etc. struct Opts; static_assert(is_type(spec_to_opts(^Opts, ^Spec))); Opts opts; template for (constexpr auto [sm, om] : std::views::zip(nonstatic_data_members_of(^Spec), (^Opts))) { nonstatic_data_members_ofauto const& cur = spec.[:sm:]; constexpr auto type = type_of(om); // find the argument associated with this option auto it = std::ranges::find_if(cmdline, [&](std::string_view arg){ return (cur.use_short && arg.size() == 2 && arg[0] == '-' && arg[1] == identifier_of(sm)[0]) || (cur.use_long && arg.starts_with("--") && arg.substr(2) == identifier_of(sm)); }); // 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; } else if (cur.initializer) { // the type isn't optional, but an initializer is provided, use that .[:om:] = *cur.initializer; optscontinue; } else { ::print(stderr, "Missing required option {}\n", display_string_of(sm)); std::exit(EXIT_FAILURE); std} } else if (it + 1 == cmdline.end()) { ::print(stderr, "Option {} for {} is missing a value\n", *it, display_string_of(sm)); std::exit(EXIT_FAILURE); std} // found our argument, try to parse it auto iss = ispanstream(it[1]); if (iss >> opts.[:om:]; !iss) { ::print(stderr, "Failed to parse {:?} into option {} of type {}\n", std[1], display_string_of(sm), display_string_of(type)); it::exit(EXIT_FAILURE); std} } return opts; } };
On Compiler Explorer: EDG, Clang.
This example is taken from Boost.Describe:
struct universal_formatter { constexpr auto parse(auto& ctx) { return ctx.begin(); } template <typename T> auto format(T const& t, auto& ctx) const { auto out = std::format_to(ctx.out(), "{}{{", has_identifier(^T) ? identifier_of(^T) : "(unnamed-type)";); auto delim = [first=true]() mutable { if (!first) { *out++ = ','; *out++ = ' '; } = false; first }; template for (constexpr auto base : bases_of(^T)) { (); delim= std::format_to(out, "{}", (typename [: type_of(base) :] const&)(t)); out } template for (constexpr auto mem : nonstatic_data_members_of(^T)) { (); delim::string_view mem_label = has_identifier(mem) ? identifier_of(mem) std: "(unnamed-member)"; = std::format_to(out, ".{}={}", mem_label, t.[:mem:]); out } *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; }; template <> struct std::formatter<B> : universal_formatter { }; template <> struct std::formatter<X> : universal_formatter { }; template <> struct std::formatter<Y> : universal_formatter { }; template <> struct std::formatter<Z> : universal_formatter { }; int main() { ::println("{}", Z()); std// Z{X{B{.m0=0}, .m1 = 1}, Y{{.m0=0}, .m2 = 2}, .m3 = 3, .m4 = 4} }
On Compiler Explorer: Clang.
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:
static_cast<[: type_of(base) const& :]>(t)
,
or(typename [: type_of(base) :] const&)t
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.
hash_append
Based on the [N3980] API:
template <typename H, typename T> requires std::is_standard_layout_v<T> void hash_append(H& algo, T const& t) { template for (constexpr auto mem : nonstatic_data_members_of(^T)) { (algo, t.[:mem:]); hash_append} }
This approach requires allowing packs in structured bindings [P1061R5], but can also be written using
std::make_index_sequence
:
template <typename T> constexpr auto struct_to_tuple(T const& t) { constexpr auto members = nonstatic_data_members_of(^T); constexpr auto indices = []{ ::array<int, members.size()> indices; std::ranges::iota(indices, 0); stdreturn indices; }(); constexpr auto [...Is] = indices; return std::make_tuple(t.[: members[Is] :]...); }
An alternative approach is:
consteval auto type_struct_to_tuple(info type) -> info { return substitute(^std::tuple, (type) nonstatic_data_members_of| std::views::transform(std::meta::type_of) | std::views::transform(std::meta::type_remove_cvref) | std::ranges::to<std::vector>()); } template <typename To, typename From, std::meta::info ... members> constexpr auto struct_to_tuple_helper(From const& from) -> To { return To(from.[:members:]...); } template<typename From> consteval auto get_struct_to_tuple_helper() { using To = [: type_struct_to_tuple(^From): ]; ::vector args = {^To, ^From}; stdfor (auto mem : nonstatic_data_members_of(^From)) { .push_back(reflect_value(mem)); args} /* Alternatively, with Ranges: args.append_range( nonstatic_data_members_of(^From) | std::views::transform(std::meta::reflect_value) ); */ return extract<To(*)(From const&)>( (^struct_to_tuple_helper, args)); substitute} template <typename From> constexpr auto struct_to_tuple(From const& from) { return get_struct_to_tuple_helper<From>()(from); }
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.
tuple_cat
Courtesy of Tomasz Kaminski, on compiler explorer:
template<std::pair<std::size_t, std::size_t>... indices> struct Indexer { template<typename Tuples> // Can use tuple indexing instead of tuple of tuples auto operator()(Tuples&& tuples) const { using ResultType = std::tuple< ::tuple_element_t< std.second, indices::remove_cvref_t<std::tuple_element_t<indices.first, std::remove_cvref_t<Tuples>>> std>... >; return ResultType(std::get<indices.second>(std::get<indices.first>(std::forward<Tuples>(tuples)))...); } }; template <class T> consteval auto subst_by_value(std::meta::info tmpl, std::vector<T> args) -> std::meta::info { ::vector<std::meta::info> a2; stdfor (T x : args) { .push_back(std::meta::reflect_value(x)); a2} return substitute(tmpl, a2); } consteval auto make_indexer(std::vector<std::size_t> sizes) -> std::meta::info { ::vector<std::pair<int, int>> args; std for (std::size_t tidx = 0; tidx < sizes.size(); ++tidx) { for (std::size_t eidx = 0; eidx < sizes[tidx]; ++eidx) { .push_back({tidx, eidx}); args} } return subst_by_value(^Indexer, args); } 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)...)); }
The tricky thing with implementing a named tuple is actually strings
as non-type template parameters. Because you cannot just pass "x"
into
a non-type template parameter of the form
auto V
, that
leaves us with two ways of specifying the constituents:
pair
type so
that we can write make_named_tuple<pair<int, "x">, pair<double, "y">>()
,
or<^int, std::meta::reflect_value("x"),
make_named_tuple^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):
template <class T, fixed_string Name> struct pair { static constexpr auto name() -> std::string_view { return Name.view(); } using type = T; }; template <class... Tags> consteval auto make_named_tuple(std::meta::info type, Tags... tags) { ::vector<std::meta::info> nsdms; stdauto f = [&]<class Tag>(Tag tag){ .push_back(data_member_spec( nsdms(^typename Tag::type), dealias{.name=Tag::name()})); }; (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); int main() { [[maybe_unused]] auto r = R{.x=1, .y=2.0}; }
On Compiler Explorer: EDG, Clang.
Alternatively, can side-step the question of non-type template parameters entirely by keeping everything in the value domain:
consteval auto make_named_tuple(std::meta::info type, ::initializer_list<std::pair<std::meta::info, std::string_view>> members) { std::vector<std::meta::data_member_spec> nsdms; stdfor (auto [type, name] : members) { .push_back(data_member_spec(type, {.name=name})); nsdms} return define_class(type, nsdms); } 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
).
The features proposed here make it a little easier to update a ticket
counter at compile time. This is not an ideal implementation (we’d
prefer direct support for compile-time —– i.e.,
consteval
—
variables), but it shows how compile-time mutable state surfaces in new
ways.
class TU_Ticket { template<int N> struct Helper; public: static consteval int next() { int k = 0; // Search for the next incomplete 'Helper<k>'. ::meta::info r; stdwhile (is_complete_type(r = substitute(^Helper, { std::meta::reflect_value(k) }))) ++k; // Define 'Helper<k>' and return its index. (r, {}); define_classreturn k; } }; constexpr int x = TU_Ticket::next(); static_assert(x == 0); constexpr int y = TU_Ticket::next(); static_assert(y == 1); constexpr int z = TU_Ticket::next(); static_assert(z == 2);
On Compiler Explorer: EDG, Clang.
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.
// Represents a 'std::meta::info' constrained by a predicate. template <std::meta::info Pred> requires (std::predicate<[:type_of(Pred):], std::meta::info>) struct metatype { ::meta::info value; std // Construction is ill-formed unless predicate is satisfied. consteval metatype(std::meta::info r) : value(r) { if (![:Pred:](r)) throw "Reflection is not a member of this metatype"; } // Cast to 'std::meta::info' allows values of this type to be spliced. consteval operator std::meta::info() const { return value; } static consteval bool check(std::meta::info r) { return [:Pred:](r); } }; // Type representing a "failure to match" any known metatypes. struct unmatched { consteval unmatched(std::meta::info) {} static consteval bool check(std::meta::info) { return true; } }; // Returns the given reflection "enriched" with a more descriptive type. template <typename... Choices> consteval std::meta::info enrich(std::meta::info r) { // Because we control the type, we know that the constructor taking info is // the first constructor. The copy/move constructors are added at the }, so // will be the last ones in the list. ::array ctors = { std*(members_of(^Choices) | std::views::filter(std::meta::is_constructor)).begin()..., *(members_of(^unmatched) | std::views::filter(std::meta::is_constructor)).begin() }; ::array checks = {^Choices::check..., ^unmatched::check}; std for (auto [check, ctor] : std::views::zip(checks, ctors)) if (extract<bool>(reflect_invoke(check, {reflect_value(r)}))) return reflect_invoke(ctor, {reflect_value(r)}); ::unreachable(); std}
We can leverage this machinery to select different function overloads based on the “type” of reflection provided as an argument.
using type_t = metatype<^std::meta::is_type>; using template_t = metatype<^std::meta::is_template>; // Example of a function overloaded for different "types" of reflections. void PrintKind(type_t) { std::println("type"); } void PrintKind(template_t) { std::println("template"); } void PrintKind(unmatched) { std::println("unknown kind"); } int main() { // Classifies any reflection as one of: Type, Function, or Unmatched. auto enrich = [](std::meta::info r) { return ::enrich<type_t, >(r); }; template_t // Demonstration of using 'enrich' to select an overload. ([:enrich(^metatype):]); // "template" PrintKind([:enrich(^type_t):]); // "type" PrintKind([:enrich(std::meta::reflect_value(3):]); // "unknown kind" PrintKind}
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.,
::PrintKind<classify(^int)>(^int). Printer// or worse... <classify(Arg1, Arg2, Arg3)>(Arg1, Arg2, Arg3). fn
On Compiler Explorer: Clang.
^
)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.
template <int P1, const int &P2> void fn() {}
static constexpr int p[2] = {1, 2};
constexpr auto spec = ^fn<p[0], p[1]>;
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 original TS landed on reflexpr(...)
as the syntax to reflect source constructs and [P1240R0] adopted that syntax as well. As
more examples were discussed, it became clear that that syntax was both
(a) too “heavy” and (b) insufficiently distinct from a function call.
SG7 eventually agreed upon the prefix
^
operator.
The “upward arrow” interpretation of the caret matches the “lift” or
“raise” verbs that are sometimes used to describe the reflection
operation in other contexts.
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.
[:
…:]
)A reflection can be “spliced” into source code using one of several splicer forms:
[: 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:
typename[: ^:: :] x = 0; // Error.
In the same way that &C::mem
can produce a pointer, pointer to member data, pointer to function, or
pointer to member function depending on what
mem
refers to, &[: r :]
can likewise produce the same set of pointers if
r
is a reflection of a suitable
entity:
r
is a reflection of a static
data member or a variable, &[:r:]
is a pointer.r
is a reflection
of a non-static data member, &[:r:]
is a pointer to data member.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 functionr
is a reflection
of a non-static member function with an implicit object parameter, &[:r:]
is a pointer to member function.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
represents:
struct C { template <class T> void f(T); // #1 void f(int); // #2 }; void (C::*p1)(int) = &C::f; // error: ambiguous constexpr auto f1 = members_of(^C, /* function templates named f */)[0]; constexpr auto f2 = members_of(^C, /* functions named f */)[0]; void (C::*p2)(int) = &[:f1:]; // ok, refers to C::f<int> (#1) void (C::*p3)(int) = &[:f2:]; // ok, refers to C::f (#2)
Another interesting question is what does this mean when
r
is the reflection of a constructor
or destructor? Consider the type:
struct X { (int, int); X};
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:
auto x = [: rc :](1, 2); // gives you an X .[: rd :](); // destroys it x
Or, with pointers:
auto pc = &[: rc :]; auto pd = &[: rd :]; auto x = (*pc)(1, 2); // gives you an X (x.*pd)(); // destroys it
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::*)()
.
However, we are not proposing splicing constructors or destructors at the moment.
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 template argument. Our implementation experience has helped to sort through which uses of these dependent names pose no difficulties, and which must be disallowed.
This proposal places the following limitations on splicers.
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.
template <std::meta::info R> void fn1() {
using enum [:R:]::EnumCls; // #1
// ...
}
template <std::meta::info R> void fn2() {
using namespace [:R:]; // #2
// ...
}
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.
template <typename T> concept C = requires { requires true; };
template <std::meta::info R> struct Outer {
template <template [:R:] S> struct Inner { /* ... */ };
};
What kind of parameter is S
? If
R
represents a class template, then
it is a non-type template parameter of deduced type, but if
R
represents 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:
template <std::meta::info R> struct Outer {
template <typename T> requires template [:R:]<T>
struct Inner { /* ... */ };
};
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; };
constexpr S s = {.[:^S::a:] = 2};
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:
With Single Splice
|
With 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
(t.[: ... members :]...) make_tuple
would evaluate as
(t.[:members[0]:], t.[:members[1]:], ..., t.[:members[N-1]:]) make_tuple
This is a very useful facility indeed!
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:
template <typename T> constexpr auto struct_to_tuple(T const& t) { constexpr auto members = nonstatic_data_members_of(^T); return with_size<members.size()>([&](auto... Is){ return std::make_tuple(t.[: members[Is] :]...); }); }
Early discussions of splice-like constructs (related to the TS
design) considered using unreflexpr(...)
for that purpose. [P1240R0] adopted that option for
expression splicing, observing that a single splicing syntax
could not viably be parsed (some disambiguation is needed to distinguish
types and templates). SG-7 eventually agreed to adopt the [: ... :]
syntax — with disambiguating tokens such as
typename
where needed — which is a little lighter and more distinctive.
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:
[::]
|
[: :]
(with space)
|
@
|
$
|
---|---|---|---|
[:refl:] |
[: refl :] |
@refl |
$refl |
[:type_of(refl):] |
[: type_of(refl) :] |
@(type_of(refl)) |
$(type_of(refl)) |
There are two other pieces of functionality that we will probably need syntax for in the future:
+
as an
annotation introducer, but
+
can begin
an expression so another token is probably better. See also: this
thread).So any syntax discussion needs to consider the entirety of the feature.
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.
std::meta::info
The type std::meta::info
can be defined as follows:
namespace std { namespace meta { using info = decltype(^::); } }
In our initial proposal a value of type std::meta::info
can represent:
We for now restrict the space of reflectable values to those of structural type in order to meet two requirements:
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:
template <typename T> void fn(T) {} void g() { constexpr auto call = ^(fn(42)); static_assert( (function_of(call))[0] == template_arguments_of^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.
The type std::meta::info
is a scalar type for which equality and inequality are
meaningful, but for which no ordering relation is defined.
static_assert(^int == ^int); static_assert(^int != ^const int); static_assert(^int != ^int &); using Alias = int; static_assert(^int != ^Alias); static_assert(^int == dealias(^Alias)); namespace AliasNS = ::std; static_assert(^::std != ^AliasNS); static_assert(^:: == parent_of(^::std));
When the
^
operator
is followed by an id-expression, the resulting std::meta::info
represents the entity named by the expression. Such reflections are
equivalent only if they reflect the same entity.
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.
using Alias1 = int; using Alias2 = int; consteval std::meta::info fn() { using Alias1 = int; return ^Alias; } static_assert(^Alias1 == ^Alias1); static_assert(^Alias1 != ^int); static_assert(^Alias1 != ^Alias2); static_assert(^Alias1 != fn()); }
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.
constexpr int i = 42, j = 42; constexpr std::meta::info r = ^i, s = ^i; static_assert(r == r && r == s); static_assert(^i != ^j); // 'i' and 'j' are different entities. static_assert(value_of(^i) == value_of(^j)); // Two equivalent values. static_assert(^i != std::meta::reflect_object(i)) // A variable is distinct from the // object it designates. static_assert(^i != std::meta::reflect_value(42)); // A reflection of an object // is not the same as its value.
std::meta
namespaceThe 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:
#include <meta> struct S {}; ::string name2 = std::meta::identifier_of(^S); // Okay. std::string name1 = identifier_of(^S); // Also okay. std
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);
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 should be defined at this point. S s; }
Hence this proposal also introduces constraints on constant evaluation as follows…
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].
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 template that we can
return.
Some of the possibilities that we have considered include:
NaN
for floating point) which
carries source location info and some useful message (i.e., the approach
suggested by P1240)std::expected<std::meta::info, E>
for some reflection-specific error type
E
, which carries source location
info and some useful messageE
,
which requires a language extension for such exceptions to be catchable
during
constexpr
evaluationWe 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:
An interesting example illustrates one reason for our preference for
exceptions over std::expected
:
template <typename T> requires (template_of(^T) == ^std::optional) void foo();
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.
There are a number of functions, both in the “core” reflection API
that we intend to provide as well as converting some of the standard
library type traits that can accept or return a range of std::meta::info
.
For example:
template_arguments_of(^std::tuple<int>)
is {^int}
substitute(^std::tuple, {^int})
is ^std::tuple<int>
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.
For parameters, there are basically three options:
std::span<std::meta::info const>
,
which now accepts braced-init-list arguments so it’s pretty convenient
in this regard.std::vector<std::meta::info>
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
(or something like it). 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>; template <reflection_range R = initializer_list<info>> consteval auto substitute(info tmpl, R&& args) -> 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.
For example, converting a struct to a tuple type:
span only
|
any range
|
---|---|
|
|
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; template <reflection_range R = initializer_list<info>> consteval auto substitute(info tmpl, R&& args) -> info { if constexpr (ranges::sized_range<R> && ranges::contiguous_range<R>) { return __builtin_substitute(tmpl, ranges::data(args), ranges::size(args)); } 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.
Consider
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>;
This paper is proposing that:
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.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.What about when querying the type of an entity?
::string Str; stdconst std::string &Ref = Str; constexpr std::meta::info StrTy = type_of(^Str); constexpr std::meta::info RefTy = type_of(^Ref);
What are StrTy
and
RefTy
? This question is more
difficult. Two distinct issues complicate the answer:
Our experience using these facilities has consistently shown that
if StrTy
represents
std::string
,
many uses of StrTy
require writing
dealias(StrTy)
rather than using StrTy
directly
(because a reflection of a type aliases compares unequal with a
reflection of the aliased type). Failure to do so often yields subtle
bugs.
While we would like for RefTy
to represent const std::string &
,
it can only represent const std::basic_string<char, std::allocator<char>> &
.
Why? Because since
std::string
is only a “name” for std::basic_string<char, std::allocator<char>>
,
the language provides no semantic answer to what “const std::string &
”
is. It is only a source-level “grammatical” construct: A
type-id. Reflecting type-ids is a brittle path, since it opens
questions like whether a reflection of const int
is the same as a reflection of int const
.
Furthermore, nothing currently requires an implementation to “remember”
that the type of Ref
was “spelled”
with the alias
std::string
after parsing it, and we aren’t confident that all major implementations
do so today. Lastly, even if we could form a reflection of
const std::string &
,
our existing metafunction and type-trait “machinery” gives no means of
unwrapping the cv-ref qualification to get ^std::string
without decaying all the way to ^std::basic_string<char, std::allocator<char>>
.
In light of the above, our position is that
type_of
should never return aliases:
That is, StrTy
represents std::basic_string<char, std::allocator<char>>
.
We believe that it would be desirable to in the future introduce an
aliased_type_of
function capable of
returning representations of both
std::string
and const std::string &
for Str
and
Ref
respectively - but this requires
both discussions with implementers, and likely new wording technology
for the Standard. To avoid jeopardizing the goal declared by the title
of this paper, we are not proposing such a function at this time.
One of the most “obvious” abilities of reflection — retrieving the name of an entity — turns out to raise issues that aren’t obvious at all: How do we represent source text in a C++ program?
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() { ::cout << u8"こんにちは世界\n"; std}
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 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:
the internal representation used, e.g., in the compiler front end’s AST-like structures (persistent)
the representation of string literals in the AST (persistent)
the representation of array of character values during constant-evaluation (transient)
(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
identifier_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; ::cout << identifier_of(^hello_world) << "\n"; // Doesn't work if identifier_of produces a std::string. std}
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
identifier_of
do produce “string
view” results. For example:
consteval std::string_view identifier_of(info); consteval std::u8string_view identifier_of(info);
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 identifier_of(refl)
it is possible that the some source characters of the result are not
representable. We can then consider multiple options, including:
the query fails to evaluate,
any unrepresentable source characters are translated to a
different presentation, such as universal-character-names of the form
\u{ hex-number }
,
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 literal encoding.
Earlier revisions of this proposal (and its predecessor, [P1240R2]) included a metafunction called
name_of
, which we defined to return
a string_view
containing the “name”
of the reflected entity. As the paper evolved, it became necessary to
sharpen the specification of what this “name” contains. Subsequent
revisions (beginning with P2996R2, presented in Tokyo) specified that
name_of
returns the unqualified
name, whereas a new
qualified_name_of
would give the
fully qualified name.
Most would agree that qualified_name_of(^size_t)
might reasonably return "std::size_t"
,
or that qualified_name_of(^std::any::reset)
could return "std::any::reset"
.
But what about for local variables, or members of local classes? Should
inline and anonymous namespaces be rendered as a part of the qualified
name? Should we standardize the spelling of such scopes, or leave it
implementation defined?
The situation is possibly even less clear for unqualified names.
Should cv-qualified types be rendered as const int
or int const
?
Should the type for a function returning a pointer be rendered as
T *(*)()
,
T* (*)()
,
or T * (*)()
?
Should such decisions be standardized, or left to implementations? But
the real kicker is when one considers non-type template arguments, which
can (and do) contain arbitrarily complex values of arbitrary structural
types (along with any complete object, or subobject thereof, which has
static storage duration).
The more that we tried to specify formatting behavior for just the
unqualified names of arbitrary types, the more convinced we became that
this did not feel like an algorithm that should be frozen in the
standard library - at least, not at this time. There are just too many
toggles that a programmer might reasonably want to flip (one need only
look at Clang’s
PrettyPrinter
class for
inspiration). On the other hand, it is perfectly reasonable to ask that
implementations give some means of describing what it is that a
reflection contains - that is exactly the purpose of the
display_string_of
function.
Our stance is therefore that reflection pretty printers, for now,
should be left to organically develop within the ecosystem of
open-source C++ libraries. To ensure that this is possible, the
Clang/P2996 fork has implemented its
display_string_of
metafunction
entirely within the library. It is capable of printing type names, value
representations, template arguments, and much more. Best of all, it can
be extended without modifying the compiler.
What of name_of
and
qualified_name_of
? As of the R5
revision of this paper, we have removed them. In their stead is
identifier_of
, which is only a
constant expression if the name of the represented construct is an
identifier, and has_identifier
for
checking this condition. A few other metafunctions fill in some gaps:
operator_of
determines the identity
of an overloaded operator, and predicates like
is_operator_function
and
is_conversion_function_template
let
printing libraries handle those unqualified names that are not
identifiers. parent_of
supports
walking up the chain of functions, namespaces, and classes enclosing the
declaration of an entity, thus enabling homegrown implementations of
qualified_name_of
. Meanwhile, the
prime real estate of name_of
remains
available for future library extensions.
As a nice side-effect, the
identifier_of
model altogether
dodges some contentious questions that arose during LEWG discussions in
St Louis: Should asking the “name” of an anonymous entity (e.g.,
anonymous unions) return the empty string, or fail to be a constant
expression? Since the C++ grammar requires that an
identifier
contain at least
one character, the identifier_of
function never returns an empty string: it is seen that the only
possibility is to fail to be a constant expression.
Certain metafunctions (e.g.,
members_of
) return reflections that
represent entities without ever naming those entities in source code
(i.e., eliding lookup). Although it is often clear which entities should
be returned from the perspective of a reader, or even the perspective of
an implementation, core wording has no notion that directly corresponds
to “compilation state”.
Lookup is rather defined in terms of “reachability”, which is roughly a mapping from a “program point” to the set of declarations reachable from that point. Lookup frequently occurs from a single point, but template instantiation (and a few other niche circumstances) can lead to lookup taking place from multiple points (i.e., the point in a template from which a name is specified, and the point from which the template was instantiated). The set of points from which lookup takes place is the instantiation context ([module.context]).
template <typename T> int fn() { return /*P1*/ T::value; } struct S { static const int value = 42; } int main() { return /*P2*/ fn<S>(); } // The instantiation context when looking up 'S::value' in 'fn<T>' is {P1, P2}. // Even though 'S' is not found from P1, it is found from P2; lookup succeeds.
This works because the notion of template instantiation is baked into
the definition of “instantiation context”, which is thereafter used to
define lookup. But we have no such benefit in the case of metafunctions
like members_of
, which do not
utilize template instantiation.
consteval size_t count_fields(std::meta::info Ty) { return /*P1*/ nonstatic_data_members_of(Ty).size(); } struct S { int i, j, k; } static_assert(/*P2*/ count_fields(^S) == 3);
If we naively define
nonstatic_data_members_of
to return
members reachable from the “point of call”, then the above code would
fail: after all, S
is not reachable
from P1
. We instead must
define the declarations to be those reachable from where constant
evaluation begins (i.e.,
P2
). We encode this idea in
our definition of the evaluation context:
22 During the evaluation of a manifestly constant-evaluated expression
M
, the evaluation context of an expressionE
comprises […] the instantiation context ofM
([module.context]), […] .
This gives the tool needed to define the declarations returned by
members_of
to be (roughly) those
reachable from the evaluation context. However, a second
problem related to reachability is posed by
define_class
.
consteval std::meta::info make_defn(std::meta::info Cls, std::meta::info Mem) { // Synthesizes: // struct Mem {}; // struct Cls { Mem m; }; return /*P1*/ define_class(Cls, { (/*P2*/ define_class(Mem, {}), {.name="m"}) data_member_spec}); } /* P3*/ struct C; /* P4*/ struct M; static_assert(/*P5*/ is_type(make_defn(^C, ^M)) /*P6*/); /*P7*/ C obj;
Although we want this code to be valid, we have several obstacles to navigate.
C
and
M
be defined from
P1
and
P2
when no declarations of
those classes are reachable from those program points?C
and
M
(i.e., from what program points
will the generated definitions be reachable)?M
is reachable during the evaluation
of define_class
on
C
?The prior discourse regarding
members_of
gives a straightforward
answer to (1); the define_class
function is defined in terms of the evaluation context, which
makes available all declarations reachable from
P5
.
An answer to (2) can be seen by considering the declarations at
P3
,
P4
, and
P7
: Since we want the
declaration of obj
to be
well-formed, the generated definition of
C
must precede
P7
. On the other hand,
placing the definition of C
prior to P4
would weirdly
place the definition of the class C
,
which contains a data memer of type
M
, prior to the declaration of
M
itself. We propose that the point
of declaration for all definitions generated by
define_class
immediately follows the
end of the manifestly constant-evaluated expression that produces the
definition: In this case, just prior to
P6
.
This leaves one gap, and it is the question posed by (3): If the
definition of M
, generated by
evaluation of define_class(Mem, {})
,
is located just prior to
P6
, then the definition is
still not reachable from the evaluation context (such as we have defined
it) during evaluation of define_class(Cls, ...)
.
Circling back to “reachability” as a mapping from program points to declarations, there are two clear paths forward: Either modify which declarations are reachable from a program point, or modify the set of program points in the evaluation context. We choose the later approach, and attempt to provide some machinery that can be reused for future “generative reflection” proposals.
We begin by specially indicating that the generated definitions of
C
and
M
are not just declarations, but
injected declarations, and that such injected declarations are
produced by an evaluation of an expression. The reachability of
these declarations is evidently different from other declarations: It
depends not only on a program point, but also on which compile-time
evaluations of expressions (which have no relation to lexical ordering)
are sequenced after the production of the injected
declarations.
To bridge the world of program points to the world of sequenced
evaluations, we introduce a notion dual to “injected declarations”: For
every injected declaration, there is a corresponding injected
point. Injected points have a special property: the only
declaration reachable from an injected point is its corresponding
injected declaration. Jumping back to our above example, joining the
injected point of the injected declaration of
M
to our evaluation context gives
exactly what is needed for M
to be
usable during the definition of C
.
More precisely: M
is reachable
during the definition of C
because
the evaluation of the expression that produces the definition of
M
is sequenced before the
evalauation of the expression that produces
C
. This is captured by our full and
final definition of the evaluation context:
22 The evaluation context is a set of points within the program that determines which declarations are found by certain expressions used for reflection. During the evaluation of a manifestly constant-evaluated expression
M
, the evaluation context of an expressionE
comprises the union of
Lastly, we clarify that during the definition of an injected
declaration, the instantiation context consists of the
evaluation context of the expression that is producing the
declaration. In our example above, this ensures that the definition of
M
is reachable not just
from the invocation of define_class
for C
, but from within the actual
generated definition of
C
.
This machinery is “off in the weeds” of technicalities related to modules, lookup, etc., but we believe (hope?) that it provides a sound basis upon which to build generative reflection within the framework provided by core language wording: not only for P2996, but for future papers as well.
Several important metafunctions, such as std::meta::nonstatic_data_members_of
,
return a
std::vector
value. Unfortunately, that means that they are currently not usable in a
freestanding environment, but [P3295R0] currently proposes freestanding
std::vector
,
std::string
,
and std::allocator
in
constant evaluated contexts, explicitly to make the facilities proposed
by this paper work in freestanding.
Here is a synopsis for the proposed library API. The functions will be explained below.
namespace std::meta { using info = decltype(^::); template <typename R> concept reflection_range = /* see above */; // name and location consteval auto identifier_of(info r) -> string_view; consteval auto u8identifier_of(info r) -> u8string_view; consteval auto display_string_of(info r) -> string_view; consteval auto u8display_string_of(info r) -> u8string_view; consteval auto source_location_of(info r) -> source_location; // type queries consteval auto type_of(info r) -> info; consteval auto parent_of(info r) -> info; consteval auto dealias(info r) -> info; // object and value queries consteval auto object_of(info r) -> info; consteval auto value_of(info r) -> info; // template queries consteval auto template_of(info r) -> info; consteval auto template_arguments_of(info r) -> vector<info>; // member queries consteval auto members_of(info r) -> vector<info>; consteval auto bases_of(info type_class) -> 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 enumerators_of(info type_enum) -> vector<info>; consteval auto get_public_members(info type) -> vector<info>; consteval auto get_public_static_data_members(info type) -> vector<info>; consteval auto get_public_nonstatic_data_members(info type) -> vector<info>; consteval auto get_public_bases(info type) -> vector<info>; // substitute template <reflection_range R = initializer_list<info>> consteval auto can_substitute(info templ, R&& args) -> bool; template <reflection_range R = initializer_list<info>> consteval auto substitute(info templ, R&& args) -> info; // reflect_invoke template <reflection_range R = initializer_list<info>> consteval auto reflect_invoke(info target, R&& args) -> info; template <reflection_range R1 = initializer_list<info>, reflection_range R2 = initializer_list<info>> consteval auto reflect_invoke(info target, R1&& tmpl_args, R2&& args) -> info; // reflect expression results template <typename T> consteval auto reflect_value(T value) -> info; template <typename T> consteval auto reflect_object(T& value) -> info; template <typename T> consteval auto reflect_function(T& value) -> info; // extract
template <typename T> consteval auto extract(info) -> T; // other type predicates (see the wording) consteval auto is_public(info r) -> bool; consteval auto is_protected(info r) -> bool; consteval auto is_private(info r) -> bool; consteval auto is_virtual(info r) -> bool; consteval auto is_pure_virtual(info r) -> bool; consteval auto is_override(info r) -> bool; consteval auto is_final(info r) -> bool; consteval auto is_deleted(info r) -> bool; consteval auto is_defaulted(info r) -> bool; consteval auto is_explicit(info r) -> bool; consteval auto is_noexcept(info r) -> bool; consteval auto is_bit_field(info r) -> bool; consteval auto is_enumerator(info r) -> bool; consteval auto is_const(info r) -> bool; consteval auto is_volatile(info r) -> bool; consteval auto is_mutable_member(info r) -> bool; consteval auto is_lvalue_reference_qualified(info r) -> bool; consteval auto is_rvalue_reference_qualified(info r) -> bool; consteval auto has_static_storage_duration(info r) -> bool; consteval auto has_thread_storage_duration(info r) -> bool; consteval auto has_automatic_storage_duration(info r) -> bool; consteval auto has_internal_linkage(info r) -> bool; consteval auto has_module_linkage(info r) -> bool; consteval auto has_external_linkage(info r) -> bool; consteval auto has_linkage(info r) -> bool; consteval auto is_class_member(info r) -> bool; consteval auto is_namespace_member(info r) -> bool; consteval auto is_nonstatic_data_member(info r) -> bool; consteval auto is_static_member(info r) -> bool; consteval auto is_base(info r) -> bool; consteval auto is_data_member_spec(info r) -> bool; consteval auto is_namespace(info r) -> bool; consteval auto is_function(info r) -> bool; consteval auto is_variable(info r) -> bool; consteval auto is_type(info r) -> bool; consteval auto is_type_alias(info r) -> bool; consteval auto is_namespace_alias(info r) -> bool; consteval auto is_complete_type(info r) -> bool; consteval auto has_complete_definition(info r) -> bool; consteval auto is_template(info r) -> bool; consteval auto is_function_template(info r) -> bool; consteval auto is_variable_template(info r) -> bool; consteval auto is_class_template(info r) -> bool; consteval auto is_alias_template(info r) -> bool; consteval auto is_conversion_function_template(info r) -> bool; consteval auto is_operator_function_template(info r) -> bool; consteval auto is_literal_operator_template(info r) -> bool; consteval auto is_constructor_template(info r) -> bool; consteval auto is_concept(info r) -> bool; consteval auto is_structured_binding(info r) -> bool; consteval auto is_value(info r) -> bool; consteval auto is_object(info r) -> bool; consteval auto has_template_arguments(info r) -> bool; consteval auto has_default_member_initializer(info r) -> bool; consteval auto is_special_member_function(info r) -> bool; consteval auto is_conversion_function(info r) -> bool; consteval auto is_operator_function(info r) -> bool; consteval auto is_literal_operator(info r) -> bool; consteval auto is_constructor(info r) -> bool; consteval auto is_default_constructor(info r) -> bool; consteval auto is_copy_constructor(info r) -> bool; consteval auto is_move_constructor(info r) -> bool; consteval auto is_assignment(info r) -> bool; consteval auto is_copy_assignment(info r) -> bool; consteval auto is_move_assignment(info r) -> bool; consteval auto is_destructor(info r) -> bool; consteval auto is_user_provided(info r) -> bool; consteval auto is_user_declared(info r) -> bool; // define_class struct data_member_options_t; consteval auto data_member_spec(info type_class, = {}) -> info; data_member_options_t options template <reflection_range R = initializer_list<info>> consteval auto define_class(info type_class, R&&) -> info; // define_static_string and define_static_array consteval auto define_static_string(string_view str) -> const char *; consteval auto define_static_string(u8string_view str) -> const char8_t *; template <ranges::input_range R> consteval auto define_static_array(R&& r) -> span<ranges::range_value_t<R> const>; // data layout struct member_offsets { size_t bytes; size_t bits; constexpr auto total_bits() const -> size_t; auto operator<=>(member_offsets const&) const = default; }; consteval auto offset_of(info r) -> member_offsets; consteval auto size_of(info r) -> size_t; consteval auto alignment_of(info r) -> size_t; consteval auto bit_size_of(info r) -> size_t; }
identifier_of
,
display_string_of
,
source_location_of
namespace std::meta { consteval auto identifier_of(info) -> string_view; consteval auto u8identifier_of(info) -> u8string_view; consteval auto display_string_of(info) -> string_view; consteval auto u8display_string_of(info) -> u8string_view; consteval auto has_identifier(info) -> bool; consteval auto source_location_of(info r) -> source_location; }
Given a reflection r
representing
a language construct X
whose
declaration introduces an identifier, and if that identifier is
representable using the ordinary literal encoding, then identifier_of(r)
returns a non-empty string_view
containing that identifier. Otherwise, it is not a constant expression.
Whether a reflected construct has an identifier can be checked with the
has_identifier
metafunction.
The function u8identifier_of
returns the same identifier but as a
u8string_view
. Note that since all
identifiers can be represented as UTF-8 string literals,
u8identifier_of
never fails to be a
constant expression because of representability concerns.
Given any reflection r
, display_string_of(r)
and u8display_string_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 (note: as an exercise, the Clang
implementation of this proposal implements a pretty-printing
display_string_of
as
a non-intrinsic library function).
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.
type_of
,
parent_of
,
dealias
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):
consteval auto type_doof(std::meta::info r) -> std::meta::info { return type_remove_cvref(is_type(r) ? r : type_of(r)); } #define typeof(e) [: type_doof(^e) :]
parent_of(r)
is a reflection designating its immediately enclosing class, function,
or (possibly inline or anonymous) namespace.
If r
represents an alias, dealias(r)
represents 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);
object_of
,
value_of
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.
template_of
,
template_arguments_of
namespace std::meta { consteval auto template_of(info r) -> info; 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:
::vector<int> v = {1, 2, 3}; stdstatic_assert(template_of(type_of(^v)) == ^std::vector); static_assert(template_arguments_of(type_of(^v))[0] == ^int);
members_of
,
static_data_members_of
,
nonstatic_data_members_of
,
bases_of
,
enumerators_of
namespace std::meta { consteval auto members_of(info r) -> vector<info>; consteval auto bases_of(info type_class) -> 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 enumerators_of(info type_enum) -> vector<info>; consteval auto get_public_members(info type_class) -> vector<info>; consteval auto get_public_static_data_members(info type_class) -> vector<info>; consteval auto get_public_nonstatic_data_members(info type_class) -> vector<info>; consteval auto get_public_bases(info type_class) -> vector<info>; }
The template members_of
returns a
vector of reflections representing the direct members of the class type
or namespace 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.
The template bases_of
returns the
direct base classes of the class type represented by its first argument,
in declaration order.
static_data_members_of
and
nonstatic_data_members_of
return
reflections of the static and non-static data members, in order,
respectively.
enumerators_of
returns the
enumerator constants of the indicated enumeration type in declaration
order.
The get_public_meow
functions are
equivalent to meow_of
functions
except that they additionally filter the results on those members for
which is_public(member)
is true
. The
only other distinction is that
members_of
can be invoked on a
namespace, while get_public_members
can only be invoked on a class type (because it does not make sense to
ask for the public members of a namespace). This set of functions has a
distinct API by demand for ease of grepping.
substitute
namespace std::meta { template <reflection_range R = initializer_list<info>> consteval auto can_substitute(info templ, R&& args) -> bool; template <reflection_range R = initializer_list<info>> consteval auto substitute(info templ, R&& args) -> info; }
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:
constexpr auto r = substitute(^std::vector, std::vector{^int}); using T = [:r:]; // Ok, T is std::vector<int>
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:
template<typename T> struct S { typename T::X x; }; constexpr auto r = substitute(^S, std::vector{^int}); // Okay. typename[:r:] si; // Error: T::X is invalid for T = int.
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.
reflect_invoke
namespace std::meta { template <reflection_range R = initializer_list<info>> consteval auto reflect_invoke(info target, R&& args) -> info; template <reflection_range R1 = initializer_list<info>, reflection_range R2 = initializer_list<info>> consteval auto reflect_invoke(info target, R1&& tmpl_args, R2&& args) -> info; }
These metafunctions produce a reflection of the result of a call expression.
For the first overload: Letting F
be the entity represented by target
,
and A0, A1, ..., AN
be the sequence of entities represented 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.
reflect_value
,
reflect_object
,
reflect_function
namespace std::meta { template<typename T> consteval auto reflect_value(T expr) -> info; template<typename T> consteval auto reflect_object(T& expr) -> info; 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.
static_assert(substitute(^std::array, {^int, std::meta::reflect_value(5)}) ==
^std::array<int, 5>);
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.
template <int &> void fn();
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.
consteval bool is_global_with_external_linkage(void(*fn)()) {
::meta::info rfn = std::meta::reflect_function(*fn);
std
return (has_external_linkage(rfn) && parent_of(rfn) == ^::);
}
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
function of type F
, extract<F*>(r)
evaluates to a pointer to that function.
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.
For other reflection values r
,
extrace<T>(r)
is ill-formed.
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 represented by
its operand.
data_member_spec
,
define_class
namespace std::meta { struct data_member_options_t { struct name_type { template <typename T> requires constructible_from<u8string, T> consteval name_type(T &&); template <typename T> requires constructible_from<string, T> consteval name_type(T &&); }; <name_type> name; optional<int> alignment; optional<int> width; optionalbool no_unique_address = false; }; consteval auto data_member_spec(info type, = {}) -> info; data_member_options_t options template <reflection_range R = initializer_list<info>> consteval auto define_class(info type_class, R&&) -> info; }
data_member_spec
returns a
reflection of a description of a declaration of a data member of given
type. Optional alignment, bit-field-width, 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 code-units. 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, { (^int), data_member_spec(^char), data_member_spec(^double), data_member_spec}))); // U is now defined to the equivalent of // union U { // int _0; // char _1; // double _2; // }; template<typename T> struct S; constexpr auto s_int_refl = define_class(^S<int>, { (^int, {.name="i", .alignment=64}), data_member_spec(^int, {.name=u8"こんにち"}), data_member_spec}); // S<int> is now defined to the equivalent of // template<> struct S<int> { // alignas(64) int i; // int こんにち; // };
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.
define_static_string
,
define_static_array
namespace std::meta { consteval auto define_static_string(string_view str) -> const char *; consteval auto define_static_string(u8string_view str) -> const char8_t *; template <ranges::input_range R> consteval auto define_static_array(R&& r) -> span<ranges::range_value_t<R> const>; }
Given a string_view
or
u8string_view
,
define_static_string
returns a
pointer to an array of characters containing the contents of
str
followed by a null terminator.
The array object has static storage duration, is not a subobject of a
string literal object, and is usable in constant expressions; a pointer
to such an object meets the requirements for use as a non-type template
argument.
template <const char *P> struct C { }; const char msg[] = "strongly in favor"; // just an idea.. <msg> c1; // ok C<"nope"> c2; // ill-formed C<define_static_string("yay")> c3; // ok C
In the absence of general support for non-transient constexpr allocation, such a facility is essential to building utilities like pretty printers.
An example of such an interface might be built as follow:
template <std::meta::info R> requires is_value(R) consteval auto render() -> std::string; template <std::meta::info R> requires is_type(R) consteval auto render() -> std::string; template <std::meta::info R> requires is_variable(R) consteval auto render() -> std::string; // ... template <std::meta::info R> consteval auto pretty_print() -> std::string_view { return define_static_string(render<R>()); }
This strategy lies
at the core of how the Clang/P2996 fork builds its example
implementation of the
display_string_of
metafunction.
define_static_array
is a more
general version of
define_static_string
that works for
all types. The difference between the two is that
define_static_string
produces a
null-terminated array, and thus returns just a pointer, while
define_static_array
produces an
array that is the same size as the input range.
Technically, define_static_array
can be used to implement
define_static_string
:
consteval auto define_static_string(string_view str) -> char const* { return define_static_array(views::concat(str, views::single('\0'))).data(); }
But that’s a fairly awkward implementation, and the string use-case is sufficiently common as to merit a more ergonomic solution.
namespace std::meta { struct member_offsets { size_t bytes; size_t bits; constexpr auto total_bits() const -> size_t { return CHAR_BIT * bytes + bits; } auto operator<=>(member_offsets const&) const = default; }; consteval auto offset_of(info r) -> member_offsets; consteval auto size_of(info r) -> size_t; consteval auto alignment_of(info r) -> size_t; consteval auto bit_size_of(info r) -> 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 - in bytes and then leftover bits (always between
0
and
7
inclusive).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
gives the size of a
base class subobject or non-static data member, except in bits.struct Msg { uint64_t a : 10; uint64_t b : 8; uint64_t c : 25; uint64_t d : 21; }; static_assert(offset_of(^Msg::a) == member_offsets{0, 0}); static_assert(offset_of(^Msg::b) == member_offsets{1, 2}); static_assert(offset_of(^Msg::c) == member_offsets{2, 2}); static_assert(offset_of(^Msg::d) == member_offsets{5, 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); static_assert(offset_of(^Msg::a).total_bits() == 0); static_assert(offset_of(^Msg::b).total_bits() == 10); static_assert(offset_of(^Msg::c).total_bits() == 18); static_assert(offset_of(^Msg::d).total_bits() == 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
|
---|---|
|
|
|
|
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:
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.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
.
Static reflection invariably brings new ways to violate ODR.
// File 'cls.h'
struct Cls {
void odr_violator() {
if constexpr (members_of(parent_of(^std::size_t)).size() % 2 == 0)
();
branch_1else
();
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_complete_type(^T)
,
if one translation unit
#include
s a
forward declaration of T
, another
#include
s 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.
[ Editor's note:
Throughout the wording, we say that a reflection (an object of type
std::meta::info
)
represents some source construct, while splicing that
reflection designates that source construct. For instance,
^int
represents the type
int
and
[: ^int :]
designates the type
int
. ]
Modify the wording for phases 7-8 of 5.2 [lex.phases] as follows:
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 exactly once. […]
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 exactly once. […]
Add a bullet after 5.4 [lex.pptoken] bullet (3.2):
…
— 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
Modify paragraph 4.1 to cover splicing of functions:
- (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 designated 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]).
Modify the first sentence of paragraph 5 to cover splicing of variables:
- 5 A variable is named by an expression if the expression is an id-expression or splice-expression ([expr.prim.splice]) that designates it.
Modify paragraph 6 to cover splicing of structured bindings:
- 6 A structured binding is odr-used if it appears as a potentially-evaluated expression, or if a reflection of it is the operand of a potentially-evaluated splice-expression ([expr.prim.splice]).
Prepend before paragraph 15 of 6.3 [basic.def.odr]:
15pre If a class
C
is defined in a translation unit with a call to a specialization ofstd::meta::define_class
, every definition of that class shall be the result of a call to the same specialization; and for every reflection in the range of reflections describing its class members and unnamed bit-fields, every other such call shall have a corresponding value, occupying the same position in its respective range, to which the reflection compares equal.15 Otherwise, for
Forany definable item D with definitions …
Modify the first bullet of paragraph 3 of 6.5.4 [basic.lookup.argdep] as follows:
3 … Any
typedef-name
s andusing-declaration
s used to specify the types do not contribute to this set. The set of entities is determined in the following way:
FIXME. Have to handle splices in here, because they’re not actually
“component names”. Now
splice-namespace-qualifier
is only a namespace too.
Extend 6.5.5.1 [basic.lookup.qual.general]/1-2
to cover
splice-namespace-qualifier
:
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
,orcomputed-type-specifier
, orsplice-namespace-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.2 A member-qualified name is the (unique) component name ([expr.prim.id.unqual]), if any, of
- (2.1) an unqualified-id or
- (2.2) a
nested-name-specifier
of the formtype-name ::
or,namespace-name ::
, orsplice-namespace-qualifier ::
in the id-expression of a class member access expression ([expr.ref]). […]
Add a bullet to paragraph 13:
13 A declaration
D
names an entityE
if
- (13.1)
D
contains a lambda-expression whose closure type isE
,- (13.1+)
D
contains an expression that represents eitherE
or atypedef-name
ornamespace-alias
that denotesE
,- (13.2)
E
is not a function or function template andD
contains an id-expression, type-specifier, nested-name-specifier, template-name, or concept-name denotingE
, or- (13.3)
E
is a function or function template andD
contains an expression that namesE
([basic.def.odr]) or an id-expression that refers to a set of overloads that containsE
.
[ Editor's note: The below addition of “value or object of a TU-local type” is in part a drive-by fix to make sure that enumerators in a TU-local enumeration are also TU-local ]
Extend the definition of TU-local values and objects to include reflections:
16 A value or object is TU-local if either
- (16.1) it is, or is a pointer to, a TU-local function or the object associated with a TU-local variable,
or
- (16.2) it is an object of class or array type and any of its subobjects or any of the objects or functions to which its non-static data members of reference type refer is TU-local and is usable in constant expressions.
Change the first sentence in paragraph 9 of 6.8.1 [basic.types.general] as follows:
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.
Add a new paragraph at the end of 6.8.1 [basic.types.general] as follows:
* A consteval-only type is one of the following:
std::meta::info
, or- a pointer or reference to a consteval-only type, or
- an (possibly multi-dimensional) array of a consteval-only type, or
- a class type with a base class or non-static data member of consteval-only type, or
- a function type with a return type or parameter type of consteval-only type, or
- a pointer-to-member type to a class
C
of typeM
where eitherC
orM
is a consteval-only type.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 for which every expression that names the variable is within an immediate function context.
Add a new paragraph before the last paragraph of 6.8.2 [basic.fundamental] as follows:
17 - 1 A value of type
std::meta::info
is called a reflection. There exists a unique null reflection; every other reflection is a representation of
- a value with structural type ([temp.param]),
- an object with static storage duration,
- a variable,
- a structured binding,
- a function,
- an enumerator,
- a type,
- a
typedef-name
,- a class member,
- a bit-field,
- a primary class template, function template, primary variable template, alias template, or concept,
- a namespace or namespace alias,
- a base class specifier, or
- a description of a declaration of a non-static data member.
An expression convertible to a reflection is said to represent the corresponding entity, alias, object, value, base class specifier, or description of a declaration of a non-static data member.
Change the grammar for
primary-expression
in
7.5 [expr.prim]
as follows:
primary-expression: literal this ( expression ) id-expression lambda-expression fold-expression requires-expression+ splice-expression
FIXME: The wording here, and usage throughout.
Add a new grammar term for convenience:
splice-specifier: [: constant-expression :]
1 The
constant-expression
of asplice-specifier
shall be a converted constant expression ([expr.const]) contextually convertible tostd::meta::info
.2 Let
E
be the value of the convertedconstant-expression
. Thesplice-specifier
designates whatE
represents.3 A
splice-specifier
is dependent if the convertedconstant-expression
is value-dependent.
Add a carve-out for reflection in 7.5.5.1 [expr.prim.id.general]/4:
4 An
id-expression
that denotes a non-static data member or implicit object member function of a class can only be used:
- (4.1) as part of a class member access (after any implicit transformation (see above)) in which the object expression refers to the member’s class or a class derived from that class, or
- (4.2) as an operand to the reflection operator ([expr.reflect]), or
Add a production to the grammar for
nested-name-specifier
as
follows:
nested-name-specifier: :: type-name :: namespace-name ::+ splice-namespace-qualifier :: computed-type-specifier :: nested-name-specifier identifier :: nested-name-specifier templateopt simple-template-id ::+ + splice-namespace-qualifier: + splice-specifier
Add a new paragraph restricting
splice-namespace-qualifier
,
and renumber accordingly:
0 The
splice-specifier
of asplice-namespace-qualifier
shall designate a namespace or namespace alias.1 The component names of a
qualified-id
are […]
Clarify that a splice cannot appear in a declarative
nested-name-specifier
:
2 A
nested-name-specifier
is declarative if it is part of
- a
class-head-name
,- an
enum-head-name
,- a
qualified-id
that is theid-expression
of adeclarator-id
, or- a declarative
nested-name-specifier
.A declarative
nested-name-specifier
shall not have adecltype-specifier
or asplice-specifier
. A declaration that uses a declarativenested-name-specifier
shall be a friend declaration or inhabit a scope that contains the entity being redeclared or specialized.
Extend the next paragraph to also cover splices, and prefer the verb “designate” over “nominate”:
4 The
nested-name-specifier
::
nominatesdesignates the global namespace. Anested-name-specifier
with acomputed-type-specifier
nominatesdesignates the type denoted by thecomputed-type-specifier
, which shall be a class or enumeration type. Anested-name-specifier
with asplice-namespace-qualifier
nominatesdesignates the same namespace or namespace alias as thesplice-namespace-qualifier
. If anested-name-specifier
N is declarative and has asimple-template-id
with a template argument list A that involves a template parameter, let T be the templatenominateddesignated by N without A. T shall be a class template.…
Add a new subsection of 7.5 [expr.prim] following 7.5.8 [expr.prim.req]
Expression Splicing [expr.prim.splice]
FIXME: text for the template version.
splice-expression: splice-specifier template splice-specifier < template-argument-listopt >
1 The
splice-specifier
shall not designate an unnamed bit-field, a constructor or destructor, or a constructor template or destructor template.2 For a
splice-expression
of the formsplice-specifier
, letE
be the object, value, or entity designated bysplice-specifier
.
(2.1) If
E
is an object, a function, or a non-static data member, the expression is an lvalue designatingE
. The expression has the same type asE
, and is a bit-field if and only ifE
is a bit-field.(2.2) Otherwise, if
E
is a variable or a structured binding, the expression is an lvalue designating the same object asE
. The expression has the same type asE
, and is a bit-field if and only ifE
is a bit-field.(2.3) Otherwise,
E
shall be a value or an enumerator. The expression is a prvalue whose evaluation computesE
and whose type is the same asE
.[ Note 1: Access checking of class members occurs during lookup, and therefore does not pertain to splicing. — end note ]
Add a production to
postfix-expression
for
splices in member access expressions:
[1]{.pnum} Postfix expressions group left-to-right. postfix-expression: ... postfix-expression . templateopt id-expression+ postfix-expression . templateopt splice-expression postfix-expression -> templateopt id-expression+ postfix-expression -> templateopt splice-expression
Modify paragraph 1 to account for splices in member access expressions:
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 designating a class member, is a postfix expression. [ Note 1: If the keywordtemplate
is used, the following unqualified name is considered to refer to a template ([temp.names]). If asimple-template-id
results and is followed by a::
, the id-expression or splice-expression is a qualified-id. — end note ]
Modify paragraph 2 to account for splices in member access expressions:
2 For the first option, if the dot is followed by an
id-expression
namesorsplice-expression
designating a static member or an enumerator, the first expression is a discarded-value expression ([expr.context]); if theid-expression
orsplice-expression
designatesnamesa 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 expressionE1->E2
is converted to the equivalent form(*(E1)).E2
; the remainder of [expr.ref] will address only the first option (dot).
Modify paragraph 3 to account for splices in member access expressions:
3 The postfix expression before the dot is evaluated; the result of that evaluation, together with the
id-expression
orsplice-expression
, determines the result of the entire postfix expression.
Modify paragraph 4 to account for splices in member access expressions:
4 Abbreviating
postfix-expression
.id-expression
postfix-expression.EXPR
, whereEXPR
is theid-expression
orsplice-expression
following the dot, asE1.E2
,E1
is called theobject expression
. […]
Adjust the language in paragraphs 6-9 to account for splice-specifiers.
6 If
E2
isdesignates a bit-field,E1.E2
is a bit-field. […]7 If the entity designated by
E2
is declared to have type “reference toT
”, thenE1.E2
is an lvalue of typeT
. IfE2
isdesignates a static data member,E1.E2
designates the object or function to which the reference is bound, otherwiseE1.E2
designates the object or function to which the corresponding reference member ofE1
is bound. Otherwise, one of the following rules applies.
- (7.1) If
E2
isdesignates a static data member and the type ofE2
isT
, thenE1.E2
is an lvalue; […]- (7.2) If
E2
isdesignates a non-static data member and the type ofE1
is “cq1 vq1X
”, and the type ofE2
is “cq2 vq2T
”, […]. If the entity designated byE2
is declared to be amutable
member, then the type ofE1.E2
is “vq12T
”. If the entity designated byE2
is not declared to be amutable
member, then the type ofE1.E2
is “cq12 vq12T
”.[…]
(7.4) If
E
isdesignates a nested type, the expressionE1.E2
is ill-formed.(7.5) If
E2
isdesignates a member enumerator and the type ofE2
isT
, the expressionE1.E2
is a prvalue of typeT
whose value is the value of the enumerator.8 If
E2
isdesignates a non-static memberM
, the program is ill-formed if the class of whichE2
M
is directly a member is an ambiguous base ([class.member.lookup]) of the naming class ([class.access.base]) ofE2
M
.9 If the entity designated by
E2
is a non-static member and the result ofE1
is an object whose type is not similar ([conv.qual]) to the type ofE1
, the behavior is undefined.
Change 7.6.2.1 [expr.unary.general] paragraph 1 to add productions for the new operator:
1 Expressions with unary operators group right-to-left.
unary-expression: ... delete-expression+ reflect-expression
Modify paragraphs 3 and 4 to permit forming a pointer-to-member with a splice.
3 The operand of the unary
&
operator shall be an lvalue of some typeT
.
(3.1) If the operand is a
qualified-id
orsplice-expression
namingdesignating a non-static or variant member of some classC
, other than an explicit object member function, the result has type “pointer to member of classC
of typeT
” and designatesC::m
.(3.2) Otherwise, the result has type “pointer to
T
” and points to the designated object (6.7.1 [intro.memory]) or function (6.8.4 [basic.compound]). If the operand designates an explicit object member function (9.3.4.6 [dcl.fct]), the operand shall be aqualified-id
or asplice-expression
.4 A pointer to member is only formed when an explicit
&
is used and its operand is aqualified-id
orsplice-expression
not enclosed in parentheses.
Add a new subsection of 7.6.2 [expr.unary] following 7.6.2.9 [expr.delete]
The Reflection Operator [expr.reflect]
FIXME:
template-name
andid-expression
can both refer to template names, have to handle this better. See wording in the template argument parsing section.reflect-expression: ^ :: ^ nested-name-specifieropt namespace-name ^ nested-name-specifieropt template-name ^ nested-name-specifieropt concept-name ^ type-id ^ id-expression
1 The unary
^
operator, called the reflection operator, yields a prvalue of typestd::meta::info
(6.8.2 [basic.fundamental]).2 A reflect-expression is parsed as the longest possible sequence of tokens that could syntactically form a reflect-expression.
[ Example 1:— end example ]static_assert(is_type(^int())); // ^ applies to the type-id "int()" template<bool> struct X {}; bool operator<(std::meta::info, X<false>); consteval void g(std::meta::info r, X<false> xv) { r == ^int && true; // error: ^ applies to the type-id "int&&" r == ^int & true; // error: ^ applies to the type-id "int&" r == (^int) && true; // OK r == ^int &&&& true; // OK ^X < xv; // error: < starts template argument list (^X) < xv; // OK }
4 When applied to
::
, the reflection operator produces a reflection for the global namespace. When applied to anamespace-name
, the reflection operator produces a reflection for the indicated namespace or namespace alias.5 When applied to a
template-name
, the reflection operator produces a reflection for the indicated template.6 When applied to a
concept-name
, the reflection operator produces a reflection for the indicated concept.7 When applied to a
typedef-name
, the reflection operator produces a reflection of the indicatedtypedef-name
. When applied to any othertype-id
, the reflection operator produces a reflection of the indicated type.8 When applied to an
id-expression
, the reflection operator produces a reflection as follows:
(8.1) When applied to an enumerator, the reflection operator produces a reflection of the enumerator designated by the operand.
(8.2) Otherwise, when applied to an overload set
S
, if the assignment ofS
to an invented variable of typeconst auto
(9.2.9.7.2 [dcl.type.auto.deduct]) would select a unique candidate functionF
fromS
, the result is a reflection ofF
. Otherwise, the expression^S
is ill-formed.(8.3) Otherwise, when applied to one of
- (8.3.1) a non-type template parameter of non-class and non-reference type or
- (8.3.2) a
pack-index-expression
of non-class and non-reference typethe reflection operator produces a reflection of the value computed by the operand.
(8.4) Otherwise, the reflection operator produces a reflection of the variable, function, or non-static member designated by the operand. The
id-expression
is not evaluated.[ Example 2:— end example ]template <typename T> void fn() requires (^T != ^int); template <typename T> void fn() requires (^T == ^int); template <typename T> void fn() requires (sizeof(T) == sizeof(int)); constexpr auto R = ^fn<char>; // OK constexpr auto S = ^fn<int>; // error: cannot reflect an overload set constexpr auto r = ^std::vector; // OK
Extend 7.6.10 [expr.eq]/2 to also handle
std::meta::info
:
2 The converted operands shall have arithmetic, enumeration, pointer, or pointer-to-member type, or
typeone of the typesstd::meta::info
orstd::nullptr_t
. The operators==
and!=
both yieldtrue
orfalse
, i.e., a result of typebool
. In each case below, the operands shall have the same type after the specified conversions have been applied.
Add a new paragraph between 7.6.10 [expr.eq]/5 and /6:
5 Two operands of type
std::nullptr_t
or one operand of typestd::nullptr_t
and the other a null pointer constant compare equal.* If both operands are of type
std::meta::info
, comparison is defined as follows:
- (*.1) If one operand is a null reflection value, then they compare equal if and only if the other operand is also a null reflection value.
- (*.2) Otherwise, if one operand represents a
template-id
referring to a specialization of an alias template, then they compare equal if and only if the other operand represents the sametemplate-id
([temp.type]).- (*.3) Otherwise, if one operand represents a namespace alias or a
typedef-name
, then they compare equal if and only if the other operand represents a namespace alias ortypedef-name
sharing the same name, declared within the same enclosing scope, and aliasing the same underlying entity.- (*.4) Otherwise, if one operand represents a value, then they compare equal if and only if the other operand represents a template-argument-equivalent value (13.6 [temp.type]).
- (*.5) Otherwise, if one operand represents an object, then they compare equal if and only if the other operand represents the same object.
- (*.6) Otherwise, if one operand represents an entity, then they compare equal if and only if the other operand represents the same entity.
- (*.7) Otherwise, if one operand represents a base class specifier, then they compare equal if and only if the other operand represents the same base class specifier.
- (*.8) Otherwise, both operands
O1
andO2
represent descriptions of declarations of non-static data members: LetC1
andC2
be invented class types such that eachCk
has a single non-static data member having the properties described byOk
. The operands compare equal if and only if the data members ofC1
andC2
would share the same type, name (if any),alignment-specifiers
(if any), width, and attributes.6 If two operands compare equal, the result is
true
for the==
operator andfalse
for the!=
operator. If two operands compare unequal, the result isfalse
for the==
operator andtrue
for the!=
operator. Otherwise, the result of each of the operators is unspecified.
Add a new paragraph prior to the definition of manifestly constant evaluated (7.7 [expr.const]/20), and renumber accordingly:
20 An expression or conversion is plainly constant-evaluated if it is:
- (20.1) a
constant-expression
, or- (20.2) the condition of a constexpr if statement (8.5.2 [stmt.if]),
- (20.3) the initializer of a
constexpr
(9.2.6 [dcl.constexpr]) orconstinit
(9.2.7 [dcl.constinit]) variable, or a subexpression thereof, or- (20.4) an immediate invocation, unless it
- (20.4.1) results from the substitution of template parameters
- during template argument deduction (13.10.3 [temp.deduct]),
- in a
concept-id
(13.3 [temp.names]), or- in a
requires-expression
(7.5.8 [expr.prim.req]), or- (20.4.2) is an initializer for a variable that is neither
constexpr
(9.2.6 [dcl.constexpr]) norconstinit
(9.2.7 [dcl.constinit]), or a subexpression thereof.[ Note 1: Plainly constant-evaluated expressions are evaluated exactly once, and that evaluation precedes any subsequent parsing (5.2 [lex.phases]). As detailed below, evaluations of such expressions are allowed to produce injected declarations. — end note ]
[ Example 1:— end example ]consteval bool cfn(int) { return true; } template <int V> requires (cfn(V)) // cfn(V) is not plainly constant-evaluated consteval int g() { if constexpr (cfn(V+1)) { // cfn(V+1) is plainly constant-evaluated return cfn(V+2); // cfn(V+2) is plainly constant-evaluated } else { return 0; } } constexpr bool b1 = !cfn(1); // !cfn(1) is plainly constant-evaluated const bool b2 = cfn(2); // cfn(2) is not plainly constant-evaluated
Modify the definition of manifestly constant-evaluated to leverage that of plainly constant-evaluated:
21 An expression or conversion is manifestly constant-evaluated if it is:
- (21.1) a
plainly constant-evaluated expression, orconstant-expression
- (21.3) an immediate invocation, or
- (21.4) the result of substitution into an atomic constraint expression to determine whether it is satisfied (13.5.2.3 [temp.constr.atomic]), or
- (21.5) the initializer for a variable that is usable in constant expressions or has constant initialization (6.9.3.2 [basic.start.static]).
[ Note 2: All plainly constant-evaluated expressions are manifestly constant-evaluated, but some manifestly constant-evaluated expressions (e.g., initializers that can require trial evaluations) are not plainly constant-evaluated. Such expressions are still evaluated during translation, but (unlike plainly constant-evaluated expressions) may be evaluated multiple times, and there are no constraints on the relative order of their evaluation. — end note ]
Add new paragraphs defining evaluation context, injected declaration, and injected point after the example following the definition of manifestly constant-evaluated, and renumber accordingly:
22 The evaluation of an expression
E
can introduce an injected declaration. For each such declarationD
, the injected point is a corresponding program point which follows the last non-injected point in the translation unit containingD
. The evaluation ofE
is said to produce the declarationD
.[ Note 13: Special rules concerning reachability apply to injected points ([module.reach]). — end note ]
23 The program is ill-formed if an injected declaration is produced by the evaluation of an expression
E
that is
- (23.1) a manifestly constant-evaluated expression that is not plainly constant-evaluated, or
- within the body of an immediate-escalating function
F
, unless[ Note 14: An immediate invocation within an immediate-escalating function is similar to a trial evaluation of a variable initializer: if it fails to be a constant expression, the implementation may be forced to evaluate it again. The above rule is intended to only permit evaluations to produce declarations when such evaluations can be guaranteed to only happen once. — end note ]
[ Example 2:— end example ]consteval bool make_decl(int); // calling 'make_decl(n)' produces a declaration template <int R> requires (make_decl(R)) bool tfn(); constexpr bool b1 = !make_decl(1); // OK, constexpr variable so this is plainly // constant evaluated bool b2 = !make_decl(2); // error: initializer !make_decl(42) produced // a declaration but is not plainly constant // evaluated constexpr bool b3 = tfn<3>(); // error: the invocation of make_decl(R) in the // requires clause produced a declaration but is // not plainly constant evaluated consteval int *not_constant() { (4); make_declreturn new int {}; } constexpr bool b4 = [] { int *p = not_constant(); // error: not_constant() produces a declaration // in an immediate-escalated function, but is // not plainly constant-evalauted. delete p; return true; }();
24 The evaluation context is a set of points within the program that determines which declarations are found by certain expressions used for reflection. During the evaluation of a manifestly constant-evaluated expression
M
, the evaluation context of an evaluationE
comprises the union of
Extend the grammar for
computed-type-specifier
as
follows:
computed-type-specifier: decltype-specifier pack-index-specifier+ splice-type-specifier
Add a new subsection of 9.2.9 [dcl.type] following 9.2.9.8 [dcl.type.class.deduct].
+ splice-type-specifier + typenameopt splice-specifier
1 The
typename
may be omitted only within a type-only context (13.8.1 [temp.res.general]).2 The
splice-specifier
shall designate a type. The type designated by thesplice-type-specifier
is the same type designated by thesplice-specifier
.
Change paragraphs 6-8 of 9.4.1 [dcl.init.general] [ Editor's note: No changes are necessary for value-initialization, which already forwards to zero-initialization for scalar types ]:
6 To zero-initialize an object or reference of type
T
means:
- (6.0) if
T
isstd::meta::info
, the object is initialized to a null reflection value;- (6.1) if
T
isaany other scalar type ([basic.types.general]), the object is initialized to the value obtained by converting the integer literal0
(zero) toT
;- (6.2) […]
7 To default-initialize an object of type
T
means:
- (7.1) If
T
is a (possibly cv-qualified) class type ([class]), […]- (7.2) If T is an array type, […]
- (7.*) If
T
isstd::meta::info
, the object is zero-initialized.- (7.3) Otherwise, no initialization is performed.
8 A class type
T
is const-default-constructible if default-initialization ofT
would invoke a user-provided constructor ofT
(not inherited from a base class) or if
- (8.1) […]
If a program calls for the default-initialization of an object of a const-qualified type
T
,T
shall bestd::meta::info
or a const-default-constructibleclasstype, or array thereof.9 To value-initialize an object of type T means: […]
Add a bullet to paragraph 9 of 9.3.4.6 [dcl.fct] to allow for reflections of abominable function types:
9 A function type with a cv-qualifier-seq or a ref-qualifier (including a type named by typedef-name ([dcl.typedef], [temp.param])) shall appear only as:
- (9.1) the function type for a non-static member function,
- (9.2) …
- (9.5) the type-id of a template-argument for a type-parameter ([temp.arg.type])
.,
- (9.6) the operand of a reflect-expression ([expr.reflect]).
Change paragraph 2 of 9.5.3 [dcl.fct.def.delete] to allow for reflections of deleted functions:
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
declarationExtend the grammar for
using-enum-declarator
as
follows:
using-enum-declaration: using enum using-enum-declarator ; using-enum-declarator: nested-name-specifieropt identifier nested-name-specifieropt simple-template-id+ splice-specifier
Modify paragraph 1 of 9.7.2 [enum.udecl] as follows:
1 A
using-enum-declarator
that is not asplice-specifier
names the set of declarations found by lookup (6.5.3 [basic.lookup.unqual], 6.5.5 [basic.lookup.qual]) for theusing-enum-declarator
. Theusing-enum-declarator
shall designate a non-dependent type with a reachableenum-specifier
.
Add a production to the grammar for
qualified-namespace-specifier
as follows:
namespace-alias: identifier namespace-alias-definition: namespace identifier = qualified-namespace-specifier qualified-namespace-specifier: nested-name-specifieropt namespace-name+ splice-specifier
Add the following prior to paragraph 1, and renumber accordingly:
0 If a
qualified-namespace-specifier
is asplice-specifier
, thesplice-specifier
shall designate a namespace or namespace alias; thequalified-namespace-specifier
designates the same namespace or namespace alias designated by thesplice-specifier
. Otherwise, thequalified-namespace-specifier
designates the namespace found by lookup (6.5.3 [basic.lookup.unqual], 6.5.5 [basic.lookup.qual]).
Prefer the verb “designate” for
qualified-namespace-specifiers
in the paragraph that immediately follows:
2 The
identifier
in anamespace-alias-definition
becomes anamespace-alias
and denotes the namespacedenoteddesignated by thequalified-namespace-specifier
.
Use
qualified-namespace-specifier
in the grammar for
using-directive
:
using-directive:- attribute-specifier-seqopt using namespace nested-name-specifieropt namespace-name + attribute-specifier-seqopt using namespace qualified-namespace-specifier
Add the following prior to the first paragraph of 9.8.4 [namespace.udir], and renumber accordingly:
0 The
qualified-namespace-specifier
shall neither contain a dependentnested-name-specifier
nor a dependentsplice-specifier
.1 A
using-directive
shall not appear in class scope, but may appear in namespace scope or in block scope.[…]
Prefer the verb “designate” rather than “nominate” in the notes that follow:
[ Note 2: A
using-directive
makes the names in thenominateddesignated namespace usable in the scope […]. During unqualified name lookup, the names appear as if they were declared in the nearest enclosing namespace which contains both theusing-directive
and thenomindateddesignated namespace. — end note ][…]
[ Note 4: A
using-directive
is transitive: if a scope contains ausing-directive
thatnominatesdesignates a namespace that itself containsusing-directives
, the namespacesnominateddesignated by thoseusing-directives
are also eligible to be considered. — end note ]
Add a production to the grammar for
attribute-specifier
as
follows:
attribute-specifier: [ [ attribute-using-prefixopt attribute-list ] ]+ [ [ using attribute-namespace :] ] alignment-specifier
and update the grammar for balanced token as follows:
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 :]
Change a sentence in paragraph 4 of 9.12.1 [dcl.attr.grammar] as follows:
4 […] An
attribute-specifier
that contains noattribute
s and noalignment-specifier
has no effect. [ Note 1: That includes anattribute-specifier
of the form[ [ using attribute-namespace :] ]
which is thus equivalent to replacing the:]
token by the two-token sequence:
]
. — end note ] …
Modify the definition of reachability to account for injected declarations:
3 A declaration
D
is reachable from a pointP
if
- (3.1)
P
is not an injected point andD
appears prior toP
in the same translation unit,or- (3.2)
D
is an injected declaration for whichP
is the corresponding injected point, or- (3.3)
D
is not discarded ([module.global.frag]), appears in a translation unit that is reachable fromP
, and does not appear within a private-module-framgent.
Extend paragraph 5, and modify note 3, to clarify the existence of subobjects corresponding to non-static data members of reference types.
5 A data member or member function may be declared
static
in its member-declaration, in which case it is a static member (see [class.static]) (a static data member ([class.static.data]) or static member function ([class.static.mfct]), respectively) of the class. Any other data member or member function is a non-static member (a non-static data member or non-static member function ([class.mfct.non.static]), respectively). For each non-static data member of reference type, there is a unique member subobject whose size and alignment is the same as if the data member were declared with the corresponding pointer type.[ Note 3:
A non-static data member of non-reference type is a member subobject of a class object.An object of class type has a member subobject corresponding to each non-static data member of its class — end note ]
Add built-in operator candidates for std::meta::info
to 12.5 [over.built]:
16 For every
T
, whereT
is a pointer-to-member type,std::meta::info
, orstd::nullptr_t
, there exist candidate operator functions of the formbool operator==(T, T); bool operator!=(T, T);
Extend the last sentence of paragraph 4 to disallow splicing concepts in template parameter declarations.
4 … The concept designated by a type-constraint shall be a type concept ([temp.concept]) that is not a
splice-template-name
.
Modify the grammars for
template-id
and
template-argument
as
follows:
+ splice-template-name: + template splice-specifier + + splice-template-argument: + splice-specifier + template-name: identifier+ splice-template-name template-argument: constant-expression type-id id-expression braced-init-list+ splice-template-argument
Extend paragraph 1 to cover template splicers:
The component name of a
simple-template-id
,template-id
, ortemplate-name
that is anidentifier
is the first name in it. If thetemplate-name
is asplice-template-name
, thesplice-specifier
shall designate a concept, variable template, class template, alias template, or function template that is not a constructor template or destructor template; thesplice-template-name
designates the entity designated by thesplice-specifier
.
Extend paragraph 3 of 13.3 [temp.names]:
3 A
<
is interpreted as the delimiter of a template-argument-list if it follows a name that is not a conversion-function-id and
- (3.1) that follows the keyword template or a ~ after a nested-name-specifier or in a class member access expression, or
- (3.2) for which name lookup finds the injected-class-name of a class template or finds any declaration of a template, or
- (3.3) that is an unqualified name for which name lookup either finds one or more functions or finds nothing, or
- (3.4) that is a terminal name in a using-declarator ([namespace.udecl]), in a declarator-id ([dcl.meaning]), or in a type-only context other than a nested-name-specifier ([temp.res]).
[ Note 1: If the name is an identifier, it is then interpreted as a template-name. The keyword template is used to indicate that a dependent qualified name ([temp.dep.type]) denotes a template where an expression might appear. — end note ]
A
<
is also interpreted as the delimiter of atemplate-argument-list
if it follows asplice-template-name
.[ Example 1:— end example ]struct X { template<std::size_t> X* alloc(); template<std::size_t> static X* adjust(); }; template<class T> void f(T* p) { T* p1 = p->alloc<200>(); // error: < means less than T* p2 = p->template alloc<200>(); // OK, < starts template argument list T::adjust<100>(); // error: < means less than T::template adjust<100>(); // OK, < starts template argument list + static constexpr auto r = ^T::adjust; + T* p3 = [:r:]<200>(); // error: < means less than + T* p4 = template [:r:]<200>(); // OK, < starts template argument list }
Change paragraph 9 to allow splicing into a concept-id:
9 A concept-id is a simple-template-id where the template-name is either a concept-name or a splice-template-name whose splice-specifier designates a concept. A concept-id is a prvalue of type bool, and does not name a template specialization.
Adjust paragraph 3 of [temp.arg.general] to not apply to splice template arguments:
3 A
template-argument
of the formsplice-specifier
is interpreted as asplice-template-argument
. In atemplate-argument
that is not asplice-template-argument
, an ambiguity between atype-id
and an expression is resolved to atype-id
, regardless of the form of the correspondingtemplate-parameter
.[ Example 2:— end example ]template<class T> void f(); template<int I> void f(); void g() { f<int()>(); // int() is a type-id: call the first f() + constexpr int x = 42; + f<[:^int:]>(); // splice-template-argument: calls the first f() + f<[:^x:]>(); // splice-template-argument: calls the second f() }
Extend 13.4.2 [temp.arg.type]/1 to cover splice template arguments:
1 A
template-argument
for atemplate-parameter
which is a type shall either be atype-id
or asplice-template-argument
whosesplice-specifier
designates a type.
TODO: splice-specifier shall designate a value or something.
Extend 13.4.4 [temp.arg.template]/1 to cover splice template arguments:
1 A
template-argument
for a templatetemplate-parameter
shall be the name of a class template or an alias template, expressed asid-expression
, or asplice-template-argument
whosesplice-specifier
designates a template.
Extend template-argument-equivalent to handle std::meta::info
:
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) […]
Extend paragraph 2 to enable reflection of alias template specializations.
2
WhenExcept when used as the operand of areflect-expression
, atemplate-id
refersreferring to a specialization of an alias template, itis equivalent to the associated type obtained by substitution of itstemplate-arguments
for thetemplate-parameter
s in thedefining-type-id
of the alias template.
Extend the grammar of
concept-name
to allow for
splicing reflections of concepts:
concept-name: identifier+ splice-template-name
Modify paragraph 2 to account for splicing reflections of concepts:
A
concept-definition
declares a concept. Itsconcept-name
shall be anidentifier
, and theidentifier
becomes a concept-name referring to that concept within its scope. The optional attribute-specifier-seq appertains to the concept.
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
Add a new paragraph at the end of 13.8.3.3 [temp.dep.expr]:
9 A
primary-expression
of the formsplice-specifier
ortemplate splice-specifier < template-argument-listopt >
is type-dependent if thesplice-specifier
is value-dependent or if the optionaltemplate-argument-list
contains a value-dependent non-type or template argument, or a dependent type argument.
Add at the end of 13.8.3.4 [temp.dep.constexpr]/2 (before the note):
2 An id-expression is value-dependent if:
- (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 )
A
reflect-expression
is value-dependent if the operand of the reflection operator is a type-dependent or value-dependent expression or if that operand is a dependenttype-id
, a dependentnamespace-name
, or a dependenttemplate-name
.
Add a new paragraph after 13.8.3.4 [temp.dep.constexpr]/4:
6 A
primary-expression
of the formsplice-specifier
ortemplate splice-specifier < template-argument-listopt >
is value-dependent if theconstant-expression
is value-dependent or if the optionaltemplate-argument-list
contains a value-dependent non-type or template argument, or a dependent type argument.
Extend paragraph 9 to clarify that
splice-specifier
s may not
appear in preprocessor directives, while also applying a “drive-by fix”
to disallow lambdas in the same context.
9 Preprocessing directives of the forms
# if constant-expression new-line groupopt # elif constant-expression new-line groupopt
check whether the controlling constant expression evaluates to nonzero. The program is ill-formed if a
splice-specifier
orlambda-expression
appears in the controlling constant expression.
For convenience, we’re going to add a new library element to 16.3.2.4 [structure.specifications]/3:
3 Descriptions of function semantics contain the following elements (as appropriate):
(3.1) Constraints: […]
(3.2) Mandates: the conditions that, if not met, render the program ill-formed. […]
- (3.2+1) Constant When: the conditions that are required for a call to this function to be a core constant expression ([expr.const])
Insert before paragraph 7:
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]).
<type_traits>
synopsisAdd a new primary type category type trait:
Header
<type_traits>
synopsis…
// [meta.unary.cat], primary type categories template<class T> struct is_void; ... template<class T> struct is_function;+ template<class T> struct is_reflection; // [meta.unary.cat], primary type categories template<class T> constexpr bool is_void_v = is_void<T>::value; ... template<class T> constexpr bool is_function_v = is_function<T>::value;+ template<class T> + constexpr bool is_reflection_v = is_reflection<T>::value;
Add the is_reflection
primary
type category to the table in paragraph 3:
Template | Condition | Comments |
---|---|---|
|
T is
void
|
|
… | … | … |
|
|
|
<meta>
synopsisAdd a new subsection in 21 [meta] after 21.3 [type.traits]:
Header
<meta>
synopsis#include <initializer_list> #include <ranges> #include <string_view> #include <vector> namespace std::meta { using info = decltype(^::); // [meta.reflection.operators], operator representations enum class operators { see below; }; using enum operators; consteval operators operator_of(info r); consteval string_view operator_symbol_of(operators op); consteval u8string_view u8operator_symbol_of(operators op); // [meta.reflection.names], reflection names and locations consteval bool has_identifier(info r); consteval string_view identifier_of(info r); consteval string_view u8identifier_of(info r); consteval string_view display_string_of(info r); consteval string_view u8display_string_of(info r); consteval source_location source_location_of(info r); // [meta.reflection.queries], reflection queries consteval bool is_public(info r); consteval bool is_protected(info r); consteval bool is_private(info r); consteval bool is_virtual(info r); consteval bool is_pure_virtual(info r); consteval bool is_override(info r); consteval bool is_final(info r); consteval bool is_deleted(info r); consteval bool is_defaulted(info r); consteval bool is_user_provided(info r); consteval bool is_user_declared(info r); consteval bool is_explicit(info r); consteval bool is_noexcept(info r); consteval bool is_bit_field(info r); consteval bool is_enumerator(info r); consteval bool is_const(info r); consteval bool is_volatile(info r); consteval bool is_mutable_member(info r); consteval bool is_lvalue_reference_qualified(info r); consteval bool is_rvalue_reference_qualified(info r); consteval bool has_static_storage_duration(info r); consteval bool has_thread_storage_duration(info r); consteval bool has_automatic_storage_duration(info r); consteval bool has_internal_linkage(info r); consteval bool has_module_linkage(info r); consteval bool has_external_linkage(info r); consteval bool has_linkage(info r); consteval bool is_complete_type(info r); consteval bool has_complete_definition(info r); consteval bool is_namespace(info r); consteval bool is_variable(info r); consteval bool is_type(info r); consteval bool is_type_alias(info r); consteval bool is_namespace_alias(info r); consteval bool is_function(info r); consteval bool is_conversion_function(info r); consteval bool is_operator_function(info r); consteval bool is_literal_operator(info r); consteval bool is_special_member_function(info r); consteval bool is_constructor(info r); consteval bool is_default_constructor(info r); consteval bool is_copy_constructor(info r); consteval bool is_move_constructor(info r); consteval bool is_assignment(info r); consteval bool is_copy_assignment(info r); consteval bool is_move_assignment(info r); consteval bool is_destructor(info r); consteval bool is_template(info r); consteval bool is_function_template(info r); consteval bool is_variable_template(info r); consteval bool is_class_template(info r); consteval bool is_alias_template(info r); consteval bool is_conversion_function_template(info r); consteval bool is_operator_function_template(info r); consteval bool is_literal_operator_template(info r); consteval bool is_constructor_template(info r); consteval bool is_concept(info r); consteval bool has_template_arguments(info r); consteval bool is_value(info r); consteval bool is_object(info r); consteval bool is_structured_binding(info r); consteval bool is_class_member(info r); consteval bool is_namespace_member(info r); consteval bool is_nonstatic_data_member(info r); consteval bool is_static_member(info r); consteval bool is_base(info r); consteval bool has_default_member_initializer(info r); consteval info type_of(info r); consteval info object_of(info r); consteval info value_of(info r); consteval info parent_of(info r); consteval info dealias(info r); consteval info template_of(info r); consteval vector<info> template_arguments_of(info r); // [meta.reflection.member.queries], reflection member queries consteval vector<info> members_of(info r); consteval vector<info> bases_of(info type); consteval vector<info> static_data_members_of(info type); consteval vector<info> nonstatic_data_members_of(info type); consteval vector<info> enumerators_of(info type_enum); consteval vector<info> get_public_members(info type); consteval vector<info> get_public_static_data_members(info type); consteval vector<info> get_public_nonstatic_data_members(info type); consteval vector<info> get_public_bases(info type); // [meta.reflection.layout], reflection layout queries struct member_offsets { size_t bytes; size_t bits; constexpr size_t total_bits() const; auto operator<=>(member_offsets const&) const = default; }; consteval member_offsets offset_of(info r); consteval size_t size_of(info r); consteval size_t alignment_of(info r); consteval size_t bit_size_of(info r); // [meta.reflection.extract], value extraction template<class T> consteval T extract(info); // [meta.reflection.substitute], reflection substitution template <class R> concept reflection_range = see below; template <reflection_range R = initializer_list<info>> consteval bool can_substitute(info templ, R&& arguments); template <reflection_range R = initializer_list<info>> consteval info substitute(info templ, R&& arguments); // [meta.reflection.result], expression result reflection template<class T> consteval info reflect_value(T value); template<class T> consteval info reflect_object(T& object); template<class T> consteval info reflect_function(T& fn); template <reflection_range R = initializer_list<info>> consteval info reflect_invoke(info target, R&& args); template <reflection_range R1 = initializer_list<info>, reflection_range R2 = initializer_list<info>> consteval info reflect_invoke(info target, R1&& tmpl_args, R2&& args); // [meta.reflection.define_class], class definition generation struct data_member_options_t { struct name_type { template<class T> requires constructible_from<u8string, T> consteval name_type(T &&); template<class T> requires constructible_from<string, T> consteval name_type(T &&); variant<u8string, string> contents; // exposition only }; 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 = {}); consteval bool is_data_member_spec(info r); template <reflection_range R = initializer_list<info>> consteval info define_class(info type_class, R&&); // [meta.reflection.define_static], static array generation consteval const char* define_static_string(string_view str); consteval const char8_t* define_static_string(u8string_view str); template<ranges::input_range R> consteval span<const ranges::range_value_t<R>> define_static_array(R&& r); // [meta.reflection.unary.cat], primary type categories consteval bool type_is_void(info type); consteval bool type_is_null_pointer(info type); consteval bool type_is_integral(info type); consteval bool type_is_floating_point(info type); consteval bool type_is_array(info type); consteval bool type_is_pointer(info type); consteval bool type_is_lvalue_reference(info type); consteval bool type_is_rvalue_reference(info type); consteval bool type_is_member_object_pointer(info type); consteval bool type_is_member_function_pointer(info type); consteval bool type_is_enum(info type); consteval bool type_is_union(info type); consteval bool type_is_class(info type); consteval bool type_is_function(info type); consteval bool type_is_reflection(info type); // [meta.reflection.unary.comp], composite type categories consteval bool type_is_reference(info type); consteval bool type_is_arithmetic(info type); consteval bool type_is_fundamental(info type); consteval bool type_is_object(info type); consteval bool type_is_scalar(info type); consteval bool type_is_compound(info type); consteval bool type_is_member_pointer(info type); // [meta.reflection unary.prop], type properties consteval bool type_is_const(info type); consteval bool type_is_volatile(info type); consteval bool type_is_trivial(info type); consteval bool type_is_trivially_copyable(info type); consteval bool type_is_standard_layout(info type); consteval bool type_is_empty(info type); consteval bool type_is_polymorphic(info type); consteval bool type_is_abstract(info type); consteval bool type_is_final(info type); consteval bool type_is_aggregate(info type); consteval bool type_is_signed(info type); consteval bool type_is_unsigned(info type); consteval bool type_is_bounded_array(info type); consteval bool type_is_unbounded_array(info type); consteval bool type_is_scoped_enum(info type); template <reflection_range R = initializer_list<info>> consteval bool type_is_constructible(info type, R&& type_args); consteval bool type_is_default_constructible(info type); consteval bool type_is_copy_constructible(info type); consteval bool type_is_move_constructible(info type); consteval bool type_is_assignable(info type_dst, info type_src); consteval bool type_is_copy_assignable(info type); consteval bool type_is_move_assignable(info type); consteval bool type_is_swappable_with(info type_dst, info type_src); consteval bool type_is_swappable(info type); consteval bool type_is_destructible(info type); template <reflection_range R = initializer_list<info>> consteval bool type_is_trivially_constructible(info type, R&& type_args); consteval bool type_is_trivially_default_constructible(info type); consteval bool type_is_trivially_copy_constructible(info type); consteval bool type_is_trivially_move_constructible(info type); consteval bool type_is_trivially_assignable(info type_dst, info type_src); consteval bool type_is_trivially_copy_assignable(info type); consteval bool type_is_trivially_move_assignable(info type); consteval bool type_is_trivially_destructible(info type); template <reflection_range R = initializer_list<info>> consteval bool type_is_nothrow_constructible(info type, R&& type_args); consteval bool type_is_nothrow_default_constructible(info type); consteval bool type_is_nothrow_copy_constructible(info type); consteval bool type_is_nothrow_move_constructible(info type); consteval bool type_is_nothrow_assignable(info type_dst, info type_src); consteval bool type_is_nothrow_copy_assignable(info type); consteval bool type_is_nothrow_move_assignable(info type); consteval bool type_is_nothrow_swappable_with(info type_dst, info type_src); consteval bool type_is_nothrow_swappable(info type); consteval bool type_is_nothrow_destructible(info type); consteval bool type_is_implicit_lifetime(info type); consteval bool type_has_virtual_destructor(info type); consteval bool type_has_unique_object_representations(info type); consteval bool type_reference_constructs_from_temporary(info type_dst, info type_src); consteval bool type_reference_converts_from_temporary(info type_dst, info type_src); // [meta.reflection.unary.prop.query], type property queries consteval size_t type_alignment_of(info type); consteval size_t type_rank(info type); consteval size_t type_extent(info type, unsigned i = 0); // [meta.reflection.rel], type relations consteval bool type_is_same(info type1, info type2); consteval bool type_is_base_of(info type_base, info type_derived); consteval bool type_is_convertible(info type_src, info type_dst); consteval bool type_is_nothrow_convertible(info type_src, info type_dst); consteval bool type_is_layout_compatible(info type1, info type2); consteval bool type_is_pointer_interconvertible_base_of(info type_base, info type_derived); template <reflection_range R = initializer_list<info>> consteval bool type_is_invocable(info type, R&& type_args); template <reflection_range R = initializer_list<info>> consteval bool type_is_invocable_r(info type_result, info type, R&& type_args); template <reflection_range R = initializer_list<info>> consteval bool type_is_nothrow_invocable(info type, R&& type_args); template <reflection_range R = initializer_list<info>> consteval bool type_is_nothrow_invocable_r(info type_result, info type, R&& type_args); // [meta.reflection.trans.cv], const-volatile modifications consteval info type_remove_const(info type); consteval info type_remove_volatile(info type); consteval info type_remove_cv(info type); consteval info type_add_const(info type); consteval info type_add_volatile(info type); consteval info type_add_cv(info type); // [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); // [meta.reflection.trans.sign], sign modifications consteval info type_make_signed(info type); consteval info type_make_unsigned(info type); // [meta.reflection.trans.arr], array modifications consteval info type_remove_extent(info type); consteval info type_remove_all_extents(info type); // [meta.reflection.trans.ptr], pointer modifications consteval info type_remove_pointer(info type); consteval info type_add_pointer(info type); // [meta.reflection.trans.other], other transformations consteval info type_remove_cvref(info type); consteval info type_decay(info type); template <reflection_range R = initializer_list<info>> consteval info type_common_type(R&& type_args); template <reflection_range R = initializer_list<info>> consteval info type_common_reference(R&& type_args); consteval info type_underlying_type(info type); template <reflection_range R = initializer_list<info>> `consteval info type_invoke_result(info type, R&& type_args); consteval info type_unwrap_reference(info type); consteval info type_unwrap_ref_decay(info type); // [meta.reflection.tuple.variant], tuple and variant queries consteval size_t type_tuple_size(info type); consteval info type_tuple_element(size_t index, info type); consteval size_t type_variant_size(info type); consteval info type_variant_alternative(size_t index, info type); }
enum class operators { see below;}; using enum operators;
1 This enum class specifies constants used to identify operators that can be overloaded, with the meanings listed in Table 1. The values of the constants are distinct.
Table 1: Enum class operators
[meta.reflection.operators]
Constant Corresponding operator Operator symbol nameop_new
operator new
new
op_delete
operator delete
delete
op_array_new
operator new[]
new[]
op_array_delete
operator delete[]
delete
op_co_await
operator co_await
co_await
op_parentheses
operator()
()
op_square_brackets
operator[]
[]
op_arrow
operator->
->
op_arrow_star
operator->*
->*
op_tilde
operator~
~
op_exclaim
operator!
!
op_plus
operator+
+
op_minus
operator-
-
op_star
operator*
*
op_slash
operator/
/
op_percent
operator%
%
op_caret
operator^
^
op_ampersand
operator&
&
op_pipe
operator|
|
op_equals
operator=
=
op_plus_equals
operator+=
+=
op_minus_equals
operator-=
-=
op_star_equals
operator*=
*=
op_slash_equals
operator/=
/=
op_percent_equals
operator%=
%=
op_caret_equals
operator^=
^=
op_ampersand_equals
operator&=
&=
op_pipe_equals
operator|=
|=
op_equals_equals
operator==
==
op_exclaim_equals
operator!=
!=
op_less
operator<
<
op_greater
operator>
>
op_less_equals
operator<=
<=
op_greater_equals
operator>=
>=
op_three_way_compare
operator<=>
<=>
op_ampersand_and
operator&&
&&
op_pipe_pipe
operator||
||
op_less_less
operator<<
<<
op_greater_greater
operator>>
>>
op_less_less_equals
operator<<=
<<=
op_greater_greater_equals
operator>>=
>>=
op_plus_plus
operator++
++
op_minus_minus
operator--
--
op_comma
operator,
,
consteval operators operator_of(info r);
2 Constant When:
r
represents an operator function or operator function template.3 Returns: The value of the enumerator from
operators
for which the corresponding operator has the same unqualified name as the entity represented byr
.consteval string_view operator_symbol_of(operators op); consteval u8string_view u8operator_symbol_of(operators op);
4 Constant When: The value of
op
corresponds to one of the enumerators inoperators
.5 Returns: A
string_view
oru8string_view
containing the characters of the operator symbol name corresponding toop
, respectively encoded with the ordinary literal encoding or with UTF-8.
consteval bool has_identifier(info r);
1 Returns:
- (1.1) If
r
represents an entity with an unnamed name, thenfalse
.- (1.2) Otherwise, if
r
represents a function, thentrue
if the function is not a function template specialization, constructor, destructor, operator function, or conversion function.- (1.3) Otherwise, if
r
represents a function template, thentrue
ifr
does not represent a constructor template, operator function template, or conversion function template.- (1.4) Otherwise, if
r
represents atypedef-name
, thentrue
when thetypedef-name
is an identifier.- (1.5) Otherwise, if
r
represents a class typeC
, thentrue
when eitherC
has a typdef name for linkage purposes ([dcl.typedef]) or theclass-name
introduced by the declaration ofC
is an identifier.- (1.6) Otherwise, if
r
represents a variable, thentrue
ifr
does not represent a variable template specialization.- (1.7) Otherwise, if
r
represents a structured binding, enumerator, non-static data member, template, namespace, or namespace alias, thentrue
.- (1.8) Otherwise, if
r
represents a base class specifier, thentrue
ifhas_identifier(type_of(r))
.- (1.9) Otherwise if
r
represents a description of a declaration of a non-static data member, then lettingo
be adata_member_options_t
value such thatdata_member_spec(type_of(r), o) == r
, thentrue
ifo.name
contains a value.- (1.10) Otherwise,
false
.consteval string_view identifier_of(info r); consteval u8string_view u8identifier_of(info r);
2 Let E be UTF-8 if returning a
u8string_view
, and otherwise the ordinary literal encoding.3 Constant When:
has_identifier(r)
istrue
and the identifier that would be returned (see below) is representable byE
.4 Returns:
- (4.1) If
r
represents a literal operator or literal operator template, then theud-suffix
of the operator or operator template.- (4.2) Otherwise, if
r
represents a class type, then either the typedef name for linkage purposes or the identifier introduced by the declaration of the represented type.- (4.3) Otherwise, if
r
represents an entity,typedef-name
, or namespace alias, then the identifier introduced by the declaration of what is represented byr
.- (4.4) Otherwise, if
r
represents a base class specifier, then the identifier introduced by the declaration of the type of the base class.- (4.5) Otherwise (if
r
represents a description of a declaration of a non-static data member), then lettingo
be adata_member_options_t
value such thatdata_member_spec(type_of(r), o) == r
, then thestring
oru8string
contents ofo.name.contents
encoded withE
.consteval string_view display_string_of(info r); consteval u8string_view u8display_string_of(info r);
5 Constant When: If returning
string_view
, the implementation-defined name is representable using the ordinary literal encoding.6 Returns: An implementation-defined
string_view
oru8string_view
, respectively, suitable for identifying the represented construct.consteval source_location source_location_of(info r);
7 Returns: If
r
represents a value, a non-class type, the global namespace, or a description of a declaration of a non-static data member, thensource_location{}
. Otherwise, an implementation-definedsource_location
value.8 Recommended practice: If
r
represents an entity, name, or base specifier that was introduced by a declaration, implementations should return a value corresponding to the declaration.
consteval bool is_public(info r); consteval bool is_protected(info r); consteval bool is_private(info r);
1 Returns:
true
ifr
represents a class member or base class specifier that is public, protected, or private, respectively. Otherwise,false
.consteval bool is_virtual(info r);
2 Returns:
true
ifr
represents either a virtual member function or a virtual base class specifier. Otherwise,false
.consteval bool is_pure_virtual(info r); consteval bool is_override(info r);
3 Returns:
true
ifr
represents a member function that is pure virtual or overrides another member function, respectively. Otherwise,false
.consteval bool is_final(info r);
4 Returns:
true
ifr
represents a final class or a final member function. Otherwise,false
.consteval bool is_deleted(info r); consteval bool is_defaulted(info r);
5 Returns:
true
ifr
represents a function that is defined as deleted ([dcl.fct.def.delete])or defined as defaulted ([dcl.fct.def.default]), respectively. Otherwise,false
.consteval bool is_user_provided(info r); consteval bool is_user_declared(info r);
6 Returns:
true
ifr
represents a function that is user-provided or user-declared (9.5.2 [dcl.fct.def.default]), respectively. Otherwise,false
.consteval bool is_explicit(info r);
7 Returns:
true
ifr
represents a member function that is declared explicit. Otherwise,false
. [ Note 1: Ifr
represents a member function template that is declaredexplicit
,is_explicit(r)
is stillfalse
because in general such queries for templates cannot be answered. — end note ]consteval bool is_noexcept(info r);
8 Returns:
true
ifr
represents anoexcept
function type or a function or member function that is declarednoexcept
. Otherwise,false
. [ Note 2: Ifr
represents a function template that is declarednoexcept
,is_noexcept(r)
is stillfalse
because in general such queries for templates cannot be answered. — end note ]consteval bool is_bit_field(info r);
9 Returns:
true
ifr
represents a bit-field, or ifr
represents a description of a declaration of a non-static data member for which any data member declared with the properties represented byr
would be a bit-field. Otherwise,false
.consteval bool is_enumerator(info r);
10 Returns:
true
ifr
represents an enumerator. Otherwise,false
.consteval bool is_const(info r); consteval bool is_volatile(info r);
11 Returns:
true
ifr
represents 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
.consteval bool is_mutable_member(info r);
12 Returns:
true
ifr
represents amutable
non-static data member. Otherwise,false
.consteval bool is_lvalue_reference_qualified(info r); consteval bool is_rvalue_reference_qualified(info r);
13 Returns:
true
ifr
represents a lvalue- or rvalue-reference qualified function type (respectively), or a member function with such a type. Otherwise,false
.consteval bool has_static_storage_duration(info r); consteval bool has_thread_storage_duration(info r); consteval bool has_automatic_storage_duration(info r);
14 Returns:
true
ifr
represents an object or variable that has static, thread, or automatic storage duration, respectively ([basic.stc]). Otherwise,false
.consteval bool has_internal_linkage(info r); consteval bool has_module_linkage(info r); consteval bool has_external_linkage(info r); consteval bool has_linkage(info r);
15 Returns:
true
ifr
represents a variable, function, type, template, or namespace whose name has internal linkage, module linkage, external linkage, or any linkage, respectively ([basic.link]). Otherwise,false
.consteval bool is_complete_type(info r);
16 Effects: If
is_type(r)
istrue
anddealias(r)
represents a class template specialization with a definition reachable from the evaluation context, the specialization is instantiated.17 Returns:
true
ifis_type(r)
istrue
and there is some point in the evaluation context from which the type represented bydealias(r)
is not an incomplete type ([basic.types]). Otherwise,false
.consteval bool has_complete_definition(info r);
18 Effects: If
is_type(r)
istrue
anddealias(r)
represents a class template specialization with a reachable definition, the specialization is instantiated.19 Returns:
true
ifr
represents a function, class type, or enumeration typeE
, such that no entities not already declared may be introduced within the scope ofE
. Otherwisefalse
.consteval bool is_namespace(info r);
20 Returns:
true
ifr
represents a namespace or namespace alias. Otherwise,false
.consteval bool is_variable(info r);
21 Returns:
true
ifr
represents a variable. Otherwise,false
.consteval bool is_type(info r);
22 Returns:
true
ifr
represents a type or atypedef-name
. Otherwise,false
.consteval bool is_type_alias(info r); consteval bool is_namespace_alias(info r);
23 Returns:
true
ifr
represents atypedef-name
or namespace alias, respectively [ Note 3: An instantiation of an alias template is atypedef-name
— end note ]. Otherwise,false
.consteval bool is_function(info r);
24 Returns:
true
ifr
represents a function. Otherwise,false
.consteval bool is_conversion_function(info r); consteval bool is_operator_function(info r); consteval bool is_literal_operator(info r);
25 Returns:
true
ifr
represents a conversion function, operator function, or literal operator, respectively. Otherwise,false
.consteval bool is_special_member_function(info r); consteval bool is_constructor(info r); consteval bool is_default_constructor(info r); consteval bool is_copy_constructor(info r); consteval bool is_move_constructor(info r); consteval bool is_assignment(info r); consteval bool is_copy_assignment(info r); consteval bool is_move_assignment(info r); consteval bool is_destructor(info r);
26 Returns:
true
ifr
represents a function that is a special member function, a constructor, a default constructor, a copy constructor, a move constructor, an assignment operator, a copy assignment operator, a move assignment operator, or a prospective destructor, respectively. Otherwise,false
.consteval bool is_template(info r);
27 Returns:
true
ifr
represents a function template, class template, variable template, alias template, or concept. Otherwise,false
.28 [ Note 4: A template specialization is not a template.
is_template(^std::vector)
istrue
butis_template(^std::vector<int>)
isfalse
. — end note ]consteval bool is_function_template(info r); consteval bool is_variable_template(info r); consteval bool is_class_template(info r); consteval bool is_alias_template(info r); consteval bool is_conversion_function_template(info r); consteval bool is_operator_function_template(info r); consteval bool is_literal_operator_template(info r); consteval bool is_constructor_template(info r); consteval bool is_concept(info r);
29 Returns:
true
ifr
represents a function template, variable template, class template, alias template, conversion function template, operator function template, literal operator template, constructor template, or concept respectively. Otherwise,false
.consteval bool has_template_arguments(info r);
30 Returns:
true
ifr
represents a specialization of a function template, variable template, class template, or an alias template. Otherwise,false
.consteval bool is_value(info r); consteval bool is_object(info r);
31 Returns:
true
ifr
represents a value or object, respectively. Otherwise,false
.consteval bool is_structured_binding(info r);
32 Returns:
true
ifr
represents a structured binding. Otherwise,false
.consteval bool is_class_member(info r); consteval bool is_namespace_member(info r); consteval bool is_nonstatic_data_member(info r); consteval bool is_static_member(info r); consteval bool is_base(info r);
33 Returns:
true
ifr
represents a class member, namespace member, non-static data member, static member, base class specifier, respectively. Otherwise,false
.consteval bool has_default_member_initializer(info r);
34 Returns:
true
ifr
represents a non-static data member that has a default member initializer. Otherwise,false
.consteval info type_of(info r);
35 Constant When:
r
represents a value, object, variable, function that is not a constructor or destructor, enumerator, non-static data member, bit-field, base class specifier, or description of a declaration of a non-static data member.36 Returns: If
r
represents an entity, object, or value, then the type of what is represented byr
. Otherwise, ifr
represents a base class specifier, then the type of the base class. Otherwise, the type of any data member declared with the properties represented byr
.consteval info object_of(info r);
37 Constant When:
r
is a reflection representing either an object or a variable denoting an object with static storage duration ([expr.const]).38 Returns: If
r
is a reflection of a variable, then a reflection of the object denoted by the variable. Otherwise,r
.[ Example 1:— end example ]int x; int& y = x; static_assert(^x != ^y); // OK, x and y are different variables so their // reflections compare different static_assert(object_of(^x) == object_of(^y)); // OK, because y is a reference // to x, their underlying objects are the same
consteval info value_of(info r);
39 Constant When:
r
is a reflection representing
- (39.1) either an object or variable, usable in constant expressions from a point in the evaluation context ([expr.const]), whose type is a structural type ([temp.type]),
- (39.2) an enumerator, or
- (39.3) a value.
40 Returns:
- (40.1) If
r
is a reflection of an objecto
, or a reflection of a variable which designates an objecto
, then a reflection of the value held byo
. The reflected value has typetype_of(o)
, with the cv-qualifiers removed if this is a scalar type- (40.2) Otherwise, if
r
is a reflection of an enumerator, then a reflection of the value of the enumerator.- (40.3) Otherwise,
r
.[ Example 2:— end example ]constexpr int x = 0; constexpr int y = 0; static_assert(^x != ^y); // OK, x and y are different variables so their // reflections compare different static_assert(value_of(^x) == value_of(^y)); // OK, both value_of(^x) and value_of(^y) represent // the value 0 static_assert(value_of(^x) == reflect_value(0)); // OK, likewise
consteval info parent_of(info r);
41 Constant When:
r
represents a variable, structured binding, function, enumerator, class, class member, bit-field, template, namespace or namespace alias (other than::
),typedef-name
, or base class specifier.42 Returns: A reflection of the class, function, or namespace enclosing the first declaration of what is represented by
r
.consteval info dealias(info r);
43 Returns: If
r
represents atypedef-name
or namespace alias A, then a reflection representing the entity named by A. Otherwise,r
.[ Example 3:— end example ]using X = int; using Y = X; static_assert(dealias(^int) == ^int); static_assert(dealias(^X) == ^int); static_assert(dealias(^Y) == ^int);
consteval info template_of(info r); consteval vector<info> template_arguments_of(info r);
45 Constant When:
has_template_arguments(r)
istrue
.46 Returns: A reflection of the template of
r
, and the reflections of the template arguments of the specialization represented byr
, respectively.[ Example 4:— end example ]template <class T, class U=T> struct Pair { }; template <class T> using PairPtr = Pair<T*>; 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);
consteval vector<info> members_of(info r);
1 Constant When:
r
is a reflection representing either a namespace or a class type that is complete from some point in the evaluation context.2 A member of a class or namespace
E
is members-of-representable if it is either
- a class that is not a closure type,
- a
typedef-name
,- a primary class template, function template, primary variable template, alias template, or concept,
- a variable or reference,
- a function whose constraints (if any) are satisfied,
- a non-static data member,
- a namespace, or
- a namespace alias,
and if its first declaration is within a definition of
E
.[ Note 1: Counterexamples of representable members include: injected class names, partial template specializations, friend declarations, and static assertions. — end note ]
3 A member
M
of a class or namespace is members-of-visible from a pointP
if there exists a declarationD
ofM
that is reachable fromP
, and eitherM
is not TU-local orD
is declared in the translation unit containingP
.4 Effects: If
dealias(r)
represents a class template specialization with a definition reachable from the evaluation context, the specialization is instantiated.5 Returns: A
vector
containing reflections of all members-of-representable members of the entity represented byr
that are members-of-visible from a point in the evaluation context ([expr.const]). IfE
represents a classC
, then the vector also contains reflections representing all unnamed bit-fields declared within the member-specification ofC
. Class members and unnamed bit-fields are indexed in the order in which they are declared, but the order of namespace members is unspecified. [ Note 2: Base classes are not members. — end note ]consteval vector<info> bases_of(info type);
6 Constant When:
dealias(type)
is a reflection representing a complete class type.7 Effects: If
dealias(type)
represents a class template specialization with a reachable definition, the specialization is instantiated.8 Returns: Let
C
be the type represented bydealias(type)
. Avector
containing the reflections of all the direct base class specifiers, if any, ofC
. The base class specifiers are indexed in the order in which they appear in the base-specifier-list ofC
.consteval vector<info> static_data_members_of(info type);
9 Constant When:
dealias(type)
represents a complete class type.10 Effects: If
dealias(type)
represents a class template specialization with a reachable definition, the specialization is instantiated.11 Returns: A
vector
containing the reflections of the direct static data members of the type represented bydealias(type)
, in the order in which they are declared.consteval vector<info> nonstatic_data_members_of(info type);
12 Constant When:
dealias(type)
represents a complete class type.13 Effects: If
dealias(type)
represents a class template specialization with a reachable definition, the specialization is instantiated.14 Returns: A
vector
containing the reflections of the direct non-static data members of the type represented bydealias(type)
, in the order in which they are declared.consteval vector<info> enumerators_of(info type_enum);
15 Constant When:
dealias(type_enum)
represents an enumeration type andhas_complete_definition(dealias(type_enum))
istrue
.16 Returns: A
vector
containing the reflections of each enumerator of the enumeration represented bydealias(type_enum)
, in the order in which they are declared.consteval vector<info> get_public_members(info type);
17 Constant When:
dealias(type)
represents a complete class type.18 Effects: If
dealias(type)
represents a class template specialization with a reachable definition, the specialization is instantiated.19 Returns: A
vector
containing each element,e
, ofmembers_of(type)
such thatis_public(e)
istrue
, in order.consteval vector<info> get_public_static_data_members(info type);
20 Constant When:
dealias(type)
represents a complete class type.21 Effects: If
dealias(type)
represents a class template specialization with a reachable definition, the specialization is instantiated.22 Returns: A
vector
containing each element,e
, ofstatic_data_members_of(type)
such thatis_public(e)
istrue
, in order.consteval vector<info> get_public_nonstatic_data_members(info type);
23 Constant When:
dealias(type)
represents a complete class type.24 Effects: If
dealias(type)
represents a class template specialization with a reachable definition, the specialization is instantiated.25 Returns: A
vector
containing each element,e
, ofnonstatic_data_members_of(type)
such thatis_public(e)
istrue
, in order.consteval vector<info> get_public_bases(info type);
26 Constant When:
dealias(type)
represents a complete class type.27 Effects: If
dealias(type)
represents a class template specialization with a reachable definition, the specialization is instantiated.28 Returns: A
vector
containing each element,e
, ofbases_of(type)
such thatis_public(e)
istrue
, in order.
constexpr size_t member_offsets::total_bits() const;
1 Returns:
bytes * CHAR_BIT + bits
.consteval member_offsets offset_of(info r);
2 Constant When:
r
represents a non-static data member or base class specifier.3 Let
V
be a constant defined as follows:
- (3.1) If
r
represents a virtual base class specifier of an abstract class, thenV
is an implementation-defined value.- (3.2) Otherwise,
V
is the offset in bits from the beginning of an object of typeparent_of(r)
to the subobject associated with the entity represented byr
.4 Returns:
{V / CHAR_BIT, V % CHAR_BIT}
.consteval size_t size_of(info r);
5 Constant When:
r
is a reflection of a type, object, value, variable of non-reference type, non-static data member, base class specifier, or description of a declaration of a non-static data member. Ifr
represents a typeT
, there is a point within the evaluation context from whichT
is not incomplete.6 Returns: If
r
represents a non-static data member whose associated subobject has typeT
, or a description of a declaration of such a data member, thensizeof(T)
. Otherwise, ifr
represents a typeT
, thensizeof(T)
. Otherwise,size_of(type_of(r))
.[ Note 1: The subobject corresponding to a non-static data member of reference type has the same size and alignment as the corresponding pointer type. — end note ]
consteval size_t alignment_of(info r);
7 Constant When:
r
is a reflection representing a type, object, variable, non-static data member that is not a bit-field, base class specifier, or description of a declaration of a non-static data member. Ifr
represents a typeT
, there is a point within the evaluation context from whichT
is not incomplete.8 Returns: If
r
represents a type, variable, or object, then the alignment requirement of the entity or object. Otherwise, ifr
represents a base class specifier, thenalignment_of(type_of(r))
. Otherwise, ifr
represents a non-static data member, then the alignment requirement of the subobject associated with the represented entity within any object of typeparent_of(r)
. Otherwise, ifr
represents a description of a declaration of a non-static data member, then thealignment-specifier
of any data member declared having the properties described byr
.consteval size_t bit_size_of(info r);
9 Constant When:
r
is a reflection of a type, object, value, variable of non-reference type, non-static data member, base class specifier, or description of a declaration of a non-static data member. Ifr
represents a typeT
, there is a point within the evaluation context from whichT
is not incomplete.10 Returns: If
r
represents a non-static data member that is a bit-field, or a description of a declaration of such a bit-field data member, then the width of the bit-field. Otherwise,CHAR_BIT * size_of(r)
.
1 The
extract
function template may be used to extract a value out of a reflection when the type is known.2 The following are defined for exposition only to aid in the specification of
extract
:template <class T> consteval T extract-ref(info r); // exposition only
3 [ Note 1:
T
is a reference type. — end note ]4 Constant When:
r
represents a variable or object of typeU
that is usable in constant expressions from a point in the evaluation context andis_convertible_v<remove_reference_t<U>(*)[], remove_reference_t<T>(*)[]>
istrue
.5 Returns: the object represented by
object_of(r)
.template <class T> consteval T extract-member-or-function(info r); // exposition only
6 Constant When:
- (6.1) If
r
represents a non-static data member of a classC
with typeX
, then whenT
isX C::*
andr
does not represent a bit-field.- (6.2) Otherwise, if
r
represents an implicit object member function of classC
with typeF
orF noexcept
, then whenT
isF C::*
.- (6.3) Otherwise,
r
represents a function, static member function, or explicit object member function of function typeF
orF noexcept
, then whenT
isF*
.7 Returns: a pointer value designating the entity represented by
r
.template <class T> consteval T extract-val(info r); // exposition only
8 Let
U
be the type of the value or enumerator thatr
represents.9 Constant When:
- (9.1)
U
is a pointer type,T
andU
are similar types ([conv.qual]), andis_convertible_v<U, T>
istrue
,- (9.2)
U
is not a pointer type and the cv-unqualified types ofT
andU
are the same, or- (9.3)
U
is a closure type,T
is a function pointer type, and the valuer
represents is convertible toT
.10 Returns: the value or enumerator
V
represented byr
, converted toT
.template <class T> consteval T extract(info r);
11 Effects:
template <class R> concept reflection_range = ::input_range<R> && ranges<ranges::range_value_t<R>, info> && same_as<remove_cvref_t<ranges::range_reference_t<R>>, info>; same_as
template <reflection_range R = initializer_list<info>> consteval bool can_substitute(info templ, R&& arguments);
1 Constant When:
templ
represents a template.2 Let
Z
be the template represented bytempl
and letArgs...
be the sequence of entities, variables, or aliases represented by the elements ofarguments
.3 Returns:
true
ifZ<Args...>
is a valid template-id ([temp.names]). Otherwise,false
.4 Remarks: If attempting to substitute leads to a failure outside of the immediate context, the program is ill-formed.
template <reflection_range R = initializer_list<info>> consteval info substitute(info templ, R&& arguments);
5 Constant When:
can_substitute(templ, arguments)
istrue
.6 Let
Z
be the template represented bytempl
and letArgs...
be the sequence of entities, variables, or aliases represented by the elements ofarguments
.7 Returns:
^Z<Args...>
.
template <typename T> consteval info reflect_value(T expr);
1 Mandates:
T
is a structural type that is not a reference type.2 Constant When: Any value computed by
expr
having pointer type, or every subobject of the value computed byexpr
having pointer or reference type, shall be the address of or refer to an object or entity that
- (2.1) is a permitted result of a constant expression ([expr.const]),
- (2.2) is not a temporary object ([class.temporary]),
- (2.3) is not a string literal object ([lex.string]),
- (2.4) is not the result of a
typeid
expression ([expr.typeid]), and- (2.5) is not an object associated with a predefined
__func__
variable ([dcl.fct.def.general]).3 Returns: A reflection of the value computed by an lvalue-to-rvalue conversion applied to
expr
. The type of the represented value is the cv-unqualified version ofT
.template <typename T> consteval info reflect_object(T& expr);
4 Mandates:
T
is not a function type.5 Constant When:
expr
designates an object or entity that
- (5.1) is a permitted result of a constant expression ([expr.const]),
- (5.2) is not a temporary object ([class.temporary]),
- (5.3) is not a string literal object ([lex.string]),
- (5.4) is not the result of a
typeid
expression ([expr.typeid]), and- (5.5) is not an object associated with a predefined
__func__
variable ([dcl.fct.def.general]).6 Returns: A reflection of the object designated by
expr
.template <typename T> consteval info reflect_function(T& expr);
7 Mandates:
T
is a function type.8 Returns:
^fn
, wherefn
is the function designated byexpr
.template <reflection_range R = initializer_list<info>> consteval info reflect_invoke(info target, R&& args); template <reflection_range R1 = initializer_list<info>, reflection_range R2 = initializer_list<info>> consteval info reflect_invoke(info target, R1&& tmpl_args, R2&& args);
9 An expression
E
is said to be reciprocal to a reflectionr
if
- (9.1)
r
represents a variable, andE
is an lvalue designating the object named by that variable,- (9.2)
r
represents an object or function, andE
is an lvalue that designates that object or function, or- (9.3)
r
represents a value, andE
is a prvalue that computes that value.10 For exposition only, let
- (10.1)
F
be either an entity or an expression, such that iftarget
represents a function or function template thenF
is that entity, and iftarget
represents a variable, object, or value, thenF
is an expression reciprocal totarget
,- (10.2)
TArgs...
be a sequence of entities and expressions corresponding to the elements oftmpl_args
(if any), such that for everytarg
intmpl_args
,
- (10.2.1) if
targ
represents a type ortypedef-name
, the corresponding element ofTArgs...
is that type, or the type named by thattypedef-name
, respectively,- (10.2.2) if
targ
represents a variable, object, value, or function, the corresponding element ofTArgs...
is an expression reciprocal totarg
, and- (10.2.3) if
targ
represents a class or alias template, then the corresponding element ofTArgs...
is that template,- (10.3)
Args...
be a sequence of expressions {EK
} corresponding to the reflections {rK
} inargs
, such that for everyrK
that represents a variable, object, value, or function,EK
is reciprocal torK
,- (10.4)
Arg0
be the first expression inArgs
(if any), and- (10.5)
Args+...
be the sequence of expressions inArgs
excludingArg0
(if any),and define an expression
INVOKE-EXPR
as follows:
(10.6) If
target
represents a non-member function, variable, object, or value, thenINVOKE-EXPR
is the expressionINVOKE(F, Arg0, Args+...)
.(10.7) Otherwise, if
F
is a member function that is not a constructor, thenINVOKE-EXPR
is the expressionArg0.F(Args+...)
.(10.8) Otherwise, if
F
is a function template that is not a constructor template, thenINVOKE-EXPR
is either the expressionArg0.template F<TArgs...>(Args+...)
ifF
is a member function template, orF<TArgs...>(Arg0, Args+...)
otherwise.(10.9) Otherwise, if
F
is a constructor or constructor template for a classC
, thenINVOKE-EXPR
is an expressionC(Arg0, Args+...)
for which onlyF
is considered by overload resolution; furthermore, ifF
is a constructor template, thenTArgs...
are inferred as leading template arguments during template argument deduction forF
.11 Constant When:
target
represents either a function or function template, or a variable, object or value having pointer-to-function, pointer-to-member, or closure type,tmpl_args
is empty unlesstarget
represents a function template,- every reflection in
tmpl_args
represents a type,typedef-name
, class or alias template, variable, object, value, or function,- every reflection in
args
represents a variable, object, value, or function, and- the expression
INVOKE-EXPR
is a well-formed constant expression of structural type.12 Effects: If
target
represents a function template, any specialization of the represented template that would be invoked by evaluation ofINVOKE-EXPR
is instantiated.13 Returns: A reflection of the same result computed by
INVOKE-EXPR
.
template <class T> requires constructible_from<u8string, T> consteval data_member_options_t::name_type(T&& value);
1 Effects: Initializes
contents
withu8string(value)
.template<class T> requires constructible_from<string, T> consteval data_member_options_t::name_type(T&& value);
2 Effects: Initializes
contents
withstring(value)
.[ Note 1:
name_type
provides a simple inner class that can be implicitly constructed from anything convertible tostring
oru8string
. This allows adata_member_spec
to accept an ordinary string literal (orstring_view
,string
, etc) or a UTF-8 string literal (oru8string_view
,u8string
, etc) equally well.— end note ]constexpr auto mem1 = data_member_spec(^int, {.name="ordinary_literal_encoding"}); constexpr auto mem2 = data_member_spec(^int, {.name=u8"utf8_encoding"});
consteval info data_member_spec(info type, = {}); data_member_options_t options
1 Constant When:
- (1.1)
type
represents a type;- (1.2) if
options.name.contents
contains a valueNAME
then either:- (1.3) if
options.width
contains a value, then:type
represents an integral or (possibly cv-qualified) enumeration type,options.alignment
contains no value, andoptions.no_unique_address
isfalse
;- (1.4) if
options.alignment
contains a value, it is an alignment value ([basic.align]) not less than the alignment requirement of the type represented bytype
; and- (1.5) if
options.width
contains the value zero,options.name
does not contain a value.2 Returns: A reflection of a description of a declaration of a non-static data member having the type represented by
type
, and having the optional characteristics designated byoptions
.3 Remarks: The returned reflection value is primarily useful in conjunction with
define_class
. Certain other functions instd::meta
(e.g.,type_of
,identifier_of
) can also be used to query the characteristics indicated by the arguments provided todata_member_spec
.consteval bool is_data_member_spec(info r);
4 Returns:
true
ifr
represents a description of a declaration of a non-static data member. Otherwise,false
.template <reflection_range R = initializer_list<info>> consteval info define_class(info class_type, R&& mdescrs);
5 Constant When: Letting
C
be the class represented byclass_type
andrK
be theK
th reflection value inmdescrs
,
- (5.1)
C
is incomplete from every point in the evaluation context,- (5.2)
is_data_member_spec(rK)
istrue
for everyrK
inmdescrs
, and- (5.3) the type represented by
type_of(rK)
is a valid type for data members, for everyrK
inmdescrs
.[ Note 2:
C
could be a class template specialization for which there is no reachable definition. — end note ]6 Let {
ok
} be a sequence ofdata_member_options_t
values such thatrk), ok) == rk data_member_spec(type_of(
for every
rk
inmdescrs
.7 Effects: Produces an injected declaration
D
([expr.const]) that provides a definition forC
with properties as follows:
- (7.1) The target scope of
D
is the scope to whichC
belongs ([basic.scope.scope]).- (7.2) The locus of
D
follows immediately after the manifestly constant-evaluated expression currently under evaluation.- (7.3) If
C
is a specialization of a class templateT
, thenD
is is an explicit specialization ofT
.- (7.4)
D
contains a non-static data member corresponding to each reflection valuerK
inmdescrs
. For every otherrL
inmdescrs
such thatK < L
, the declaration ofrK
precedes the declaration ofrL
.- (7.5) The non-static data member corresponding to each
rK
is declared with the type represented bytype_of(rK)
.- (7.6) Non-static data members corresponding to reflections
rK
for whichoK.no_unique_address
istrue
are declared with the attribute[[no_unique_address]]
.- (7.7) Non-static data members corresponding to reflections
rK
for whichoK.width
contains a value are declared as bit-fields whose width is that value.- (7.8) Non-static data members corresponding to reflections
rK
for whichoK.alignment
contains a value are declared with thealignment-specifier
alignas(oK.alignment)
.- (7.9) Non-static data members corresponding to reflections
rK
are declared with names determined as follows:
- If
oK.width
contains the value zero, the non-static data member is declared without a name.- Otherwise, if
has_identifier(rK)
isfalse
, the non-static data member is declared with an unnamed name.- Otherwise, the name of the non-static data member is the identifier determined by the character sequence encoded by
u8identifier_of(rK)
in UTF-8.- (7.10) If
C
is a union type for which any of its members are not trivially default constructible, then it has a user-provided default constructor which has no effect.- (7.11) If
C
is a union type for which any of its members are not trivially default destructible, then it has a user-provided default destructor which has no effect.8 Returns:
class_type
.
consteval const char* define_static_string(string_view str); consteval const char8_t* define_static_string(u8string_view str);
1 Let
S
be a constexpr variable of array type with static storage duration, whose elements are of typeconst char
orconst char8_t
respectively, for which there exists somek
≥0
such that:2 Returns:
&S[k]
3 Implementations are encouraged to return the same object whenever the same variant of these functions is called with the same argument.
template<ranges::input_range R> consteval span<const ranges::range_value_t<R>> define_static_array(R&& r);
4 Constraints:
is_constructible_v<ranges::range_value_t<R>, ranges::range_reference_t<R>>
istrue
.5 Let
D
beranges::distance(r)
andS
be a constexpr variable of array type with static storage duration, whose elements are of typeconst ranges::range_value_t<R>
, for which there exists somek
≥0
such thatS[k + i] == r[i]
for all 0 ≤i
<D
.6 Returns:
span(addressof(S[k]), D)
7 Implementations are encouraged to return the same object whenever the same the function is called with the same argument.
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 containstype
, 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 ortypedef-name
. For each function taking an argument namedtype_args
, a call to the function is a non-constant library call if anymeta::info
in that range is not a reflection of a type or atypedef-name
.
1 For any type or
typedef-name
T
, for each functionstd::meta::type_TRAIT
defined in this clause,std::meta::type_TRAIT(^T)
equals the value of the corresponding unary type traitstd::TRAIT_v<T>
as specified in 21.3.5.2 [meta.unary.cat].consteval bool type_is_void(info type); consteval bool type_is_null_pointer(info type); consteval bool type_is_integral(info type); consteval bool type_is_floating_point(info type); consteval bool type_is_array(info type); consteval bool type_is_pointer(info type); consteval bool type_is_lvalue_reference(info type); consteval bool type_is_rvalue_reference(info type); consteval bool type_is_member_object_pointer(info type); consteval bool type_is_member_function_pointer(info type); consteval bool type_is_enum(info type); consteval bool type_is_union(info type); consteval bool type_is_class(info type); consteval bool type_is_function(info type); consteval bool type_is_reflection(info type);
[ Example 1:— end example ]namespace std::meta { consteval bool type_is_void(info type) { // one example implementation return extract<bool>(substitute(^is_void_v, {type})); // another example implementation type = dealias(type); return type == ^void || type == ^const void || type == ^volatile void || type == ^const volatile void; } }
1 For any type or
typedef-name
T
, for each functionstd::meta::type_TRAIT
defined in this clause,std::meta::type_TRAIT(^T)
equals the value of the corresponding unary type traitstd::TRAIT_v<T>
as specified in 21.3.5.3 [meta.unary.comp].consteval bool type_is_reference(info type); consteval bool type_is_arithmetic(info type); consteval bool type_is_fundamental(info type); consteval bool type_is_object(info type); consteval bool type_is_scalar(info type); consteval bool type_is_compound(info type); consteval bool type_is_member_pointer(info type);
1 For any type or
typedef-name
T
, for each functionstd::meta::type_UNARY-TRAIT
defined in this clause with signaturebool(std::meta::info)
,std::meta::type_UNARY-TRAIT(^T)
equals the value of the corresponding type propertystd::UNARY-TRAIT_v<T>
as specified in 21.3.5.4 [meta.unary.prop].2 For any types or
typedef-names
T
andU
, for each functionstd::meta::type_BINARY-TRAIT
defined in this clause with signaturebool(std::meta::info, std::meta::info)
,std::meta::type_BINARY-TRAIT(^T, ^U)
equals the value of the corresponding type propertystd::BINARY-TRAIT_v<T, U>
as specified in 21.3.5.4 [meta.unary.prop].3 For any type or
typedef-name
T
, pack of types ortypedef-names
U...
, and ranger
such thatranges::to<vector>(r) == vector{^U...}
istrue
, for each function templatestd::meta::type_VARIADIC-TRAIT
defined in this clause,std::meta::type_VARIADIC-TRAIT(^T, r)
equals the value of the corresponding type propertystd::VARIADIC-TRAIT_v<T, U...>
as specified in 21.3.5.4 [meta.unary.prop].consteval bool type_is_const(info type); consteval bool type_is_volatile(info type); consteval bool type_is_trivial(info type); consteval bool type_is_trivially_copyable(info type); consteval bool type_is_standard_layout(info type); consteval bool type_is_empty(info type); consteval bool type_is_polymorphic(info type); consteval bool type_is_abstract(info type); consteval bool type_is_final(info type); consteval bool type_is_aggregate(info type); consteval bool type_is_signed(info type); consteval bool type_is_unsigned(info type); consteval bool type_is_bounded_array(info type); consteval bool type_is_unbounded_array(info type); consteval bool type_is_scoped_enum(info type); template <reflection_range R = initializer_list<info>> consteval bool type_is_constructible(info type, R&& type_args); consteval bool type_is_default_constructible(info type); consteval bool type_is_copy_constructible(info type); consteval bool type_is_move_constructible(info type); consteval bool type_is_assignable(info type_dst, info type_src); consteval bool type_is_copy_assignable(info type); consteval bool type_is_move_assignable(info type); consteval bool type_is_swappable_with(info type_dst, info type_src); consteval bool type_is_swappable(info type); consteval bool type_is_destructible(info type); template <reflection_range R = initializer_list<info>> consteval bool type_is_trivially_constructible(info type, R&& type_args); consteval bool type_is_trivially_default_constructible(info type); consteval bool type_is_trivially_copy_constructible(info type); consteval bool type_is_trivially_move_constructible(info type); consteval bool type_is_trivially_assignable(info type_dst, info type_src); consteval bool type_is_trivially_copy_assignable(info type); consteval bool type_is_trivially_move_assignable(info type); consteval bool type_is_trivially_destructible(info type); template <reflection_range R = initializer_list<info>> consteval bool type_is_nothrow_constructible(info type, R&& type_args); consteval bool type_is_nothrow_default_constructible(info type); consteval bool type_is_nothrow_copy_constructible(info type); consteval bool type_is_nothrow_move_constructible(info type); consteval bool type_is_nothrow_assignable(info type_dst, info type_src); consteval bool type_is_nothrow_copy_assignable(info type); consteval bool type_is_nothrow_move_assignable(info type); consteval bool type_is_nothrow_swappable_with(info type_dst, info type_src); consteval bool type_is_nothrow_swappable(info type); consteval bool type_is_nothrow_destructible(info type); consteval bool type_is_implicit_lifetime(info type); consteval bool type_has_virtual_destructor(info type); consteval bool type_has_unique_object_representations(info type); consteval bool type_reference_constructs_from_temporary(info type_dst, info type_src); consteval bool type_reference_converts_from_temporary(info type_dst, info type_src);
1 For any type or
typedef-name
T
, for each functionstd::meta::type_PROP
defined in this clause with signaturesize_t(std::meta::info)
,std::meta::type_PROP(^T)
equals the value of the corresponding type propertystd::PROP_v<T>
as specified in 21.3.6 [meta.unary.prop.query].2 For any type or
typedef-name
T
and unsigned integer valueI
,std::meta::type_extent(^T, I)
equalsstd::extent_v<T, I>
([meta.unary.prop.query]).consteval size_t type_alignment_of(info type); consteval size_t type_rank(info type); consteval size_t type_extent(info type, unsigned i = 0);
1 The consteval functions specified in this clause may be used to query relationships between types at compile time.
2 For any types or
typedef-name
T
andU
, for each functionstd::meta::type_REL
defined in this clause with signaturebool(std::meta::info, std::meta::info)
,std::meta::type_REL(^T, ^U)
equals the value of the corresponding type relationstd::REL_v<T, U>
as specified in 21.3.7 [meta.rel].3 For any type or
typedef-name
T
, pack of types ortypedef-names
U...
, and ranger
such thatranges::to<vector>(r) == vector{^U...}
istrue
, for each binary function templatestd::meta::type_VARIADIC-REL
,std::meta::type_VARIADIC-REL(^T, r)
equals the value of the corresponding type relationstd::VARIADIC-REL_v<T, U...>
as specified in 21.3.7 [meta.rel].4 For any types or
typedef-names
T
andR
, pack of types ortypedef-names
U...
, and ranger
such thatranges::to<vector>(r) == vector{^U...}
istrue
, for each ternary function templatestd::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 relationstd::VARIADIC-REL-R_v<R, T, U...>
as specified in 21.3.7 [meta.rel].consteval bool type_is_same(info type1, info type2); consteval bool type_is_base_of(info type_base, info type_derived); consteval bool type_is_convertible(info type_src, info type_dst); consteval bool type_is_nothrow_convertible(info type_src, info type_dst); consteval bool type_is_layout_compatible(info type1, info type2); consteval bool type_is_pointer_interconvertible_base_of(info type_base, info type_derived); template <reflection_range R = initializer_list<info>> consteval bool type_is_invocable(info type, R&& type_args); template <reflection_range R = initializer_list<info>> consteval bool type_is_invocable_r(info type_result, info type, R&& type_args); template <reflection_range R = initializer_list<info>> consteval bool type_is_nothrow_invocable(info type, R&& type_args); template <reflection_range R = initializer_list<info>> consteval bool type_is_nothrow_invocable_r(info type_result, info type, R&& type_args);
5 [ Note 1: If
t
is a reflection of the typeint
andu
is a reflection of an alias to the typeint
, thent == u
isfalse
buttype_is_same(t, u)
istrue
.t == dealias(u)
is alsotrue
. — 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
typedef-name
T
, for each functionstd::meta::type_MOD
defined in this clause,std::meta::type_MOD(^T)
returns the reflection of the corresponding typestd::MOD_t<T>
as specified in 21.3.8.2 [meta.trans.cv].consteval info type_remove_const(info type); consteval info type_remove_volatile(info type); consteval info type_remove_cv(info type); consteval info type_add_const(info type); consteval info type_add_volatile(info type); consteval info type_add_cv(info type);
1 For any type or
typedef-name
T
, for each functionstd::meta::type_MOD
defined in this clause,std::meta::type_MOD(^T)
returns the reflection of the corresponding typestd::MOD_t<T>
as specified in 21.3.8.3 [meta.trans.ref].consteval info type_remove_reference(info type); consteval info type_add_lvalue_reference(info type); consteval info type_add_rvalue_reference(info type);
1 For any type or
typedef-name
T
, for each functionstd::meta::type_MOD
defined in this clause,std::meta::type_MOD(^T)
returns the reflection of the corresponding typestd::MOD_t<T>
as specified in 21.3.8.4 [meta.trans.sign].consteval info type_make_signed(info type); consteval info type_make_unsigned(info type);
1 For any type or
typedef-name
T
, for each functionstd::meta::type_MOD
defined in this clause,std::meta::type_MOD(^T)
returns the reflection of the corresponding typestd::MOD_t<T>
as specified in 21.3.8.5 [meta.trans.arr].consteval info type_remove_extent(info type); consteval info type_remove_all_extents(info type);
1 For any type or
typedef-name
T
, for each functionstd::meta::type_MOD
defined in this clause,std::meta::type_MOD(^T)
returns the reflection of the corresponding typestd::MOD_t<T>
as specified in 21.3.8.6 [meta.trans.ptr].consteval info type_remove_pointer(info type); consteval info type_add_pointer(info type);
[ 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
typedef-name
T
, for each functionstd::meta::type_MOD
defined in this clause with signaturestd::meta::info(std::meta::info)
,std::meta::type_MOD(^T)
returns the reflection of the corresponding typestd::MOD_t<T>
as specified in 21.3.8.7 [meta.trans.other].2 For any pack of types or
typedef-names
T...
and ranger
such thatranges::to<vector>(r) == vector{^T...}
istrue
, for each unary function templatestd::meta::type_VARIADIC-MOD
defined in this clause,std::meta::type_VARIADIC-MOD(r)
returns the reflection of the corresponding typestd::VARIADIC-MOD_t<T...>
as specified in 21.3.8.7 [meta.trans.other].3 For any type or
typedef-name
T
, pack of types ortypedef-names
U...
, and ranger
such thatranges::to<vector>(r) == vector{^U...}
istrue
,std::meta::type_invoke_result(^T, r)
returns the reflection of the corresponding typestd::invoke_result_t<T, U...>
(21.3.8.7 [meta.trans.other]).consteval info type_remove_cvref(info type); consteval info type_decay(info type); template <reflection_range R = initializer_list<info>> consteval info type_common_type(R&& type_args); template <reflection_range R = initializer_list<info>> consteval info type_common_reference(R&& type_args); consteval info type_underlying_type(info type); template <reflection_range R = initializer_list<info>> consteval info type_invoke_result(info type, R&& type_args); consteval info type_unwrap_reference(info type); consteval info type_unwrap_ref_decay(info type);
[ Example 1:— end example ]// example implementation consteval info type_unwrap_reference(info type) { = dealias(type); type if (has_template_arguments(type) && template_of(type) == ^reference_wrapper) { return type_add_lvalue_reference(template_arguments_of(type)[0]); } else { return type; } }
1 For any type or
typedef-name
T
, for each functionstd::meta::type_UNARY-TRAIT
defined in this clause with the signaturesize_t(std::meta::info)
,std::meta::type_UNARY-TRAIT(^T)
equals the value of the corresponding propertystd::UNARY-TRAIT_v<T>
as defined in 22.4 [tuple] or 22.6 [variant].2 For any type or
typedef-name
T
and valueI
, for each functionstd::meta::type_BINARY-TRAIT
defined in this clause with the signatureinfo(size_t, std::meta::info)
,std::meta::type_BINARY-TRAIT(I, ^T)
returns a reflection representing the typestd::BINARY-TRAIT_t<I, T>
as defined in 22.4 [tuple] or 22.6 [variant].consteval size_t type_tuple_size(info type); consteval info type_tuple_element(size_t index, info type); consteval size_t type_variant_size(info type); consteval info type_variant_alternative(size_t index, info type);
bit_cast
And we have adjust the requirements of std::bit_cast
to
not allow casting to or from std::meta::info
as a constant, in 22.15.3 [bit.cast]/3:
3 Remarks: This function is constexpr if and only if
To
,From
, and the types of all subobjects ofTo
andFrom
are typesT
such that:
This is a feature with both a language and library component. Our
usual practice is to provide something like
__cpp_impl_reflection
and
__cpp_lib_reflection
for this. But
since the two pieces are so closely tied together, maybe it really only
makes sense to provide one?
For now, we’ll add both.
To 15.11 [cpp.predefined]:
__cpp_impl_coroutine 201902L __cpp_impl_destroying_delete 201806L __cpp_impl_three_way_comparison 201907L+ __cpp_impl_reflection 2024XXL
and 17.3.2 [version.syn]:
+ #define __cpp_lib_reflection 2024XXL // also in <meta>