Reflection for C++26

Document #: P2996R8 [Latest] [Status]
Date: 2024-12-16
Project: Programming Language C++
Audience: CWG, LEWG, LWG
Reply-to: Wyatt Childers
<>
Peter Dimov
<>
Dan Katz
<>
Barry Revzin
<>
Andrew Sutton
<>
Faisal Vali
<>
Daveed Vandevoorde
<>

Contents

1 Revision History

Since [P2996R7]:

Since [P2996R6]:

Since [P2996R5]:

Since [P2996R4]:

Since [P2996R3]:

Since [P2996R2]:

Since [P2996R1], several changes to the overall library API:

Other paper changes:

Since [P2996R0]:

2 Introduction

This is a proposal for a reduced initial set of features to support static reflection in C++. Specifically, we are mostly proposing a subset of features suggested in [P1240R2]:

(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.

2.1 Notable Additions to P1240

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.

2.2 Why a single opaque reflection type?

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:

2.3 Implementation Status

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>
  replicator_type<vals...> replicator = {};
}

template<typename R>
consteval auto expand(R range) {
  std::vector<std::meta::info> args;
  for (auto r : range) {
    args.push_back(reflect_value(r));
  }
  return substitute(^^__impl::replicator, args);
}

Used like:

With expansion statements
With expand workaround
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>";
}
template<typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  std::string result = "<unnamed>";
  [:expand(std::meta::enumerators_of(^^E)):] >> [&]<auto e>{
    if (value == [:e:]) {
      result = std::meta::identifier_of(e);
    }
  };
  return result;
}

3 Examples

We start with a number of examples that show off what is possible with the proposed set of features. It is expected that these are mostly self-explanatory. Read ahead to the next sections for a more systematic description of each element of this proposal.

A number of our examples here show a few other language features that we hope to progress at the same time. This facility does not strictly rely on these features, and it is possible to do without them - but it would greatly help the usability experience if those could be adopted as well:

3.1 Back-And-Forth

Our first example is not meant to be compelling but to show how to go back and forth between the reflection domain and the grammatical domain:

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.

3.2 Selecting Members

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() {
  S s{0, 0};
  s.[:member_number(1):] = 42;  // Same as: s.j = 42;
  s.[:member_number(5):] = 0;   // Error (member_number(5) is not a constant).
}

This 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() {
  S s{0, 0};
  s.[:member_number(1):] = 42;  // Same as: s.j = 42;
  s.[:member_number(5):] = 0;   // Error (member_number(5) is not a constant).
}

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() {
  S s{0, 0};
  s.[:member_named("j"):] = 42;  // Same as: s.j = 42;
  s.[:member_named("x"):] = 0;   // Error (member_named("x") is not a constant).
}

On Compiler Explorer: EDG, Clang.

3.3 List of Types to List of Sizes

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 = []{
  std::array<std::size_t, types.size()> r;
  std::views::transform(types, r.begin(), std::meta::size_of);
  return 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.

3.4 Implementing 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) {
  std::vector args{^^T};
  for (T k = 0; k < N; ++k) {
    args.push_back(std::meta::reflect_value(k));
  }
  return substitute(^^std::integer_sequence, args);
}

template<typename T, T N>
  using make_integer_sequence = [:make_integer_seq_refl<T>(N):];

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).

3.5 Getting Class Layout

struct member_descriptor
{
  std::size_t offset;
  std::size_t size;
};

// returns std::array<member_descriptor, N>
template <typename S>
consteval auto get_layout() {
  constexpr auto members = nonstatic_data_members_of(^^S);
  std::array<member_descriptor, members.size()> layout;
  for (int i = 0; i < members.size(); ++i) {
      layout[i] = {.offset=offset_of(members[i]).bytes, .size=size_of(members[i])};
  }
  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.

3.6 Enum to String

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:

3.7 Parsing Command-Line Options

Our 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()) {
      std::print(stderr, "Option {} is missing a value\n", *it);
      std::exit(EXIT_FAILURE);
    }

    using T = typename[:type_of(dm):];
    auto iss = std::ispanstream(it[1]);
    if (iss >> opts.[:dm:]; !iss) {
      std::print(stderr, "Failed to parse option {} into a {}\n", *it, display_string_of(^^T));
      std::exit(EXIT_FAILURE);
    }
  }
  return opts;
}

struct MyOpts {
  std::string file_name = "input.txt";  // Option "--file_name <string>"
  int    count = 1;                     // Option "--count <int>"
};

int main(int argc, char *argv[]) {
  MyOpts opts = parse_options<MyOpts>(std::vector<std::string_view>(argv+1, argv+argc));
  // ...
}

This example is based on a presentation by Matúš Chochlík.

On Compiler Explorer: EDG, Clang.

3.8 A Simple Tuple Type

#include <meta>

template<typename... Ts> struct Tuple {
  struct storage;

  static_assert(is_type(define_aggregate(^^storage, {data_member_spec(^^Ts)...})));
  storage data;

  Tuple(): data{} {}
  Tuple(Ts const& ...vs): data{ vs... } {}
};

template<typename... Ts>
  struct std::tuple_size<Tuple<Ts...>>: public integral_constant<size_t, sizeof...(Ts)> {};

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_aggregate 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_aggregate 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.

3.9 A Simple Variant Type

Similarly to how we can implement a tuple using define_aggregate 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;
  std::string s;
};

U1 has a trivial destructor, but U2’s destructor is defined as deleted (because std::string has a non-trivial destructor). This is a problem because we need to define this thing… somehow. However, for the purposes of define_aggregate, there really is only one reasonable option to choose here:

template <class... Ts>
union U {
  // all of our members
  Ts... members;

  // 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_aggregate 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_aggregate(^^Storage, {
        data_member_spec(^^Empty, {.name="empty"}),
        data_member_spec(^^Ts)...
    })));

    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={}}
        , index_(0)
    {
        std::construct_at(&storage_.[: get_nth_field(0) :]);
    }

    constexpr ~Variant() requires (std::is_trivially_destructible_v<Ts> and ...) = default;
    constexpr ~Variant() {
        if (index_ != -1) {
            with_index([&](auto I){
                std::destroy_at(&storage_.[: get_nth_field(I) :]);
            });
        }
    }

    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={}}
        , index_(-1)
    {
        std::construct_at(&storage_.[: get_nth_field(I) :], (T&&)t);
        index_ = (int)I;
    }

    // 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={}}
        , index_(-1)
    {
        rhs.with_index([&](auto I){
            constexpr auto field = get_nth_field(I);
            std::construct_at(&storage_.[: field :], rhs.storage_.[: field :]);
            index_ = I;
        });
    }

    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.

3.10 Struct to Struct of Arrays

#include <meta>
#include <array>

template <typename T, size_t N>
struct struct_of_arrays_impl {
  struct impl;

  consteval {
    std::vector<std::meta::info> old_members = nonstatic_data_members_of(^^T);
    std::vector<std::meta::info> new_members = {};
    for (std::meta::info member : old_members) {
        auto array_type = substitute(^^std::array, {
            type_of(member),
            std::meta::reflect_value(N),
        });
        auto mem_descr = data_member_spec(array_type, {.name = identifier_of(member)});
        new_members.push_back(mem_descr);
    }

    define_aggregate(^^impl, new_members);
  }
};

template <typename T, size_t N>
using struct_of_arrays = struct_of_arrays_impl<T, N>::impl;

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_aggregate is put to good use.

This example also illustrates some requirements that we have on define_aggregate. In particular, that function is said to produce an “injected declaration” and the target scope of the declaration must be “semantically sequenced” with the evaluation that produced it. Which means that the following similar structure is ill-formed:

template <class T, size_t N>
struct struct_of_arrays_impl;

template <typename T, size_t N>
using struct_of_arrays = [: []{
  // ... same logic ..

  // error: the target scope of this declaration is a
  // different instantiation from the one we are currently in.
  define_aggregate(^^struct_of_arrays_impl<T, N>, new_members);
}() :];

That could be fixed if we reorganize it like this:

template <typename T, size_t N>
using struct_of_arrays = [: []{
  // ... same logic ..

  // OK, same instantiation
  struct impl;
  define_aggregate(^^impl, new_members);
}() :];

But now struct_of_arrays<point, 30> has no linkage, whereas we wanted it to have external linkage. Hence the structure in the example above where we are instead defining a nested class in a class template — so that we have a type with external linkage but don’t run afoul of the semantically sequenced rule.

On Compiler Explorer: EDG, Clang.

3.11 Parsing Command-Line Options II

Now that we’ve seen a couple examples of using std::meta::define_aggregate 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 {
  Option<std::string, {.use_short=true, .use_long=true}> name;
  Option<int, {.use_short=true, .use_long=true}> count = 1;
};

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
    std::print("Hello {}!", opts.name);   // opts.name has type std::string
  }
}

Which we can implement like this:

struct Flags {
  bool use_short;
  bool use_long;
};

template <typename T, Flags flags>
struct Option {
  std::optional<T> initializer = {};

  // 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,
                            std::meta::info spec) -> std::meta::info {
  std::vector<std::meta::info> new_members;
  for (std::meta::info member : nonstatic_data_members_of(spec)) {
    auto type_new = template_arguments_of(type_of(member))[0];
    new_members.push_back(data_member_spec(type_new, {.name=identifier_of(member)}));
  }
  return define_aggregate(opts, new_members);
}

struct Clap {
  template <typename Spec>
  auto parse(this Spec const& spec, int argc, char** argv) {
    std::vector<std::string_view> cmdline(argv+1, argv+argc)

    // 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),
                                                            nonstatic_data_members_of(^^Opts))) {
      auto 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
          opts.[:om:] = *cur.initializer;
          continue;
        } else {
          std::print(stderr, "Missing required option {}\n", display_string_of(sm));
          std::exit(EXIT_FAILURE);
        }
      } else if (it + 1 == cmdline.end()) {
        std::print(stderr, "Option {} for {} is missing a value\n", *it, display_string_of(sm));
        std::exit(EXIT_FAILURE);
      }

      // found our argument, try to parse it
      auto iss = ispanstream(it[1]);
      if (iss >> opts.[:om:]; !iss) {
        std::print(stderr, "Failed to parse {:?} into option {} of type {}\n",
          it[1], display_string_of(sm), display_string_of(type));
        std::exit(EXIT_FAILURE);
      }
    }
    return opts;
  }
};

On Compiler Explorer: EDG, Clang.

3.12 A Universal Formatter

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++ = ' ';
      }
      first = false;
    };

    template for (constexpr auto base : bases_of(^^T)) {
      delim();
      out = std::format_to(out, "{}", (typename [: type_of(base) :] const&)(t));
    }

    template for (constexpr auto mem : nonstatic_data_members_of(^^T)) {
      delim();
      std::string_view mem_label = has_identifier(mem) ? identifier_of(mem)
                                                       : "(unnamed-member)";
      out = std::format_to(out, ".{}={}", mem_label, t.[:mem:]);
    }

    *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() {
    std::println("{}", Z());
      // 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:

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.

3.13 Implementing member-wise 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)) {
      hash_append(algo, t.[:mem:]);
  }
}

3.14 Converting a Struct to a Tuple

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 = []{
    std::array<int, members.size()> indices;
    std::ranges::iota(indices, 0);
    return 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,
                    nonstatic_data_members_of(type)
                    | std::views::transform(std::meta::type_of)
                    | std::views::transform(std::meta::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): ];

  std::vector args = {^^To, ^^From};
  for (auto mem : nonstatic_data_members_of(^^From)) {
    args.push_back(reflect_value(mem));
  }

  /*
  Alternatively, with Ranges:
  args.append_range(
    nonstatic_data_members_of(^^From)
    | std::views::transform(std::meta::reflect_value)
    );
  */

  return extract<To(*)(From const&)>(
    substitute(^^struct_to_tuple_helper, args));
}

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.

3.15 Implementing 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<
       std::tuple_element_t<
         indices.second,
         std::remove_cvref_t<std::tuple_element_t<indices.first, std::remove_cvref_t<Tuples>>>
       >...
     >;
     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
{
    std::vector<std::meta::info> a2;
    for (T x : args) {
        a2.push_back(std::meta::reflect_value(x));
    }

    return substitute(tmpl, a2);
}

consteval auto make_indexer(std::vector<std::size_t> sizes)
    -> std::meta::info
{
    std::vector<std::pair<int, int>> args;

    for (std::size_t tidx = 0; tidx < sizes.size(); ++tidx) {
        for (std::size_t eidx = 0; eidx < sizes[tidx]; ++eidx) {
            args.push_back({tidx, eidx});
        }
    }

    return subst_by_value(^^Indexer, args);
}

template<typename... Tuples>
auto my_tuple_cat(Tuples&&... tuples) {
    constexpr typename [: make_indexer({tuple_size(remove_cvref(^^Tuples))...}) :] indexer;
    return indexer(std::forward_as_tuple(std::forward<Tuples>(tuples)...));
}

3.16 Named Tuple

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:

  1. Can introduce a pair type so that we can write make_named_tuple<pair<int, "x">, pair<double, "y">>(), or
  2. Can just do reflections all the way down so that we can write
make_named_tuple<^^int, std::meta::reflect_value("x"),
                 ^^double, std::meta::reflect_value("y")>()

We do not currently support splicing string literals, and the pair approach follows the similar pattern already shown with define_aggregate (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) {
    std::vector<std::meta::info> nsdms;
    auto f = [&]<class Tag>(Tag tag){
        nsdms.push_back(data_member_spec(
            dealias(^^typename Tag::type),
            {.name=Tag::name()}));

    };
    (f(tags), ...);
    return define_aggregate(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,
                                std::initializer_list<std::pair<std::meta::info, std::string_view>> members) {
    std::vector<std::meta::data_member_spec> nsdms;
    for (auto [type, name] : members) {
        nsdms.push_back(data_member_spec(type, {.name=name}));
    }
    return define_aggregate(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, and the updated name define_aggregate).

3.17 Compile-Time Ticket Counter

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>'.
    std::meta::info r;
    while (is_complete_type(r = substitute(^^Helper,
                                           { std::meta::reflect_value(k) })))
      ++k;

    // Define 'Helper<k>' and return its index.
    define_aggregate(r, {});
    return 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.

4 Proposed Features

4.1 The Reflection Operator (^^)

The reflection operator produces a reflection value from a grammatical construct (its operand):

unary-expression:
      …
      ^^ ::
      ^^ namespace-name
      ^^ type-id
      ^^ id-expression

The expression ^^:: evaluates to a reflection of the global namespace. When the operand is a namespace-name or type-id, the resulting value is a reflection of the designated namespace or type.

When the operand is an id-expression, the resulting value is a reflection of the designated entity found by lookup. This might be any of:

For all other operands, the expression is ill-formed. In a SFINAE context, a failure to substitute the operand of a reflection operator construct causes that construct to not evaluate to constant.

Earlier revisions of this paper allowed for taking the reflection of any cast-expression that could be evaluated as a constant expression, as we believed that a constant expression could be internally “represented” by just capturing the value to which it evaluated. However, the possibility of side effects from constant evaluation (introduced by this very paper) renders this approach infeasible: even a constant expression would have to be evaluated every time it’s spliced. It was ultimately decided to defer all support for expression reflection, but we intend to introduce it through a future paper using the syntax ^^(expr).

This paper does, however, support reflections of values and of objects (including subobjects). Such reflections arise naturally when iterating over template arguments.

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.

4.1.1 Syntax discussion

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.

In Wrocław 2024, SG7 and EWG voted to adopt ^^ as the new reflection operator (as proposed by [P3381R0]). The R8 revision of this paper integrates that change.

4.2 Splicers ([::])

A reflection can be “spliced” into source code using one of several splicer forms:

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.

4.2.1 Addressed Splicing

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:

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 {
    X(int, int);
};

And let rc be a reflection of the constructor and rd be a reflection of the destructor. The sensible syntax and semantics for how you would use rc and rd should be as follows:

auto x = [: rc :](1, 2); // gives you an X
x.[: rd :]();            // destroys it

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.

4.2.2 Limitations

Splicers can appear in many contexts, but our implementation experience has uncovered a small set of circumstances in which a splicer must be disallowed. Mostly these are because any entity designated by a splicer can be dependent on a template argument, so any context in which the language already disallows a dependent name must also disallow a dependent splicer. It also becomes possible for the first time to have the “name” of a namespace or concept become dependent on a 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.

4.2.2.1 Splicing reflections of constructors

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.

4.2.2.2 Splicing namespaces in namespace definitions

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.

4.2.2.3 Splicing namespaces in using-directives and using-enum-declarators

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.

4.2.2.4 Splicing concepts in declarations of template parameters

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).

4.2.2.5 Splicing class members as designators in designated-initializer-lists

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.

4.2.3 Range Splicers

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
template <typename T>
constexpr auto struct_to_tuple(T const& t) {
  constexpr auto members = nonstatic_data_members_of(^^T);

  constexpr auto indices = []{
    std::array<int, members.size()> indices;
    std::ranges::iota(indices, 0);
    return indices;
  }();

  constexpr auto [...Is] = indices;
  return std::make_tuple(t.[: members[Is] :]...);
}
template <typename T>
constexpr auto struct_to_tuple(T const& t) {
  constexpr auto members = nonstatic_data_members_of(^^T);
  return std::make_tuple(t.[: ...members :]...);
}

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

make_tuple(t.[: ... members :]...)

would evaluate as

make_tuple(t.[:members[0]:], t.[:members[1]:], ..., t.[:members[N-1]:])

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] :]...);
  });
}

4.2.4 Syntax discussion

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:

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.

4.3 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:

  1. The compiler must know how to mangle any reflectable value (i.e., when a reflection thereof is used as a template argument).
  2. The compiler must know how to compare any two reflectable values, ideally without interpreting user-defined comparison operators (i.e., to implement comparison between reflections).

Values of structural types can already be used as template arguments (so implementations must already know how to mangle them), and the notion of template-argument-equivalent values defined on the class of structural types helps guarantee that &fn<^^value1> == &fn<^^value2> if and only if &fn<value1> == &fn<value2>.

Notably absent at this time are reflections of expressions. For example, one might wish to walk over the subexpressions of a function call:

template <typename T> void fn(T) {}

void g() {
  constexpr auto call = ^^(fn(42));
  static_assert(
      template_arguments_of(function_of(call))[0] ==
      ^^int);
}

Previous revisions of this proposal suggested limited support for reflections of constant expressions. The introduction of side effects from constant evaluations (by this very paper), however, renders this roughly as difficult for constant expressions as it is for non-constant expressions. We instead defer all expression reflection to a future paper, and only present value and object reflection in the present proposal.

4.3.1 Comparing reflections

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.

4.3.2 The associated std::meta namespace

The namespace std::meta is an associated type of std::meta::info, which allows standard library meta functions to be invoked without explicit qualification. For example:

#include <meta>
struct S {};
std::string name2 = std::meta::identifier_of(^^S);  // Okay.
std::string name1 = identifier_of(^^S);             // Also okay.

Default constructing or value-initializing an object of type std::meta::info gives it a null reflection value. A null reflection value is equal to any other null reflection value and is different from any other reflection that refers to one of the mentioned entities. For example:

#include <meta>
struct S {};
static_assert(std::meta::info() == std::meta::info());
static_assert(std::meta::info() != ^^S);

4.4 Metafunctions

We propose a number of metafunctions declared in namespace std::meta to operator on reflection values. Adding metafunctions to an implementation is expected to be relatively “easy” compared to implementing the core language features described previously. However, despite offering a normal consteval C++ function interface, each on of these relies on “compiler magic” to a significant extent.

4.4.1 Constant evaluation order

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_aggregate 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_aggregate(^^S, {})));
  S s;  // S should be defined at this point.
}

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 non-dependent 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].

4.4.2 Error-Handling in Reflection

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:

  1. Returning an invalid reflection (similar to NaN for floating point) which carries source location info and some useful message (i.e., the approach suggested by P1240)
  2. Returning a std::expected<std::meta::info, E> for some reflection-specific error type E, which carries source location info and some useful message
  3. Failing to be a constant expression
  4. Throwing an exception of type E, which requires a language extension for such exceptions to be catchable during constexpr evaluation

We found that we disliked (1) since there is no satisfying value that can be returned for a call like template_arguments_of(^^int): We could return a std::vector<std::meta::info> having a single invalid reflection, but this makes for awkward error handling. The experience offered by (3) is at least consistent, but provides no immediate means for a user to “recover” from an error.

Either std::expected or constexpr exceptions would allow for a consistent and straightforward interface. Deciding between the two, we noticed that many of usual concerns about exceptions do not apply during translation:

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

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.

4.4.3 Range-Based Metafunctions

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:

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:

  1. Accept std::span<std::meta::info const>, which now accepts braced-init-list arguments so it’s pretty convenient in this regard.
  2. Accept std::vector<std::meta::info>
  3. Accept any range whose type_value is std::meta::info.

Now, for compiler efficiency reasons, it’s definitely better to have all the arguments contiguously. So the compiler wants span (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
consteval auto type_struct_to_tuple(info type) -> meta::info {
    return substitute(
        ^^tuple,
        nonstatic_data_members_of(type)
        | views::transform(meta::type_of)
        | views::transform(meta::remove_cvref)
        | ranges::to<vector>());
}
consteval auto type_struct_to_tuple(info type) -> meta::info {
    return substitute(
        ^^tuple,
        nonstatic_data_members_of(type)
        | views::transform(meta::type_of)
        | views::transform(meta::remove_cvref)
        );
}

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.

4.4.4 Handling Aliases

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:

What about when querying the type of an entity?

std::string Str;
const 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:

  1. 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.

  2. 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.

4.4.5 Reflecting source text

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() {
  std::cout << u8"こんにちは世界\n";
}

is not standard C++ because the standard output stream does not have support for UTF-8 literals.

In practice ordinary strings encoded in the “ordinary 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:

  1. the internal representation used, e.g., in the compiler front end’s AST-like structures (persistent)

  2. the representation of string literals in the AST (persistent)

  3. 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;
  std::cout << identifier_of(^^hello_world) << "\n";  // Doesn't work if identifier_of produces a std::string.
}

We can instead return a std::string_view or std::u8string_view, but that has the downside that it effectively makes all results of querying source text persistent for the compilation.

For now, however, we propose that queries like 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:

  1. the query fails to evaluate,

  2. any unrepresentable source characters are translated to a different presentation, such as universal-character-names of the form \u{ hex-number },

  3. any source characters not in the basic source character set are translated to a different presentation (as in (2)).

Following much discussion with SG16, we propose #1: The query fails to evaluate if the identifier cannot be represented in the ordinary literal encoding.

4.4.6 Reflecting names

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.

4.4.7 Reachability and injected declarations

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 expression E comprises […] the instantiation context of M ([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_aggregate.

consteval std::meta::info make_defn(std::meta::info Cls, std::meta::info Mem) {
  // Synthesizes:
  //   struct Mem {};
  //   struct Cls { Mem m; };
  return /*P1*/ define_aggregate(Cls, {
    data_member_spec(/*P2*/ define_aggregate(Mem, {}), {.name="m"})
  });
}

/* 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.

  1. How can definitions for C and M be defined from P1 and P2 when no declarations of those classes are reachable from those program points?
  2. Where are the points of declaration for the generated definitions of C and M (i.e., from what program points will the generated definitions be reachable)?
  3. How can we ensure that the definition of M is reachable during the evaluation of define_aggregate on C?

The prior discourse regarding members_of gives a straightforward answer to (1); the define_aggregate 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_aggregate 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_aggregate(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_aggregate(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 synthesized point. Injected points have a special property: the only declaration reachable from a synthesized point is its corresponding injected declaration. Jumping back to our above example, joining the synthesized 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 expression E comprises the union of

  • (22.1) the instantiation context of M ([module.context]), and
  • (22.2) the synthesized points corresponding to any injected declarations ([expr.const]) produced by evaluations sequenced before the next evaluation of E.

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_aggregate 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.

4.4.8 Restrictions on injected declarations

The advancement of this proposal through WG21 has naturally led to increased scrutiny of the mechanisms here proposed. One such area is the possibility of leveraging injected declarations to observe failed template substitutions. Consider the following example:

struct S;

template <typename> struct TCls {
  static consteval bool sfn()  // #1
      requires ([] {
        consteval {
          define_aggregate(^^S, {});
        }
      }(), false) {
    return false;  // never selected
  }

  static consteval bool sfn()  // #2
      requires (true) {
    return true;   // always selected
  }
};

static_assert(TCls<void>::sfn());
static_assert(is_complete_type(^^S));

The above example observes the effects of the failed substitution of #1 by way of the completeness of S. Such tricks can be used to observe implementation details, like the order in which overloads are checked, that may be unportable (and which implementations might desire to change over time).

Our proposed solution, specified in [expr.const]/23.2, is to make it ill-formed to produce an injected declaration from a manifestly constant-evaluated expression inside of an instantiation to outside of that instantiation, or visa versa. Because that expression in the example above (define_aggregate(^^S, {})) is within the instantiation of the requires clause of TCls<void>::sfn, and the target scope of the injected declaration is outside of that same instantiaton, the example becomes ill-formed (diagnostic required). Note that this does not prevent writing consteval function templates that wrap define_aggregate:

template <std::meta::info R> consteval bool tfn() {
  define_aggregate(R, {});
  return true;
}

struct S;
constexpr bool b = tfn<^^S>();
  // OK, both manifestly constant-evaluated expression tfn<^^S>() and target scope of
  // injected declaration for 'S' are in the global namespace

Nor does this rule prevent a class template from producing a declaration whose target scope is the same specialization.

template <typename> struct TCls1 {
  struct Incomplete;

  consteval {
    define_aggregate(^^Incomplete, {});
      // OK, Incomplete is in the same instantiation as the define_aggregate call
  }

  static constexpr bool b = false;
};

template <typename T> struct TCls2 {
  static consteval bool sfn()  // #1
      requires (TCls1<T>::b) {
    return false;  // never selected
  }

  static consteval bool sfn()  // #2
      requires (true) {
    return true;   // always selected
  }
};

static_assert(TCls<void>::sfn());

Athough the instantiation of TCls1<void> in the requires-clause of #1 causes an injected declaration to be produced, it is not discernibly a side-effect of the failed substitution: Observing the side effect will first require one to write (some moral equivalent of) TCLs1<void>::Incomplete, the act of which would otherwise itself trigger the same side-effect.

Although this rule constrains the manner with which define_aggregate can be used, we are not aware of any motivating use cases for P2996 that are harmed. Worth mentioning, however: the rule has more dire implications for other code injection papers being considered by WG21, most notably [P3294R2] (“Code Injection With Token Sequences”). With this rule as it is, it becomes impossible for e.g., the instantiation of a class template specialization TCls<Foo> to produce an injected declaration of std::formatter<TCls<Foo>> (since the target scope would be the global namespace).

In this context, we do believe that relaxations of the rule can be considered: For instance, we ought to be able to say that the instantiation of std::formatter<TCls<Foo>> is sequenced strictly after the instantiation of TCls<Foo>, and observations such as these might make it possible to permit such injections without making it “discernible” whether they resulted from failed substitutions. The key to such an approach would be to define a partial order over the instantiations of a program, and to extend the semantically sequenced relation introduced by this proposal ([lex.phases]/7) to apply to constructs across instantiations when the relative order of their respective instantiations is defined.

All of that said, these relaxations are not needed for the code injection introduced by this proposal, and we do not seek to introduce them at this time.

4.4.9 Freestanding implementations

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.

4.4.10 Synopsis

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 expression results
  template <typename T>
    consteval auto reflect_value(const 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_aggregate
  struct data_member_options;
  consteval auto data_member_spec(info type_class,
                                  data_member_options options) -> info;
  template <reflection_range R = initializer_list<info>>
    consteval auto define_aggregate(info type_class, R&&) -> info;

  // data layout
  struct member_offset {
    ptrdiff_t bytes;
    ptrdiff_t bits;
    constexpr auto total_bits() const -> ptrdiff_t;
    auto operator<=>(member_offset const&) const = default;
  };

  consteval auto offset_of(info r) -> member_offset;
  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;

}

4.4.11 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.

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

4.4.13 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.

4.4.14 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:

std::vector<int> v = {1, 2, 3};
static_assert(template_of(type_of(^^v)) == ^^std::vector);
static_assert(template_arguments_of(type_of(^^v))[0] == ^^int);

4.4.15 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.

4.4.16 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.

4.4.17 reflect_value, reflect_object, reflect_function

namespace std::meta {
  template<typename T> consteval auto reflect_value(const 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)()) {
  std::meta::info rfn = std::meta::reflect_function(*fn);

  return (has_external_linkage(rfn) && parent_of(rfn) == ^^::);
}

4.4.18 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.

4.4.19 data_member_spec, define_aggregate

namespace std::meta {
  struct data_member_options {
    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 &&);
    };

    optional<name_type> name;
    optional<int> alignment;
    optional<int> bit_width;
    bool no_unique_address = false;
  };
  consteval auto data_member_spec(info type,
                                  data_member_options options) -> info;
  template <reflection_range R = initializer_list<info>>
  consteval auto define_aggregate(info type_class, R&&) -> info;
}

data_member_spec returns a reflection of a data member description for 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_aggregate 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_aggregate(^^U, {
  data_member_spec(^^int),
  data_member_spec(^^char),
  data_member_spec(^^double),
})));

// 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_aggregate(^^S<int>, {
  data_member_spec(^^int, {.name="i", .alignment=64}),
  data_member_spec(^^int, {.name=u8"こんにち"}),
});

// 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_aggregate for member functions.

If define_aggregate is called multiple times with the same arguments, all calls after the first will have no effect. Calling define_aggregate for a type that was defined using other arguments, defined through other means, or is in the process of being defined, is not a constant expression.

Revisions of this paper prior to P2996R8 named this function define_class. We find define_aggregate to be a better name for a few reasons:

  1. The capabilities of the function are quite limited, and are mostly good for constructing aggregate types.
  2. The new name provides good cause for forcing all data members to be public. Private data members created through such an interface are of very limited utility.
  3. The name define_class is left available for a future, more fully-featured API.

4.4.20 Data Layout Reflection

namespace std::meta {
  struct member_offset {
    ptrdiff_t bytes;
    ptrdiff_t bits;

    constexpr auto total_bits() const -> ptrdiff_t {
      return CHAR_BIT * bytes + bits;
    }

    auto operator<=>(member_offset const&) const = default;
  };

  consteval auto offset_of(info r) -> member_offset;
  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.

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_offset{0, 0});
static_assert(offset_of(^^Msg::b) == member_offset{1, 2});
static_assert(offset_of(^^Msg::c) == member_offset{2, 2});
static_assert(offset_of(^^Msg::d) == member_offset{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);

4.4.21 Other Type Traits

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::remove_cvref(t) as if that exists. Technically, the functionality isn’t strictly necessary - since it can be provided indirectly:

Direct
Indirect
remove_cvref(type)
dealias(substitute(^^std::remove_cvref_t, {type}))
is_const_type(type)
extract<bool>(substitute(^^std::is_const_v, {type}))

The indirect approach is a lot more typing, and you have to remember to dealias the result of the type traits as well (because substitute(^^std::remove_cvref_t, {^^int const}) gives you a reflection of an alias to int, not a reflection of int), so it’s both more tedious and more error prone.

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 we initially tried to do. However, we’ve run into some conflicts.

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 the name is_function. It could be:

  1. A consteval function equivalent of the type trait std::is_function<T>, such that std::meta::is_function(e) mandates that e represents a type and checks if that type is a function type.

  2. A new kind of reflection query std::meta::is_function(e) which asks if e is the reflection of a function (as opposed to a type or a namespace or a template, etc.). This is the same category of query as std::meta::is_template or std::meta::is_concept or std::meta::is_namespace.

Both of these are useful, yet they mean different things entirely - the first is ill-formed when passed a reflection of a function (as opposed to a function type), and the second would simply answer false for the reflection of any type (function type or otherwise).

Moreover, in this case it’s actually important that the reflection query std::meta::is_function does not return true for a function type so that using is_function as a filter for members_of does the expected thing — only giving you back functions, rather than also types.

A similar kind of clash could occur with other functions — for instance, we don’t have an is_array(r) right now that would check if r were the reflection of an array (as opposed to an array type), but we could in the future.

There are a few other examples of name clashes where we want the reflection query to apply to more inputs than simply types. For example, the type trait std::is_final can only ask if a type is a final class type, but the metafunction std::meta::is_final can ask if a member function is a final member function. Likewise std::meta::is_const can apply to objects or types too, and so forth.

The question becomes — how can we incorporate the type traits into the consteval metafunction domain while avoiding these name clash issues. We know of a few approaches.

  1. Put all the type traits in their own namespace, like std::meta::traits::meow. This has the benefit that we preserve the existing name, but now we lose ADL. We can’t write traits::remove_cvref(type) unless we bring in traits as a namespace alias for std::meta::traits, and if we bring in the entire namespace then we’re back to the name clash problem (it’s just that now the calls become ambiguous).

  2. Add a prefix or suffix to every type trait. This preserves the ability to use ADL and makes the new names easy to remember (since std::meow_v<T> just directly translates into std::meta::type_meow(type) for all meow), at the cost of worse names.

  3. Do something more tailored on a case-by-case basis.

We don’t think the nested namespace approach (#1) is a good idea because of the loss of ADL and the more inconvenient call syntax.

Previous revisions of this proposal used the type_ prefix (#2) uniformly. This had the downside that some type traits end up reading awkwardly (type_is_pointer as opposed to is_pointer_type) but several others do read much better (type_has_virtual_destructor as opposed to has_virtual_destructor_type). Some type traits look equally ridiculous with either a prefix or suffix (type_common_type vs common_type_type).

A more bespoke approach (#3) would be to do something based on the grammar of the existing type traits:

Note that either way, we’re also including a few common traits that aren’t defined in the same places — those are the tuple traits (tuple_size/tuple_element) and the variant traits (variant_size/variant_alternative).

Starting from R8, this paper uses option #3. That is: every type trait std::is_meow is introduced as std::meta::is_meow_type, while all other type traits std::meow are introduced as std::meta::meow.

4.5 ODR Concerns

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_1();
    else
      branch_2();
  }
};

Two translation units including cls.h can generate different definitions of Cls::odr_violator() based on whether an odd or even number of declarations have been imported from std. Branching on the members of a namespace is dangerous because namespaces may be redeclared and reopened: the set of contained declarations can differ between program points.

The creative programmer will find no difficulty coming up with other predicates which would be similarly dangerous if substituted into the same if constexpr condition: for instance, given a branch on is_complete_type(^^T), if one translation unit #includes a forward declaration of T, another #includes a complete definition of T, and they both afterwards #include "cls.h", the result will be an ODR violation.

Additional papers are already in flight proposing additional metafunctions that pose similar dangers. For instance, [P3096R2] proposes the parameters_of metafunction. This feature is important for generating language bindings (e.g., Python, JavaScript), but since parameter names can differ between declarations, it would be dangerous for a member function defined in a header file to branch on the name of a parameter.

These cases are not difficult to identify: Given an entity E and two program points P1 and P2 from which a reflection of E may be optained, it is unsafe to branch runtime code generation on any property of E (e.g., namespace members, parameter names, completeness of a class) that can be modified between P1 and P2. Worth noting as well, these sharp edges are not unique (or new) to reflection: It is already possible to build an ODR trap based on the completeness of a class using C++23.

Education and training are important to help C++ users avoid such sharp edges, but we do not find them sufficiently concerning to give pause to our enthusiasm for the features proposed by this paper.

5 Proposed Wording

[ 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. ]

5.1 Language

3 [intro.defs] Terms and definitions

Add splice-specifier to the list of template argument forms in definition 3.5.

(3.5) argument

⟨template instantiation⟩ constant-expression, type-id, or id-expression, or splice-specifier in the comma-separated list bounded by the angle brackets

5.2 [lex.phases] Phases of translation

[ Editor's note: In addition to changes necessary for this proposal, we are applying the “drive-by fix” of merging phases 7/8, in order to clarify that template instantiation is interleaved with translation. In so doing, we replace the notion of “instantiation units” with a partial ordering among all program constructs in a translation unit. ]

Modify the wording for phases 7-8 of 5.2 [lex.phases] as follows:

7-8 Whitespace characters separating tokens are no longer significant. Each preprocessing token is converted into a token (5.6 [lex.token]). The resulting tokens constitute a translation unit and are syntactically and semantically analyzed and translated.

Note 3: The process of analyzing and translating the tokens can occasionally result in one token being replaced by a sequence of other tokens ([temp.names]) — end note ]

It is implementation-defined whether the sources for module-units and header units on which the current translation unit has an interface dependency (10.1 [module.unit], 10.3 [module.import]) are required to be available.

Note 4: Source files, translation units and translated translation units need not necessarily be stored as files, nor need there be any one-to-one correspondence between these entities and any external representation. The description is conceptual only, and does not specify any particular implementation. — end note ]

Translated translation units and instantiation units are combined as follows:

Note 5: Some or all of these can be supplied from a library. — end note ]

Each translated translation unit is examined to produce a list of required instantiations.

While the tokens constituting translation units are being analyzed and translated, a collection of required instantiations is produced.

Note 5: This can include instantiations which have been explicitly requested ([temp.explicit]). — end note ]

The contexts from which instantiations may be performed are determined by their respective points of instantiation (13.8.4.1 [temp.point]).

Note 6: Other requirements in this document can further constrain the context from which an instantiation can be performed. For example, a constexpr function template specialization might have a point of instantation at the end of a translation unit, but its use in certain constant expressions could require that it be instantiated at an earlier point ([temp.inst]). — end note ]

The definitions of the required templates are located. It is implementation-defined whether the source of the translation units containing these definitions is required to be available.

Note 7: An implementation can choose to encode sufficient information into the translated translation unit so as to ensure the source is not required here. — end note ]

All required instantiations are perfomed to produce instantiation units.

Note 8: These are similar to translated translation units, but contain no references to uninstantiated templates and no template definitions. — end note ]

Each instantiation results in new program constructs. The program is ill-formed if any instantiation fails.

Note 9: Constructs that are separately subject to instantiation are specified in ([temp.spec.general]). — end note ]

Instantiation confers a partition over the program constructs, and the tokens comprising the translation unit furthermore endow the partitioned constructs with a partial order. Two constructs are semantically sequenced if the tokens by which they are described belong to the same translation unit and if either both constructs result from the same instantiation or if neither results from any instantiation. Given two semantically sequenced program constructs, whether one semantically follows the other is determined as follows:

  • (7.8-1) If both constructs are within the member-specification of a class C (11.4.1 [class.mem.general]) and one such construct (call it X) is in a complete-class context of C but the other (call it Y) is not, then X semantically follows Y.
  • (7-8.1) Otherwise, if the syntactic endpoint of the tokens that describe one such construct (call it X) occurs lexically after the syntactic endpoint of the tokens that describe the other (call it Y), then X semantically follows Y.

During the analysis and translation of tokens and instantiations, certain expressions are evaluated ([expr.const]). Diagnosable rules (4.1.1 [intro.compliance.general]) that apply to constructs that semantically follow a plainly constant-evaluated expression P are considered in a context where P has been evaluated exactly once.

Note 10: Other requirements in this document can further constrain the contexts from which expressions are evaluated. For example, a declaration that lexically follows all points from which a given instantiation can be perfomed will be considered in a context where all plainly constant-evaluated expressions resulting from that instantiation have been evaluated exactly once. — end note ]

8 All external entity references are resolved. […]

5.4 [lex.pptoken] Preprocessing tokens

Add a bullet after bullet (4.2):

  • 4 If the input stream has been parsed into preprocessing tokens up to a given character:

    • (4.1)

    • (4.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 <:.

    • (4.3) 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 [:.

      Note 1: The tokens [: and :] cannot be composed from digraphs. — end note ]

    • (4.4)

5.12 [lex.operators] Operators and punctuators

Change the grammar for operator-or-punctuator in paragraph 1 of 5.12 [lex.operators] to include the reflection operator and the splice-specifier delimiters:

  operator-or-punctuator: one of
         {        }        [        ]        (        )        [:        :]
         <:       :>       <%       %>       ;        :        ...
         ?        ::       .       .*        ->       ->*      ~
         !        +        -        *        /        %        ^        ^^       &
         |        =        +=       -=       *=       /=       %=       ^=       &=
         |=       ==       !=       <        >        <=       >=       <=>      &&
         ||       <<       >>       <<=      >>=      ++       --       ,
         and      or       xor      not      bitand   bitor    compl
         and_eq   or_eq    xor_eq   not_eq

6.1 [basic.pre] Preamble

Add type aliases and namespace aliases to the list of entities in paragraph 3. As drive-by fixes, remove “variable”, “object”, “reference”, and “template specialization”; replace “class member” with “non-static data member”, since all other cases are subsumed by existing one. Add “template parameters” and “init-captures”, which collectively subsume “packs”.

3 An entity is a value, object, reference variable, structured binding, function, enumerator, type, type alias, class non-static data member, bit-field, template, template specialization, namespace, namespace alias, template parameter, or init-capture pack.

Introduce a notion of an “underlying entity” in paragraph 5, and utilize it for the definition of a name “denoting” an entity. Type aliases are now entities, so also modify accordingly.

5 Every name is introduced by a declaration, which is a

[…]

The underlying entity of an entity is that entity unless otherwise specified. A name denotes the underlying entity of the entity declared by each declaration that introduces the name. An entity E is denoted by the name (if any) that is introduced by a declaration of E or by a typedef-name introduced by a declaration specifying E.

Note 1: Type aliases and namespace aliases are examples of entities whose underlying entities are distinct from themselves. — end note ]

Modify paragraph 6 such that denoting a variable by its name finds the variable, not the associated object.

6 A variable is introduced by the declaration of a reference other than a non-static data member or of an object. The variable’s name, if any, denotes the reference or object.

6.2 [basic.def] Declarations and definitions

Modify the third sentence of paragraph 1 to clarify that type aliases are now entities.

1 […] A declaration of an entity or typedef-name X is a redeclaration of X if another declaration of X is reachable from it (10.7 [module.reach]). […]

Since namespace aliases are now entities, but their declarations are not definitions, add namespace-alias-definition to the list of declarations in paragraph 2, just before using-declaration:

2 Each entity declared by a declaration is also defined by that declaration unless:

  • (2.1) it declares a function without specifying the function’s body (9.5 [dcl.fct.def]),

[…]

[…]

Also modify the example that follows:

Example 1: All but one of the following are definitions:

  int a;                          // defines a
  extern const int c = 1;         // defines c
  int f(int x) { return x+a; }    // defines f and defines x
  struct S { int a; int b; };     // defines S, S::a, and S::b
  struct X {                      // defines X
    int x;                        // defines non-static data member x
    static int y;                 // declares static data member y
    X() : x(0) { }                // defines a constructor of X
  };
  int X::y = 1;                   // defines X::y
  enum { up, down };              // defines up and down
  namespace N {int d; }           // defines N and N::d
- namespace N1 = N;               // defines N1
  X anX;                          // defines anX

whereas these are just declarations:

  extern int a;                   // declares a
  extern const int c;             // declares c
  int f(int);                     // declares f
  struct S;                       // declares S
  typedef int Int;                // declares Int
+ namesapce N1 = N;               // declares N1
  extern X anotherX;              // declares anotherX
  using N::d;                     // declares d
— end example ]

6.3 [basic.def.odr] One-definition rule

Add splice-expressions to the set of potential results of an expression in paragraph 3.

3 An expression or conversion is potentially evaluated unless it is an unevaluated operand ([expr.context]), a subexpression thereof, or a conversion in an initialization or conversion sequence in such a context. The set of potential results of an expression E is defined as follows:

  • (3.1) If E is an id-expression (7.5.5 [expr.prim.id]) or a splice-expression ([expr.prim.splice]), the set contains only E.
  • (3.2) […]

Note 1: This set is a (possibly-empty) set of id-expressions and splice-expressions, each of which is either E or a subexpression of E. — end note ]

Example 1: In the following example, the set of potential results of the initializer of n contains the first S::x subexpression, but not the second S::x subexpression. The set of potential results of the initializer of o contains the [:^^S::x:] subexpression.

  struct S { static const int x = 0; };
  const int &f(const int &r);
  int n = b ? (1, S::x)           // S::x is not odr-used here
            : f(S::x);            // S::x is odr-used here, so a definition is required
+ int o = [:^^S::x:];
— end example ]

Break bullet 4.1 into sub-bullets, modify it to cover splicing of functions, and replace [basic.lookup] with [over.pre] since the canonical definition of “overload set” is relocated there by this proposal:

  • (4.1) A function is named by an expression or conversion E if it is the selected member of an overload set ([basic.lookup] [over.pre], [over.match], [over.over]) in an overload resolution performed as part of forming that expression or conversion, unless it is a pure virtual function and either the expression 

    • (4.1.1) E is not an id-expression naming the function with an explicitly qualified name,
    • (4.1.2) E is not a splice-expression, or
    • (4.1.3) the expression E 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 named by an expression if that expression is either an id-expression or a splice-expression that designates that structured binding. A structured binding is odr-used if it is named by a potentially-evaluated expression.

Prepend before paragraph 15 of 6.3 [basic.def.odr]:

15pre If a class C is defined in a translation unit as a result of a call to a specialization of std::meta::define_aggregate and another translation unit contains a definition of C that is not a result of calling the same specialization with the same function arguments, the program is ill-formed; a diagnostic is required only if C is attached to a named module and a prior definition is reachable at the point where a later definition occurs.

15 For any other definable item D with definitions in multiple translation units,

  • if D is a non-inline non-templated function or variable, or
  • if the definitions in different translation units do not satisfy the following requirements,

the program is ill-formed; a diagnostic is required only if the definable item is attached to a named module and a prior definition is reachable at the point where a later definition occurs. […]

Prefer the verb “denote” in bullet 15.5 to emphasize that ODR “looks through” aliases, and clarify that objects are not entities in bullet 15.5.2.

  • (15.5) In each such definition, corresponding names, looked up according to 6.5 [basic.lookup], shall refer to denote the same entity, after overload resolution (12.2 [over.match]) and after matching of partial template specialization (13.10.4 [temp.over]), except that a name can refer to
    • (15.5.1) a non-volatile const object […], or
    • (15.5.2) a reference with internal or no linkage initialized with a constant expression such that the reference refers to the same entity object or function in all definitions of D.

Clarify in bullet 15.11 that default template-arguments in splice-specialization-specifiers also factor into ODR.

  • (15.10) In each such definition, the overloaded operators referred to, the implicit calls to conversion functions, constructors, operator new functions and operator delete functions, shall refer to the same function.
  • (15.11) In each such definition, a default argument used by an (implicit or explicit) function call or a default template argument used by an (implicit or explicit) template-id, or simple-template-id, or splice-specialization-specifier is treated as if its token sequence were present in the definition of D; that is, the default argument or default template argument is subject to the requirements described in this paragraph (recursively).

And add a bullet thereafter that factors the result of a reflect-expression into ODR.

  • (15.11+) In each such definition, corresponding reflect-expressions ([expr.reflect]) compute equivalent values (7.6.10 [expr.eq]).

6.4.1 [basic.scope.scope] General

Change bullet 4.2 to refer to the declaration of a “type alias” instead of a typedef-name.

(4.2) one declares a type (not a typedef-name type alias) and the other declares a variable, non-static data member other than an anonymous union (11.5.2 [class.union.anon]), enumerator, function, or function template, or

6.5.1 [basic.lookup.general] General

Modify paragraph 1 to cross-reference to the new definition of “overload set” given in [over.pre], rather than define it here.

1 […] If the declarations found by name lookup all denote functions or function templates, the declarations are said to form an overload set overload set (12.1 [over.pre]). Otherwise, […]

Adjust the definition in paragraph 2 of when a program point follows a declaration to account for the removal of instantiation units.

2 A program point P is said to follow any declaration with which it is semantically sequenced (5.2 [lex.phases]) in the same translation unit whose locus (6.4.2 [basic.scope.pdecl]) is before P.

Adjust paragraph 4 since type aliases are now entities.

4 In certain contexts, only certain kinds of declarations are included. After any such restriction, any declarations of classes or enumerations are discarded if any other declarations are found.

Note 4: A type (but not a typedef-name type alias or template) is therefore hidden by any other entity in its scope. — end note ]

However, if lookup is type-only, only declarations of types and templates whose specializations are types are considered; furthermore, if declarations of a typedef-name type alias and of the type to which it refers its underlying entity are found, the declaration of the typedef-name type alias is discarded instead of the type declaration.

6.5.4 [basic.lookup.argdep] Argument-dependent name lookup

Modify the first bullet of paragraph 3 of 6.5.4 [basic.lookup.argdep] as follows:

3 … Any typedef-names and using-declarations used to specify the types do not contribute to this set. The set of entities is determined in the following way:

  • (3.1) If T is std::meta::info ([meta.reflection.synop]), its associated set of entities is the singleton containing the enumeration type std::meta::operators ([meta.reflection.operators]).

    Note 1: The std::meta::info type is a type alias, so an explicit rule is needed to associate calls whose arguments are reflections with the namespace std::meta. — end note ]

  • (3.2) If T is a any other fundamental type, its associated set of entities is empty.

  • (3.3) If T is a class type …

6.5.5.1 [basic.lookup.qual.general] General

Extend paragraph 1 to cover splice-specifiers:

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, splice-scope-specifier, or computed-type-specifier is followed by a ::, it shall either be a dependent splice-scope-specifier ([temp.dep.splice]) or it shall designate a namespace, class, enumeration, or dependent type, and the :: is never interpreted as a complete nested-name-specifier.

6.5+ [basic.splice] Splice specifiers

Add a new subsection after 6.5 [basic.lookup], and renumber accordingly:

Splice specifiers [basic.splice]

splice-specifier:
  [: constant-expression :]

splice-specialization-specifier:
  splice-specifier < template-argument-listopt >

1 The constant-expression of a splice-specifier shall be a converted constant expression of type std::meta::info (7.7 [expr.const]). A splice-specifier whose converted constant-expression represents a construct X is said to designate either

Note 1: A splice-specifier is dependent if the converted constant-expression is value-dependent ([temp.dep.splice]). — end note ]

2 The splice-specifier of a non-dependent splice-specialization-specifier shall designate a template.

3 Note 2: A < following a splice-specifier is interpreted as the delimiter of a template-argument-list when the splice-specifier is preceded by typename or template, or when it appears in a type-only context (13.3 [temp.names]). — end note ]

Example 1:
constexpr int v = 1;
template <int V> struct TCls {
  static constexpr int s = V + 1;
};

using alias = [:^^TCls:]<[:^^v:]>;
  // OK, a splice-specialization-specifier with a splice-specifier
  // as a template argument

static_assert(alias::s == 2);
— end example ]

Add a bullet to paragraph 13 and handle splice-expressions in the existing bullets:

13 A declaration D names an entity E if

  • (13.1) D contains a lambda-expression whose closure type is E,
  • (13.1+) D contains a reflect-expression or a manifestly constant-evaluated expression of type std::meta::info that represents E,
  • (13.2) E is not a function or function template and D contains an id-expression, type-specifier, nested-name-specifier, template-name, or concept-name denoting E or a splice-expression designating E, or
  • (13.3) E is a function or function template and D contains an expression that names E ([basic.def.odr]) or an id-expression or splice-expression that refers to a set of overloads that contains E.

Modify paragraph 15 to make all type aliases and namespace aliases explicitly TU-local.

An entity is TU-local if it is

  • (15.1) a type, function, variable, or template that […]
  • (15.1+) a type alias or a namespace alias, TODO
  • (15.2) […]

[ 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 in p16 to include reflections:

16 A value or object is TU-local if

  • (16.0) it is of TU-local type,
  • (16.1) it is, or is a pointer to, a TU-local function or the object associated with a TU-local variable, or
  • (16.1+) it is a reflection representing either
    • (16.1+.1) a TU-local value or object, or
    • (16.1+.2) a direct base class relationship introduced by an exposure, 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.

6.8.1 [basic.types.general] General

Change the first sentence in paragraph 9 of 6.8.1 [basic.types.general] as follows:

9 Arithmetic types (6.8.2 [basic.fundamental]), enumeration types, pointer types, pointer-to-member types (6.8.4 [basic.compound]), std::meta::info, std::nullptr_t, and cv-qualified 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:

12 A type is consteval-only if it is either std::meta::info or a type compounded from a consteval-only type ([basic.compound]). Every object of consteval-only type shall be

  • (12.1) the object associated with a constexpr variable or a subobject thereof,
  • (12.2) a template parameter object (13.2 [temp.param]) or a subobject thereof, or
  • (12.3) an object whose lifetime begins and ends during the evaluation of a manifestly constant-evaluated expression.

6.8.2 [basic.fundamental] Fundamental types

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

An expression convertible to a reflection is said to represent the corresponding construct. sizeof(std::meta::info) shall be equal to sizeof(void*).

Example 1:
int arr[] = {1, 2, 3};
auto [a1, a2, a3] = arr;
void fn();
enum Enum { A };
using Alias = int;
struct B {};
struct S : B {
  int mem;
  int : 0;
};
template <auto> struct TCls {};
template <auto> void TFn();
template <auto> int TVar;
template <auto> concept Concept = requires { true; };
namespace NS {};
namespace NSAlias = NS;

constexpr auto r1 = std::meta::reflect_value(42);  // represents int value of 42

constexpr auto r2 = std::meta::reflect_object(arr[1]);  // represents int object

constexpr auto r3 = ^^arr;      // represents a variable
constexpr auto r4 = ^^a3;       // represents a structured binding
constexpr auto r5 = ^^fn;       // represents a function
constexpr auto r6 = ^^Enum::A;  // represents an enumerator
constexpr auto r7 = ^^Alias;    // represents a type alias
constexpr auto r8 = ^^S;        // represents a type
constexpr auto r9 = ^^S::mem;   // represents a class member

constexpr auto r10 = std::meta::members_of(^^S)[1];
    // represents an unnamed bit-field

constexpr auto r11 = ^^TCls;     // represents a class template
constexpr auto r12 = ^^TFn;      // represents a function template
constexpr auto r13 = ^^TVar;     // represents a variable template
constexpr auto r14 = ^^Concept;  // represents a concept
constexpr auto r15 = ^^NSAlias;  // represents a namespace alias
constexpr auto r16 = ^^NS;       // represents a namespace

constexpr auto r17 = std::meta::bases_of(^^S)[0];
    // represents a direct base class relationship

constexpr auto r18 = std::meta::data_member_spec(^^int, {.name="member"});
    // represents a data member description
— end example ]

17 - 2 Note 1: Implementations are discouraged from representing any constructs described by this document that are not explicitly enumerated in the list above (e.g., partial template specializations, attributes, placeholder types, statements). Constructs not described by this document can also be represented. — end note ]

7.2.1 [basic.lval] Value category

Apply a drive-by fix to bullet 1.1 clarifying that a glvalue can also determine the identity of a non-static data member.

  • (1.1) A glvalue is an expression whose evaluation determines the identity of an object, or function, or non-static data member.

Account for move-eligible splice-expressions in bullet 4.1 of Note 3.

  • (4.1) a move-eligible id-expression or splice-expression ([expr.prim.id.unqual]),

7.2.3 [expr.context] Context dependence

Add reflect-expressions to the list of unevaluated operands in paragraph 1.

1 In some contexts, unevaluated operands appear ([expr.prim.req], [expr.typeid], [expr.sizeof], [expr.unary.noexcept], [expr.reflect], [temp.pre], [temp.concept]). An unevaluated operand is not evaluated.

Add splice-expression to the list of expressions in paragraph 2.

2 In some contexts, an expression only appears for its side effects. Such an expression is called a discarded-value expression. The array-to-pointer and function-to-pointer standard conversions are not applied. The lvalue-to-rvalue conversion is applied if and only if the expression is a glvalue of volatile-qualified type and it is one of the following:

  • (2.1) ( expression ), where expression is one of these expressions,
  • (2.2) id-expression ([expr.prim.id]),
  • (2.2+) splice-expression ([expr.prim.splice]),
  • (2.3) […]

7.5 [expr.prim] Primary expressions

Add splice-expression to the grammar for primary-expression:

  primary-expression:
     literal
     this
     ( expression )
     id-expression
     lambda-expression
     fold-expression
     requires-expression
+    splice-expression

7.5.5.1 [expr.prim.id.general] General

Modify paragraph 2 to avoid transforming non-static members into implicit member accesses when named as operands to reflect-expressions.

2 If an id-expression E denotes a non-static non-type member of some class C at a point where the current class (7.5.3 [expr.prim.this]) is X and

  • (2.1) E is potentially evaluated or C is X or a base class of X, and
  • (2.2) E is not the id-expression of a class member access expression (7.6.1.5 [expr.ref]), and
  • (2.3) E is not the id-expression of a reflect-expression ([expr.reflect]), and
  • (2.4) if E is a qualified-id, E is not the un-parenthesized operand of the unary & operator (7.6.2.2 [expr.unary.op]),

the id-expression is transformed into a class member access expression using (*this) as the object expression.

7.5.5.2 [expr.prim.id.unqual] Unqualified names

Modify paragraph 4 to allow splice-expressions to be move-eligible:

4 An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type. In the following contexts, an a (possibly parenthesized) id-expression or splice-expression ([expr.prim.splice]) E is move-eligible:

  • (4.1) If the id-expression (possibly parenthesized) E is an operand of a return ([stmt.return]) or co_return ([stmt.return.coroutine]) statement, and names an implicitly movable entity declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or
  • (4.2) if the id-expression (possibly parenthesized) E is the operand of a throw-expression ([expr.throw]) and names an implicitly movable entity that belongs to a scope that does not contain the compound-statement of the innermost lambda-expression, try-block, or function-try-block (if any) whose compound-statement or ctor-initializer contains the throw-expression.

7.5.5.3 [expr.prim.id.qual] Qualified names

Extend the grammar for nested-name-specifier as follows:

  nested-name-specifier:
      ::
      type-name ::
      namespace-name ::
      computed-type-specifier ::
+     splice-scope-specifier ::
      nested-name-specifier identifier ::
      nested-name-specifier templateopt simple-template-id ::
+
+  splice-scope-specifier:
+     splice-specifier
+     templateopt splice-specialization-specifier

Add a paragraph after paragraph 1 specifying the rules for parsing a splice-scope-specifier, as well as an example:

1+ A splice-specifier or splice-specialization-specifier that is not followed by :: is never interpreted as part of a splice-scope-specifier. The template may only be omitted from the form templateopt splice-specialization-specifier :: when the splice-specialization-specifier is preceded by typename.

Example 1:
template <int V>
struct TCls {
  static constexpr int s = V;
  using type = int;
};

constexpr int v1 = [:^^TCls<1>:]::s;
constexpr int v2 = template [:^^TCls:]<2>::s;
  // OK, template binds to splice-scope-specifier

constexpr typename [:^^TCls:]<3>::type v3 = 3;
  // OK, typename binds to the qualified name

constexpr [:^^TCls:]<3>::type v4 = 4;
  // error: [:^^TCls:]< is parsed as a splice-expression followed
  // by a comparison operator
— end example ]

Clarify in paragraph 2 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 the id-expression of a declarator-id, or
  • a declarative nested-name-specifier.

A declarative nested-name-specifier shall not have a computed-type-specifier or a splice-scope-specifier. A declaration that uses a declarative nested-name-specifier shall be a friend declaration or inhabit a scope that contains the entity being redeclared or specialized.

Break the next paragraph into a bulleted list, extend it to also cover splices, and prefer the verb “designate” over “nominate”:

3 The entity designated by a nested-name-specifier is determined as follows:

  • (3.1) The nested-name-specifier :: nominates designates the global namespace. 
  • (3.2) A nested-name-specifier with a computed-type-specifier nominates designates the same type denoted designated by the computed-type-specifier, which shall be a class or enumeration type. 
  • (3.3) For a nested-name-specifier of the form splice-specifier ::, the splice-specifier shall designate a class or enumeration type or a namespace. The nested-name-specifier designates the same entity as the splice-specifier.
  • (3.4) For a nested-name-specifier of the form templateopt splice-specialization-specifier ::, the splice-specifier of the splice-specialization-specifier shall designate a primary class template or an alias template T. Letting S be the specialization of T corresponding to the template-argument-list (if any) of the splice-specialization-specifier, S shall either be a class template specialization or an alias template specialization that denotes a class or enumeration type. The nested-name-specifier designates S if T is a class template or the type denoted by S if T is an alias template.
  • (3.5) If a nested-name-specifier N is declarative and has a simple-template-id with a template argument list A that involves a template parameter, let T be the template nominated designated by N without A. T shall be a class template.
    • (3.5.1) If A is the template argument list (13.4 [temp.arg]) of the corresponding template-head H (13.7.3 [temp.mem]), N nominates designates the primary template of T; H shall be equivalent to the template-head of T (13.7.7.2 [temp.over.link]).
    • (3.5.2) Otherwise, N nominates designates the partial specialization (13.7.6 [temp.spec.partial]) of T whose template argument list is equivalent to A (13.7.7.2 [temp.over.link]); the program is ill-formed if no such partial specialization exists.
  • (3.6) Any other nested-name-specifier nominates designates the entity denoted designated by its type-name, namespace-name, identifier, or simple-template-id. If the nested-name-specifier is not declarative, the entity shall not be a template.

7.5.6.3 [expr.prim.lambda.capture] Captures

Modify bullet 7.1 as follows:

  • (7.1) An id-expression or splice-expression that names a local entity potentially references that entity; an id-expression that names one or more non-static class members and does not form a pointer to member ([expr.unary.op]) potentially references *this.

And extend the example following paragraph 7 with uses of expression splices:

Example 4:
void f(int, const int (&)[2] = {});       // #1
void f(const int &, const int (&)[1]);    // #2

void test() {
  const int x = 17;
  auto g = [](auto a) {
    f(x);                    // OK, calls #1, does not capture x
 
    constexpr auto r = ^^x;  // OK, unevaluated operand does not capture x
    f([:r:]);                // OK, calls #1, also does not capture x
  }

  auto g1 = [=](auto a) {
    f(x);                   // OK, calls #1, captures x
    f([:^^x:]);             // OK, calls #1, also captures x
  }
}
— end example ]

Modify paragraph 11 (and note 7 which follows):

  • 11 An id-expression or splice-expression within the compound-statement of a lambda-expression that is an odr-use of an entity captured by copy is transformed into an access to the corresponding unnamed data member of the closure type.

    Note 7: An id-expression or splice-expression that is not an odr-use refers to the original entity, never to a member of the closure type. However, such an id-expression can still cause the implicit capture of the entity. — end note ]

And extend the example following paragraph 11 with uses of expression splices:

Example 8:
void f(const int *);
void g() {
  const int N = 10;
  [=] {
    int arr[N];      // OK, not an odr-use, refers to variable with automatic storage duration
    f(&N);           // OK, causes N to be captured; &N points to
                     // the corresponding member of the closure type
 
    f(&[:^^N:])      // OK, also causes N to be captured
  }
}
— end example ]

7.5.8* [expr.prim.splice] Expression splicing

Add a new subsection of 7.5 [expr.prim] following 7.5.8 [expr.prim.req]

Expression Splicing [expr.prim.splice]

splice-expression:
   splice-specifier
   template splice-specifier
   template splice-specialization-specifier

1 A splice-specifier or splice-specialization-specifier immediately followed by :: or preceded by typename is never interpreted as part of a splice-expression.

Example 1:
struct S { static constexpr int a = 1; };
template <typename> struct TCls { static constexpr int b = 2; };

constexpr int c = [:^^S:]::a;                   // [:^^S:] is not an expression

constexpr int d = template [:^^TCls:]<int>::b;  // template [:^^TCls:]<int> is not
                                                // an expression

template <auto V> constexpr int e = [:V:];   // splice-expression
constexpr int f = template [:^^e:]<^^S::a>;  // splice-expression

auto g = typename [:^^int:](42);
  // [:^^int:] forms part of a type, not a splice-expression
— end example ]

2 For a splice-expression of the form splice-specifier, let S be the construct designated by splice-specifier.

  • 3 If S is a function, overload resolution ([over.match], [temp.over]) is performed from an initial set of candidate functions containing only that function. The expression is an lvalue referring to the selected function and has the same type as that function.

  • (3.1) Otherwise, if S is an object or a non-static data member, the expression is an lvalue designating S. The expression has the same type as S, and is a bit-field if and only if S is a bit-field.

  • (3.2) Otherwise, if S is a variable or a structured binding, S shall either have static or thread storage duration or shall inhabit a scope enclosing the expression. The expression is an lvalue referring to the object or function X associated with or referenced by S, has the same type as S, and is a bit-field if and only if X is a bit-field.

    Note 1: The type of a splice-expression designating a variable or structured binding of reference type will be adjusted to a non-reference type (7.2.2 [expr.type]). — end note ]

  • (3.3) Otherwise, if S is a value or an enumerator, the expression is a prvalue that computes S and whose type is the same as S.

  • (3.4) Otherwise, the expression is ill-formed.

4 For a splice-expression of the form template splice-specifier, the splice-specifier shall designate a function template. Overload resolution is performed from an initial set of candidate functions containing only that function template. The expression is an lvalue referring to the selected function and has the same type as that function.

5 For a splice-expression of the form template splice-specialization-specifier, the splice-specifier of the splice-specialization-specifier shall designate a template. Let T be that template and let S be the specialization of T corresponding to the template-argument-list (if any) of the splice-specialization-specifier.

  • (5.1) If T is a function template, overload resolution is performed from an initial set of candidate functions containing only the function associated with S. The expression is an lvalue referring to the selected function and has the same type as that function.

  • (5.2) Otherwise, if T is a primary variable template, the expression is an lvalue referring to the same object associated with S and has the same type as S.

  • (5.3) Otherwise, if T is a concept, the expression is a prvalue that computes the same boolean value as the concept-id formed by S.

  • (5.4) Otherwise, the expression is ill-formed.

Note 2: Access checking of class members occurs during lookup, and therefore does not pertain to splicing. — end note ]

6 A splice-expression that designates a non-static data member or implicit object member function of a class can only be used:

  • (6.1) as part of a class member access in which the object expression refers to the member’s class or a class derived from that class,
  • (6.2) to form a pointer to member (7.6.2.2 [expr.unary.op]), or
  • (6.3) if that splice-expression designates a non-static data member and it appears in an unevaluated operand.

Note 3: The implicit transformation (7.5.5 [expr.prim.id]) whereby an id-expression denoting a non-static member becomes a class member access does not apply to a splice-expression. — end note ]

7 While performing overload resolution to determine the entity referred to by a splice-expression, the best viable function is designated in a manner exempt from access rules.

7.6.1.1 [expr.post.general] General

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 . splice-expression
    postfix-expression -> templateopt id-expression
+   postfix-expression -> splice-expression

7.6.1.5 [expr.ref] Class member access

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, is a postfix expression.

Note 1: If the keyword template is used, the following unqualified name is considered to refer to a template ([temp.names]). If a simple-template-id results and is followed by a ::, the id-expression is a qualified-id. — end note ]

Modify paragraph 2 to account for splices in member access expressions:

2 For the first option, if the a dot that is followed by an id-expression names or splice-expression that designates a static member or an enumerator, the first expression is a discarded-value expression (7.2.3 [expr.context]); if the id-expression or splice-expression designates names a non-static data member, the first expression shall be a glvalue. For the second option (arrow), the first expression A postfix expression that is followed by an arrow shall be a prvalue having pointer type. The expression E1->E2 is converted to the equivalent form (*(E1)).E2; the remainder of [expr.ref] will address only the first option (dot) the form using a 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 or splice-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 or postfix-expression.splice-expression as E1.E2, E1 is called the object expression. […]

Adjust the language in paragraphs 6-9 to account for splice-expressions. Explicitly add a fallback to paragraph 7 that makes other cases ill-formed.

6 If E2 is designates a bit-field, E1.E2 is a bit-field. […]

7 If E2 designates an entity that is declared to have type “reference to T”, then E1.E2 is an lvalue of type T. If In that case, if E2 is designates a static data member, E1.E2 designates the object or function to which the reference is bound, otherwise E1.E2 designates the object or function to which the corresponding reference member of E1 is bound. Otherwise, one of the following rules applies.

  • (7.1) If E2 is designates a static data member and the type of E2 is T, then E1.E2 is an lvalue; […]

  • (7.2) Otherwise, if If E2 is designates a non-static data member and the type of E1 is “cq1 vq1 X”, and the type of E2 is “cq2 vq2 T”, […]. If the entity designated by E2 is declared to be a mutable member, then the type of E1.E2 is “vq12 T”. If the entity designated by E2 is not declared to be a mutable member, then the type of E1.E2 is “cq12 vq12 T”.

  • (7.3) Otherwise, if If E2 is an overload set, […]

  • (7.4) Otherwise, if If E2 is designates a nested type, the expression E1.E2 is ill-formed.

  • (7.5) Otherwise, if If E2 is designates a member enumerator and the type of E2 is T, the expression E1.E2 is a prvalue of type T whose value is the value of the enumerator.

  • (7.6) Otherwise, the program is ill-formed.

8 If E2 is an id-expression denoting a non-static member, the program is ill-formed if the class of which E2 is directly denotes a member is an ambiguous base (6.5.2 [class.member.lookup]) of the naming class (11.8.3 [class.access.base]) of E2.

9 If the entity designated by E2 is a non-static member and the result of E1 is an object whose type is not similar ([conv.qual]) to the type of E1, the behavior is undefined.

7.6.2.1 [expr.unary.general] General

Add reflect-expression to the grammar for unary-expression in paragraph 1:

1 Expressions with unary operators group right-to-left.

  unary-expression:
     ...
     delete-expression
+    reflect-expression

7.6.2.2 [expr.unary.op] Unary operators

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 type T.

  • (3.1) If the operand is a qualified-id or splice-expression naming designating a non-static or variant member of some class C, other than an explicit object member function, the result has type “pointer to member of class C of type T” and designates C::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 a qualified-id or a splice-expression.

4 A pointer to member is only formed when an explicit & is used and its operand is a qualified-id or splice-expression not enclosed in parentheses.

7.6.2.10* [expr.reflect] The reflection operator

Add a new subsection of 7.6.2 [expr.unary] following 7.6.2.9 [expr.delete]

The reflection operator [expr.reflect]

reflect-expression:
   ^^ ::
   ^^ qualified-namespace-specifier
   ^^ 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 type std::meta::info (6.8.2 [basic.fundamental]).

Note 1: Constructs not described by this document can also be represented by reflections, and can appear as operands of reflect-expressions. — end note ]

2 A reflect-expression is parsed as the longest possible sequence of tokens that could syntactically form a reflect-expression.

Example 1:
static_assert(std::meta::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;  // error: 'int &&&&' is not a valid type
  ^^X < xv;              // OK
  (^^X) < xv;            // OK
}
— end example ]

3 A reflect-expression that could be validly interpreted as ^^ template-name is never interpreted as ^^ id-expression, and is interpreted as ^^ type-id if and only if the operand refers to the current instantiation ([temp.dep.type]).

4 A reflect-expression of the form ^^ :: represents the global namespace. A reflect-expression of the form ^^ qualified-namespace-specifier is interpreted as follows:

  • 5 If lookup of the qualified-namespace-specifier unambiguously finds a namespace-alias-definition, the reflect-expression represents the namespace alias declared by that namespace-alias-definition.
  • 6 Otherwise, the reflect-expression represents the namespace denoted by the qualified-namespace-specifier.

7 A reflect-expression of the form ^^ nested-name-specifieropt template-name represents the primary class template, function template, primary variable template, or alias template denoted by the template-name.

8 A reflect-expression of the form ^^ nested-name-specifieropt concept-name represents the concept denoted by the concept-name.

9 A reflect-expression of the form ^^ type-id is interpreted as follows:

  • (9.1) If the type-id is a typedef-name that was introduced by the declaration of a template parameter, the reflect-expression represents the type denoted by that typedef-name.
  • (9.2) Otherwise, if the type-id is any other typedef-name, the reflect-expression represents the type alias associated with that typedef-name.
  • (9.3) Otherwise, the reflect-expression represents the type denoted by the type-id.

10 A reflect-expression of the form ^^ id-expression is interpreted as follows:

  • (10.1) If the id-expression denotes an overload set S and overload resolution for the expression &S determines a unique function F (12.3 [over.over]), the reflect-expression represents F.

  • (10.2) Otherwise, if the id-expression denotes a local entity captured by an enclosing lambda-expression, the reflect-expression is ill-formed.

  • (10.3) Otherwise, if the id-expression denotes a variable, structured binding, enumerator, or non-static data member or member function, the reflect-expression represents that entity.

  • (10.4) Otherwise, the reflect-expression is ill-formed. Note 2: This includes pack-index-expressions and non-type template parameters. — end note ]

The id-expression of a reflect-expression is an unevaluated operand (7.2.3 [expr.context]).

Example 2:
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 a = ^^fn<char>;     // OK
constexpr auto b = ^^fn<int>;      // error: ambiguous

constexpr auto c = ^^std::vector;  // OK

template <typename T>
struct S {
  static constexpr auto r = ^^T;
  using type = T;
}
static_assert(S<int>::r == ^^int);
static_assert(^^S<int>::type != ^^int);

typedef struct X {} Y;
typedef struct Z {} Z;
constexpr auto e = ^^Y;  // OK, represents the type alias Y
constexpr auto f = ^^Z;  // OK, represents the type Z (not the type alias)
— end example ]

7.6.10 [expr.eq] Equality Operators

Extend paragraph 2 to also handle std::meta::info:

2 The converted operands shall have arithmetic, enumeration, pointer, or pointer-to-member type, type std::meta::info, or type std::nullptr_t. The operators == and != both yield true or false, i.e., a result of type bool. In each case below, the operands shall have the same type after the specified conversions have been applied.

Add a new paragraph between paragraphs 5 and 6:

5 Two operands of type std::nullptr_t or one operand of type std::nullptr_t and the other a null pointer constant compare equal.

5+ If both operands are of type std::meta::info, comparison is defined as follows:

  • (5+.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.
  • (5+.2) Otherwise, if one operand represents a value, then they compare equal if and only if the other operand represents a value that is template-argument-equivalent (13.6 [temp.type]).
  • (5+.3) Otherwise, if one operand represents an object, then they compare equal if and only if the other operand represents the same object.
  • (5+.4) Otherwise, if one operand represents an entity, then they compare equal if and only if the other operand represents the same entity.
  • (5+.5) Otherwise, if one operand represents a direct base class relationship, then they compare equal if and only if the other operand represents the same direct base class relationship.
  • (5+.6) Otherwise, both operands O1 and O2 represent data member descriptions. The operands compare equal if and only if the data member descriptions represented by O1 and O2 compare equal (11.4.1 [class.mem.general]).

6 If two operands compare equal, the result is true for the == operator and false for the != operator. If two operands compare unequal, the result is false for the == operator and true for the != operator. Otherwise, the result of each of the operators is unspecified.

7.7 [expr.const] Constant Expressions

Modify paragraph 10 to mention splice-expressions:

10 During the evaluation of an expression E as a core constant expression, all id-expressions, splice-expressions, and uses of *this that refer to an object or reference whose lifetime did not begin with the evaluation of E are treated as referring to a specific instance of taht object or reference whose lifetime and that of all subobjects (including all union members) includes the entire constant evaluation. […]

Modify paragraph 15 to disallow returning non-consteval-only pointers and references to consteval-only objects from constant expressions.

15 A constant expression is either a glvalue core constant expression E that  

  • (15.1) refers to an entity object or function that is a permitted result of a constant expression, and

  • (15.2) if E designates a function of consteval-only type (6.8.1 [basic.types.general]) or an object whose complete object is of consteval-only type, then E is also of consteval-only type,

    Example 1:
    struct Base { };
    struct Derived : Base { std::meta::info r; };
    
    consteval const Base& fn(const Derived& derived) { return derived; }
    
    constexpr auto obj = Derived{^^::}; // OK
    constexpr auto const& d = obj; // OK
    constexpr auto const& b = fn(obj); // error: not a constant expression
      // because Derived is a consteval-only type but Base is not.
    — end example ]

or a prvalue core constant expression whose value satisfies the following constraints:

  • (15.3) if the value is an object of class type, each non-static data member of reference type refers to an entity object or function that is a permitted result of a constant expression, and if the object or function is of consteval-only type, or is the subobject of such an object, then so is the value,
  • (15.4) if the value is an object of scalar type, it does not have an indeterminate or erroneous value (6.7.4 [basic.indet]),
  • (15.5) if the value is of pointer type, then: 
    • (15.5.1) it is a pointer to an object of static storage duration, or a pointer past the end of such an object (7.6.6 [expr.add]), and if the complete object of that object is of consteval-only type then so is that pointer,
    • (15.5.2) it is a pointer to a non-immediate function, and if that function is of consteval-only type then so is that pointer, or
    • (15.5.3) it is a null pointer value,
  • (15.6) if the value is of pointer-to-member-function type, it does not designate an immediate function, and if that function is of consteval-only type then so is that value, and
  • (15.7) if the value is an object of class or array type, each subobject satisfies these constraints for the value.

Modify (and clean up) the definition of immediate-escalating in paragraph 18 to also apply to expressions of consteval-only type.

18 An potentially-evaluated expression or conversion is immediate-escalating if it is not neither initially in an immediate function context nor a subexpression of an immediate invocation, and it is either

  • (18.1) a potentially-evaluated an id-expression or splice-expression that denotes designates an immediate function, that is not a subexpression of an immediate invocation, or
  • (18.2) an immediate invocation that is not a constant expression, or and is not a subexpression of an immediate invocation.
  • (18.3) of consteval-only type (6.8.1 [basic.types.general]).

Add a new paragraphs prior to the definition of manifestly constant-evaluated (7.7 [expr.const]/21), and renumber accordingly:

21pre A non-dependent expression or conversion is plainly constant-evaluated if it is not in a complete-class context (11.4.1 [class.mem.general]) and is either

Note 1: The evaluation of a plainly constant-evaluated expression E can produce injected declarations (see below) and happens exactly once (5.2 [lex.phases]). Any such declarations are reachable from a point that follows immediately after E. — end note ]

Add a note following the definition of manifestly constant-evaluated to clarify the relationship with plainly constant-evaluated expressions:

21 An expression or conversion is manifestly constant-evaluated if it is:

  • (21.1) a constant-expression, or
  • (21.2) the condition of a constexpr if statement (8.5.2 [stmt.if]), or
  • (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., template arguments) are not plainly constant-evaluated. Such expressions are still evaluated during translation, but (unlike plainly constant-evaluated expressions) can be evaluated multiple times, and there are no constraints on the relative order of their evaluation. — end note ]

After the example following the definition of manifestly constant-evaluated, introduce new terminology and rules for injecting declarations and renumber accordingly:

22 The evaluation of an expression can introduce one or more injected declarations. Each such declaration has an associated synthesized point which follows the last non-synthesized program point in the translation unit containing that declaration. The evaluation is said to produce the declaration.

Note 13: Special rules concerning reachability apply to synthesized points (10.7 [module.reach]). — end note ]

23 The program is ill-formed if the evaluation of a manifestly constant-evaluated expression M produces an injected declaration D and either

  • (23.1) M is not a plainly constant-evaluated expression, or
  • (23.2) the target scope of D is not semantically sequenced with M (5.2 [lex.phases]).

Furthermore, the program is ill-formed, no diagnostic required, if

  • (23.3) an evaluation E1 produces an injected declaration D during the evaluation of a manifestly constant-evaluated expression M,
  • (23.4) a second evaluation E2 computes a reflection of D during the same evaluation of M, and
  • (23.5) E1 and E2 are either unsequenced or indeterminately sequenced ([intro.execution]).
Example 2:
consteval bool complete_type(std::meta::info r) {
  std::meta::define_aggregate(r, {});
  return true;
}

struct S1;
constexpr bool b1 = !complete_type(^^S1);
  // OK, constexpr variable so this is plainly constant-evaluated

struct S2;
bool b2 = !complete_type(^^S2);
  // error: initializer !complete_type(^^S2) produced an injected declaration but is
  // not plainly constant-evaluated

template <typename>
    requires ([] {
      struct S3;
      return complete_type(^^S3);
    }())
bool tfn1();

constexpr bool b3 = tfn1<void>();
  // error: the requires-clause produces an injected declaration but is not
  // plainly constant-evaluated

template <std::meta::info R> consteval bool tfn2() {
  return complete_type(R);
}

struct S4;
constexpr bool b4 = tfn2<^^S4>();
  // OK, manifestly constant-evaluated expression tfn2<^^S4>() is semantically sequenced
  // with S4

template <std::meta::info R> struct T1 {
  static constexpr bool b5 = complete_type(R);
}

struct S5;
T1<^^S5> t1;  // error: target scope of the injected declaration is not semantically
  // sequenced with the manifestly constant-evaluated expression that produced it

template <typename> struct T2 {
  struct S6;
  static void sfn() requires ([]{
    constexpr bool b = complete_type(^^S6);
    return b;
  }) { }
};

constexpr bool b6 = T2<void>::sfn();
  // error: target scope of the injected declaration (T2<void>) is not semantically
  // sequenced with the manifestly constant-evaluated expression that produces it
  // (requires-clause is separately instantiated from the enclosing specialization)

struct S7;
constexpr unsigned v = complete_decl() + std::meta::size_of(^^S7);
  // ill-formed, no diagnostic required: operands of + are indeterminately sequenced
— end example ]

24 The evaluation context is a set of points within the program that determines the behavior of certain functions used for reflection ([meta.reflection]). During the evaluation of a manifestly constant-evaluated expression M, the evaluation context of an evaluation E comprises the union of

9.1 [dcl.pre] Preamble

Strike the assertion that a typedef-name is synonymous with its associated type from paragraph 8 (type aliases are entities now).

8 If the decl-specifier-seq contains the typedef specifier, the declaration is a typedef declaration and each declarator-id is declared to be a typedef-name, synonymous with its associated type (9.2.4 [dcl.typedef]).

9.2.4 [dcl.typedef] The typedef specifier

Modify paragraphs 1-2 to clarify that the typedef specifier now introduces an entity.

1 Declarations containing the decl-specifier typedef declare identifiers that can be used later for naming type aliases whose underlying entities are fundamental (6.8.2 [basic.fundamental]) or compound (6.8.4 [basic.compound]) types. The typedef specifier shall not be combined in a decl-specifier-seq with any other kind of specifier except a defining-type-specifier, and it shall not be used in the decl-specifier-seq of a parameter-declaration (9.3.4.6 [dcl.fct]) nor in the decl-specifier-seq of a function-definition (9.5 [dcl.fct.def]). If a typedef-specifier appears in a declaration without a declarator, the program is ill-formed.

  typedef-name:
      identifier
      simple-template-id

A name declared with the typedef specifier becomes a typedef-name. A typedef-name names The underlying entity of the type alias is the type associated with the identifier (9.3 [dcl.decl]) or simple-template-id (13.1 [temp.pre]); a typedef-name is thus a synonym for denotes another type. A typedef-name does not introduce a new type the way a class declaration (11.3 [class.name]) or enum declaration (9.7.1 [dcl.enum]) does.

2 A typedef-name type alias can also be introduced declared by an alias-declaration. The identifier following the using keyword is not looked up; it becomes a the typedef-name of a type alias and the optional attribute-specifier-seq following the identifier appertains to that typedef-name type alias. Such a typedef-name type alias has the same semantics as if it were introduced by the typedef specifier. In particular, it does not define a new type.

9.2.9.3 [dcl.type.simple] Simple type specifiers

Extend the grammar for computed-type-specifier as follows:

  computed-type-specifier:
      decltype-specifier
      pack-index-specifier
+     splice-type-specifier

Extend the definition of “placeholder for a deduced class type” in p3 to accommodate splice-type-specifiers.

3 A placeholder-type-specifier is a placeholder for a type to be deduced ([dcl.spec.auto]). A type-specifier of the form typenameopt nested-name-specifieropt template-name is a placeholder for a deduced class type ([dcl.type.class.deduct]) if it either 

  • (3.1) is of the form typenameopt nested-name-specifieropt template-name, or
  • (3.2) is of the form typenameopt splice-specifier and the splice-specifier designates a class template or alias template.

The nested-name-specifier, if any, shall be non-dependent and the template-name or splice-specifier shall name designate a deducible template. A deducible template is either a class template or is an alias template whose defining-type-id is of the form

typenameopt nested-name-specifieropt templateopt simple-template-id

where the nested-name-specifier (if any) is non-dependent and the template-name of the simple-template-id names a deducible template.

Add a row to [tab:dcl.type.simple] to cover the splice-type-specifier production.

Table 17: simple-type-specifiers and the types they specify [tab:dcl.type.simple]
Specifier(s)
Type
type-name the type named
simple-template-id the type as defined in [temp.names]
decltype-specifier the type as defined in [dcl.type.decltype]
pack-index-specifier the type as defined in [dcl.type.pack.index]
placeholder-type-specifier the type as defined in [dcl.spec.auto]
template-name the type as defined in [dcl.type.class.deduct]
splice-type-specifier the type as defined in [dcl.type.splice]
...

9.2.9.6 [dcl.type.decltype] Decltype specifiers

Add a bullet after bullet 1.3 to apply to splice-expressions, and extend the example that follows the paragraph:

1 For an expression E, the type denoted by decltype(E) is defined as follows:

[…]

  • (1.3) otherwise, if E is an unparenthesized id-expression or an unparenthesized class member access ([expr.ref]), decltype(E) is the type of the entity named by E. If there is no such entity, the program is ill-formed;
  • (1.3+) otherwise, if E is an unparenthesized splice-expression, decltype(E) is the type of the entity, object, or value designated by the splice-specifier of E;

[…]

The operand of the decltype specifier is an unevaluated operand.

Example 1:
const int && foo();
int i;
struct A {double x; };
const A* a = new A();
decltype(foo()) x1 = 17;       // type is const int&&
decltype(i) x2;                // type is int
decltype(a->x) x3;             // type is double
decltype((a->x)) x4 = x3;      // type is const double&
decltype([:^^x1:]) x5 = 18;    // type is const int&&

void f() {
  [](auto ...pack) {
    decltype(pack...[0]) x5 x6;    // type is int
    decltype((pack...[0])) x6 x7;  // type is int&
  }
}
— end example ]

[dcl.type.splice] Type splicing

Add a new subsection of (9.2.9 [dcl.type]) following (9.2.9.8 [dcl.type.class.deduct]).

Type Splicing [dcl.type.splice]

splice-type-specifier:
   typenameopt splice-specifier
   typenameopt splice-specialization-specifier

1 A splice-specifier or splice-specialization-specifier immediately followed by :: is never interpreted as part of a splice-type-specifier. A splice-specifier or splice-specialization-specifier not preceded by typename is only interpreted as a splice-type-specifier within a type-only context (13.8.1 [temp.res.general]).

Example 1:
struct S { using type = int; };
template <auto R> struct TCls {
  typename [:R:]::type member;  // typename applies to the qualified name
};

int fn() {
  [:^^S::type:] *var;           // error: [:^^S::type:] is an expression
  typename [:^^S::type:] *var;  // OK, declares variable with type int*
}

using alias = [:^^S::type:];    // OK, type-only context
— end example ]

2 For a splice-type-specifier of the form typenameopt splice-specifier, the splice-specifier shall designate a type, a primary class template, an alias template, or a concept. The splice-type-specifier designates the same entity as the splice-specifier.

3 For a splice-type-specifier of the form typenameopt splice-specialization-specifier, the splice-specifier of the splice-specialization-specifier shall designate a primary class template or an alias template. Let T be that template and let S be the specialization of T corresponding to the template-argument-list (if any) of the splice-specialization-specifier.

  • (3.1) If T is a primary class template, the splice-type-specifier designates S.
  • (3.2) Otherwise if T is an alias template, the splice-type-specifier designates the type denoted by S.

9.3.4.6 [dcl.fct] Functions

Use “denoted by” instead of “named by” in paragraph 9 to be more clear about the entity being referred to, and add a bullet 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 denoted by typedef-name ([dcl.typedef], [temp.param])) shall appear only as:

  • (9.1) the function type for a non-static member function,
  • (9.2) the function type to which a pointer to member refers,
  • (9.3) the top-level function type of a function typedef declaration or alias-declaration,
  • (9.4) the type-id in the default argument of a type-parameter ([temp.param]),
  • (9.5) the type-id of a template-argument for a type-parameter ([temp.arg.type])., or
  • (9.6) the operand of a reflect-expression ([expr.reflect]).

Extend the example that follows to demonstrate taking the reflection of an abominable function type:

Example 4:
typedef int FIC(int) const;
FIC f;                                          // error: does not declare a member function
struct S {
  FIC f;                                        // OK
};
FIC S::*pm = &S::f;                             // OK
constexpr std::meta::info r = ^^void(int) &;    // OK
— end example ]

9.3.4.7 [dcl.fct.default] Default arguments

Modify paragraph 9 to allow reflections of non-static data members to appear in default function arguments, and extend example 8 which follows.

9 A default argument is evaluated each time the function is called with no argument for the corresponding parameter.

[…]

A non-static member shall not appear in a default argument unless it appears as the id-expression of a class member access expression ([expr.ref]) or reflect-expression ([expr.reflect]) or unless it is used to form a pointer to member ([expr.unary.op]).

Example 8:
int b;
class X {
  int a;
  int mem1(int i = a);    // error: non-static member `a` used as default argument
  int mem2(int i = b);    // OK; use `X::b`
  consteval void mem3(std::meta::info r = ^^a) {};    // OK
  static int b;
}
— end example ]

9.4.1 [dcl.init.general] Initializers (General)

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 is std::meta::info, the object is initialized to a null reflection value;
  • (6.1) if T is a any other scalar type (6.8.1 [basic.types.general]), the object is initialized to the value obtained by converting the integer literal 0 (zero) to T;
  • (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 is std::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 of T would invoke a user-provided constructor of T (not inherited from a base class) or if

If a program calls for the default-initialization of an object of a const-qualified type T, T shall be std::meta::info or a const-default-constructible class type, or array thereof.

9 To value-initialize an object of type T means: […]

9.5.3 [dcl.fct.def.delete] Deleted definitions

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 a reflect-expression ([expr.reflect]), is ill-formed.

9.7.2 [enum.udecl] The using enum declaration

Extend 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-type-specifier

Modify paragraph 1 to handle splice-type-specifiers:

1 A using-enum-declarator of the form splice-type-specifier designates the same construct designated by the splice-type-specifier. Any other A using-enum-declarator names the set of declarations found by type-only lookup ([basic.lookup.general]) for the using-enum-declarator (6.5.3 [basic.lookup.unqual], 6.5.5 [basic.lookup.qual]). The using-enum-declarator shall designate a non-dependent type with a reachable enum-specifier.

9.8.3 [namespace.alias] Namespace alias

Modify the grammar for namespace-alias-definition in paragraph 1, and clarify that such declarations declare a “namespace alias” (which is now an entity as per [basic.pre]).

1 A namespace-alias-definition declares an alternative name for a namespace a namespace alias according to the following grammar:

  namespace-alias:
      identifier

  namespace-alias-definition:
      namespace identifier = qualified-namespace-specifier
+     namespace identifier = splice-specifier

  qualified-namespace-specifier:
      nested-name-specifieropt namespace-name

The splice-specifier (if any) shall designate a namespace.

Remove the details about what the namespace-alias denotes; this will fall out from the “underlying entity” of the namespace alias defined below:

2 The identifier in a namespace-alias-definition becomes a namespace-alias and denotes the namespace denoted by the qualified-namespace-specifier.

Add the following paragraph after paragraph 2 and before the note:

2+ The underlying entity (6.1 [basic.pre]) of the namespace alias is the namespace either denoted by the qualified-namespace-specifier or designated by the splice-specifier.

9.8.4 [namespace.udir] Using namespace directive

Add splice-specifier to the grammar for using-directive:

  using-directive:
      attribute-specifier-seqopt using namespace nested-name-specifieropt namespace-name
+     attribute-specifier-seqopt using namespace splice-specifier

Add the following prior to the first paragraph of 9.8.4 [namespace.udir], and renumber accordingly:

0 The splice-specifier, if any, designates a namespace. The nested-name-specifier and the splice-specifier shall not be dependent.

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 the nominated designated 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 the using-directive and the nomindated designated namespace. — end note ]

[…]

Note 4: A using-directive is transitive: if a scope contains a using-directive that nominates designates a namespace that itself contains using-directives, the namespaces nominated designated by those using-directives are also eligible to be considered. — end note ]

9.12.1 [dcl.attr.grammar] Attribute syntax and semantics

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 no attributes and no alignment-specifier has no effect. Note 1: That includes an attribute-specifier of the form [ [ using attribute-namespace :] ] which is thus equivalent to replacing the :] token by the two-token sequence : ]. — end note ]

9.12.5 [dcl.attr.deprecated] Deprecated attribute

Prefer “type alias” to “typedef-name” in paragraph 2.

2 The attribute may be applied to the declaration of a class, a typedef-name type alias, a variable, a non-static data member, a function, a namespace, an enumeration, an enumerator, a concept, or a template specialization.

9.12.9 [dcl.attr.unused] Maybe unused attribute

Prefer “type alias” to “typedef-name” in paragraph 2.

2 The attribute may be applied to the declaration of a class, typedef-name type alias, variable (including a structured binding declaration), structured binding, non-static data member, function, enumeration, or enumerator, or to an identifier label (8.2 [stmt.label]).

10.4 [module.global.frag] Global module fragment

Extend bullet 3.1 to include splice-specifiers that designate D. Separately account for splice-specifiers that might have a splice-specialization-specifier designating a specialization.

3 A declaration D is decl-reachable from a declaration S in the same translation unit if

  • (3.1) D does not declare a function or function template and S contains an id-expression, namespace-name, type-name, template-name, or concept-name naming D or a splice-specifier or splice-expression designating D, or
  • (3.2) […]

Specify in paragraph 3 that it is unspecified whether spliced types are replaced by their designated types, and renumber accordingly. Add an additional bullet further clarifying that it is unspecified whether any splice specifier is replaced.

In this determination, it is unspecified

  • (3.6) whether a reference to an alias-declaration, typedef declaration, using-declaration, or namespace-alias-definition is replaced by the declarations they name prior to this determination,
  • (3.7) whether a simple-template-id that does not denote a dependent type and whose template-name names an alias template is replaced by its denoted type prior to this determination,
  • (3.8) whether a decltype-specifier or splice-type-specifier that does not denote designate a dependent type is replaced by its denoted designated type prior to this determination, and
  • (3.9) whether a non-value-dependent constant expression is replaced by the result of constant evaluation prior to this determination., and
  • (3.10) whether a splice-specifier that is not dependent is replaced by the construct that it designates prior to this determination.

10.7 [module.reach] Reachability

Modify the definition of reachability to account for injected declarations:

3 A declaration D is reachable from a point P if

  • (3.1) P is not a synthesized point and D appears prior to P in the same translation unit, or
  • (3.2) D is an injected declaration for which P is the corresponding synthesized point, or
  • (3.3) D is not discarded (10.4 [module.global.frag]), appears in a translation unit that is reachable from P, and does not appear within a private-module-fragment.

11.4.1 [class.mem.general] General

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 11.4.9 [class.static]) (a static data member (11.4.9.3 [class.static.data]) or static member function (11.4.9.2 [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 (11.4.3 [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 a new paragraph to the end of the section defining data member description:

29+ A data member description is a quintuple (T, N, A, W, NUA) describing the potential declaration of a nonstatic data member where

  • (29+.1) T is a type or type alias,
  • (29+.2) N is an identifier or -1,
  • (29+.3) A is an alignment or -1,
  • (29+.4) W is a bit-field width or -1, and
  • (29+.5) NUA is a boolean value.

Two data member descriptions are equal if each of their respective components are same types, same identifiers, and equal values.

Note 4: The components of a data member description describe a data member such that

  • (29+.6) its type is specified using the type or type alias given by T,
  • (29+.7) it is declared with the name given by N if N != -1 and is otherwise unnamed,
  • (29+.8) it is declared with the alignment-specifier (9.12.2 [dcl.align]) given by alignas(A) if A != -1 and is otherwise declared without an alignment-specifier,
  • (29+.9) it is a bit-field (11.4.10 [class.bit]) with the width given by W if W != -1 and is otherwise not a bit-field,
  • (29+.10) it is declared with the attribute [[no_unique_address]] (9.12.12 [dcl.attr.nouniqueaddr]) if NUA is true and is otherwise declared without that attribute.

Data member descriptions are represented by reflections (6.8.2 [basic.fundamental]) returned by std::meta::data_member_spec ([meta.reflection.define.aggregate]) and can be reified as data members of a class using std::meta::define_aggregate ([meta.reflection.define.aggregate]). — end note ]

11.7.1 [class.derived.general] General

Introduce the term “direct base class relationship” to paragraph 2.

2 The component names of a class-or-decltype are those of its nested-name-specifier, type-name, and/or simple-template-id. A class-or-decltype shall denote a (possily cv-qualified) class type that is not an incompletely defined class (11.4 [class.mem]); any cv-qualifiers are ignored. The class denoted by the class-or-decltype of a base-specifier is called a direct base class for the class being defined; each such base-specifier introduces a direct base class relationship between the class being defined and the direct base class. The lookup for the component name of the type-name or simple-template-id is type-only (6.5 [basic.lookup]). […]

11.8.1 [class.access.general] General

Prefer “type alias” rather than typedef-name in the note that follows paragraph 4.

Note 3: Because access control applies to the declarations named, if access control is applied to a typedef-name type alias, only the accessibility of the typedef or alias declaration itself is considered. The accessibility of the entity referred to by the typedef-name underlying entity is not considered. — end note ]

12.1 [over.pre] Preamble

Move the definition “overload set” from 6.5 [basic.lookup] to paragraph 2, rewrite the preamble to better describe overload resolution, and add a note explaining the expressions that form overload sets.

2 An overload set is a set of declarations that each denote a function or function template. Using these declarations as a starting point, the process of overload resolution attempts to determine When a function is named in a call, which function declaration is being referenced and the validity of the call are determined by comparing the types of the arguments at the a point of use with the types of the parameters in candidate functions in the declarations in the overload set. This function selection process is called overload resolution and Overload resolution is defined in [over.match].

Note 1: Overload sets are formed by id-expressions naming functions and function templates and by splice-expressions designating entities of the same kinds. — end note ]

12.2.1 [over.match.general] General

Modify paragraphs 3 and 4 to clarify that access rules do not apply in all contexts.

3 If a best viable function exists and is unique, overload resolution succeeds and produces it as the result. Otherwise overload resolution fails and the invocation is ill-formed. When overload resolution succeeds, and the best viable function is not neither designated in a manner exempt from access rules nor accessible in the context in which it is used, the program is ill-formed.

4 Overload resolution results in a usable candidate if overload resolution succeeds and the selected candidate is either not a function (12.5 [over.built]), or is a function that is not deleted and is either designated in a manner exempt from access rules or is accessible from the context in which overload resolution was performed.

12.2.2.2.2 [over.call.func] Call to named function

Modify paragraph 1 to clarify that this section will also apply to splices of function templates.

1 Of interest in [over.call.func] are only those function calls in which the posfix-expression ultimately contains an id-expression or splice-expression that denotes one or more functions or function templates. Such a postfix-expression, perhaps nested arbitrarily deep in parentheses, has one of the following forms:

  postfix-expression:
     postfix-expression . id-expression
+    postfix-expression . splice-expression
     postfix-expression -> id-expression
+    postfix-expression -> splice-expression
     primary-expression

These represent two syntactic subcategories of function calls: qualified function calls and unqualified function calls.

Modify paragraph 2 to account for overload resolution of splice-expressions. Massage the wording to better account for member function templates.

2 In qualified function calls, the function is named designated by an id-expression or splice-expression preceded by an -> or . operator. Since the construct A->B is generally equivalent to (*A).B, the rest of [over] assumes, without loss of generality, that all member function calls have been normalized to the form that uses an object and the . operator. Furthermore, [over] assumes that the postfix-expression that is the left operand of the . operator has type “cv T” where T denotes a class.102 The function and function template declarations either found by name lookup if the dot is followed by an id-expression, or as specified by [expr.prim.splice] if the dot is followed by a splice-expression, undergo the adjustments described in [over.match.funcs.general] and thereafter constitute the set of candidate functions. The argument list is the expression-list in the call augmented by the addition of the left operand of the . operator in the normalized member function call as the implied object argument (12.2.2 [over.match.funcs]).

Modify paragraph 3 to account for overload resolution of splice-expressions. Massage the wording to better account for member function templates.

3 In unqualified function calls, the function is named by a primary-expression. The function and function template declarations either found by name lookup, or as specified by [expr.prim.splice] if the primary-expression is a (possibly parenthesized) splice-expression, undergo the adjustments described in [over.match.funcs.general] and thereafter constitute the set of candidate functions. Because of the rules for name lookup, the set of candidate functions consists either entirely of non-member functions or entirely of member functions of some class T. In the former case or if the primary-expression is a splice-expression or the address of an overload set, the argument list is the same as the expression-list in the call. Otherwise, the argument list is the expression-list in the call augmented by the addition of an implied function argument as in a qualified function call. If the current class is, or is derived from, T, and the keyword this (7.5.3 [expr.prim.this]) refers to it, then the implied object argument is (*this). Otherwise, a contrived object of type T becomes the implied object argument;103 if overload resolution selects a non-static member function, the call is ill-formed.

12.2.2.9 [over.match.class.deduct] Class template argument deduction

Extend paragraph 1 to work with splice-type-specifiers.

1 When resolving a placeholder for a deduced class type (9.2.9.8 [dcl.type.class.deduct]) where the template-name or splice-type-specifier names designates a primary class template C, a set of functions and function templates, called the guides of C, is formed comprising:

Extend paragraph 3 to also cover splice-type-specifiers.

3 When resolving a placeholder for a deduced class type (9.2.9.3 [dcl.type.simple]) where the template-name or splice-type-specifier names designates an alias template A, the defining-type-id of A must be of the form

typenameopt nested-name-specifieropt templateopt simple-template-id

as specified in 9.2.9.3 [dcl.type.simple]. The guides of A are the set of functions or function templates formed as follows. …

5.1.1 12.3 [over.over] Address of an overload set

Remove the explicit references to id-expressions from paragraph 1 to allow taking the address of an overload set specified by a splice-expression:

1 An id-expression whose terminal name refers expression that designates to an overload set S and that appears without arguments is resolved to a function, a pointer to function, or a pointer to member function for a specific function that is chosen from a set of functions selected from S determined based on the target type required in the context (if any), as described below. […]

The id-expression expression can be preceded by the & operator.

12.5 [over.built] Built-in operators

Add built-in operator candidates for std::meta::info to 12.5 [over.built]:

16 For every T, where T is a pointer-to-member type, std::meta::info, or std::nullptr_t, there exist candidate operator functions of the form

bool operator==(T, T);
bool operator!=(T, T);

13.2 [temp.param] Template parameters

Extend type-parameter to permit splice-specifiers as default template arguments for template template parameters. Also extend the grammar for type-constraint to include splice-type-specifier.

  type-parameter:
      type-parameter-key ...opt identifieropt
      type-parameter-key identifieropt = type-id
      type-constraint ...opt identifieropt
      type-constraint identifieropt = type-id
      template-head type-parameter-key ...opt identifieropt
      template-head type-parameter-key identifieropt = id-expression
+     template-head type-parameter-key identifieropt = splice-template-argument

  type-constraint:
      nested-name-specifieropt concept-name
      nested-name-specifieropt concept-name < template-argument-listopt>
+     splice-type-specifier

Add a paragraph after paragraph 3 to restrict which splice-type-specifiers form type-constraints.

3+ A non-dependent splice-type-specifier only forms a type-constraint when it designates a concept. A type-constraint of the form splice-type-specifier shall not appear in a type-parameter.

13.3 [temp.names] Names of template specializations

Define the term splice-template-argument, and add it as a production for template-argument.

  template-argument:
      constant-expression
      type-id
      id-expression
      braced-init-list
+     splice-template-argument

+ splice-template-argument:
+     splice-specifier

Extend and re-format paragraph 3 of 13.3 [temp.names]:

3 A < is interpreted as the delimiter of a template-argument-list if it follows

  • (3.1) a splice-specifier that either appears in a type-only context or is preceded by template or typename, or
  • (3.2) a name that is not a conversion-function-id and
    • (3.2.1) that follows the keyword template or a ~ after a nested-name-specifier or in a class member access expression, or
    • (3.2.2) for which name lookup finds the injected-class-name of a class template or finds any declaration of a template, or
    • (3.2.3) that is an unqualified name for which name lookup either finds one or more functions or finds nothing, or
    • (3.2.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 (13.8.3.2 [temp.dep.type]) denotes a template where an expression might appear. — end note ]

Example 1:
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
}
— end example ]

Clarify that the > disambiguation in paragraph 4 also applies to the parsing of splice-specialization-specifiers:

4 When parsing a template-argument-list, the first non-nested >111 is taken as the ending delimiter rather than a greater-than operator. Similarly, the first non-nested >> is treated as two consecutive but distinct > tokens, the first of which is taken as the end of the template-argument-list and completes the template-id or splice-specialization-specifier.

Note 2: The second > token produced by this replacement rule can terminate an enclosing template-id or splice-specialization-specifier construct or it can be part of a different construct (e.g., a cast). — end note ]

Extend the definition of a valid template-id to also cover splice-specialization-specifiers:

7 A template-id or splice-specialization-specifier is valid if

  • (7.1) there are at most as many arguments as there are parameters or a parameter is a template parameter pack (13.7.4 [temp.variadic]),
  • (7.2) there is an argument for each non-deducible non-pack parameter that does not have a default template-argument,
  • (7.3) each template-argument matches the corresponding template-parameter (13.4 [temp.arg]),
  • (7.4) substitution of each template argument into the following template parameters (if any) succeeds, and
  • (7.5) if the template-id or splice-specialization-specifier is non-dependent, the associated constraints are satisfied as specified in the next paragraph.

A simple-template-id or splice-specialization-specifier shall be valid unless it names a function template specialization (13.10.3 [temp.deduct]).

Extend paragraph 8 to require constraints to also be satisfied by splice-specialization-specifiers:

8 When the template-name of a simple-template-id or the splice-specifier of a splice-specialization-specifier designates names a constrained non-function template or a constrained template template-parameter, and all template-arguments in the simple-template-id or splice-specialization-specifier are non-dependent (13.8.3.5 [temp.dep.temp]), the associated constraints (13.5.3 [temp.constr.decl]) of the constrained template shall be satisfied (13.5.2 [temp.constr.constr]).

Modify footnote 111 to account for splice-specialization-specifiers:

111) A > that encloses the type-id of a dynamic_cast, static_cast, reinterpret_cast or const_cast, or which encloses the template-arguments of a subsequent template-id or splice-specialization-specifier, is considered nested for the purpose of this description.

13.4.1 [temp.arg.general] General

Modify paragraph 1; there are now four forms of template-argument.

1 There are three four forms of template-argument, three of which corresponding to the three forms of template-parameter: type, non-type and template. The fourth argument form, splice template argument, is considered to match the form of any template parameter. The type and form of each template-argument specified in a template-id or in a splice-specialization-specifier shall match the type and form specified for the corresponding parameter declared by the template in its template-parameter-list.

Clarify ambiguity between splice-expressions and splice-template-arguments in paragraph 3:

3 A template-argument of the form splice-specifier is interpreted as a splice-template-argument. In a For any other template-argument, an ambiguity between a type-id and an expression is resolved to a type-id, regardless of the form of the corresponding template-parameter.

Example 2:
  template<class T> void f();   // #1
  template<int I> void f();     // #2

  void g() {
    f<int()>();       // int() is a type-id: calls (#1) the first f()

+   constexpr int x = 42;
+   f<[:^^int:]>();    // splice-template-argument: calls (#1)
+   f<[:^^x:]>();      // splice-template-argument: calls (#2)
  }
— end example ]

Clarify in paragraph 9 that default template arguments also apply to splice-specialization-specifiers:

9 When a simple-template-id or splice-specialization-specifier does not name designate a function, a default template-argument is implicitly instantiated when the value of that default argument is needed.

13.4.2 [temp.arg.type] Template type arguments

Extend 13.4.2 [temp.arg.type]/1 to cover splice template arguments:

1 A template-argument for a template-parameter which is a type shall either be a type-id or a splice-template-argument whose splice-specifier designates a type.

13.4.3 [temp.arg.nontype] Template non-type arguments

[ Drafting note: We don’t think we have to change anything here, since if E is a splice-specifier that can be interpreted as a splice-expression, the requirements already fall out based on how paragraphs 1 and 3 are already worded ]

1 If the type T of a template-parameter ([temp.param]) contains a placeholder type ([dcl.spec.auto]) or a placeholder for a deduced class type ([dcl.type.class.deduct]), the type of the parameter is the type deduced for the variable x in the invented declaration

T x = E ;

where E is the template argument provided for the parameter.

2 The value of a non-type template-parameter P of (possibly deduced) type T […]

3 Otherwise, a temporary variable

constexpr T v = A;

is introduced.

13.4.4 [temp.arg.template] Template template arguments

Extend 13.4.4 [temp.arg.template]/1 to cover splice template arguments:

1 A template-argument for a template template-parameter shall be the name of a class template or an alias template, expressed as an id-expression or a splice-template-argument. Only primary templates are considered when matching the template argument with the corresponding parameter; partial specializations are not considered even if their parameter lists match that of the template template parameter.

13.5.4 [temp.constr.normal] Constraint normalization

Include a reference to splice-expressions in Note 1.

Note 1: Normalization of constraint-expressions is performed when determining the associated constraints ([temp.constr.constr]) of a declaration and when evaluating the value of an id-expression or splice-expression that names a concept specialization ([expr.prim.id] , [expr.prim.splice]) — end note ]

13.6 [temp.type] Type equivalence

Extend paragraph 1 to also define the “sameness” of splice-specialization-specifiers:

1 Two template-ids or splice-specialization-specifiers are the same if

  • (1.1) their template-names, operator-function-ids, or literal-operator-ids, or splice-specifiers refer to the same template, and
  • (1.2) their corresponding type template-arguments are the same type, and
  • (1.3) the template parameter values determined by their corresponding non-type template arguments (13.4.3 [temp.arg.nontype]) are template-argument-equivalent (see below), and
  • (1.4) their corresponding template template-arguments refer to the same template.

Two template-ids or splice-specialization-specifiers that are the same refer to the same class, function, or variable.

Extend template-argument-equivalent in paragraph 2 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 their values are the same, or
  • (2.4) they are of enumeration type and their values are the same, or
  • (2.5) […]

13.7.2.3 [temp.deduct.guide] Deduction guides

Extend paragraph 1 to clarify that splice-type-specifiers can also leverage deduction guides.

1 Deduction guides are used when a template-name or splice-type-specifier appears as a type specifier for a deduced class type (9.2.9.8 [dcl.type.class.deduct]). Deduction guides are not found by name lookup. Instead, when performing class template argument deduction (12.2.2.9 [over.match.class.deduct]), all reachable deduction guides declared for the class template are considered.

13.7.3 [temp.mem] Member templates

Clarify in Note 1 that a specialization of a conversion function template can be formed through a splice-expression.

Note 1: A specialization of a conversion function template is referenced in the same way as a non-template conversion function that converts to the same type (11.4.8.3 [class.conv.fct]).

An expression designating a particular specialization of a conversion function template can only be formed with a splice-expression. There is no analogous syntax to form a template-id (13.3 [temp.names]) for such a function by providing an explicit template argument list (13.10.2 [temp.arg.explicit]). — end note ]

13.7.8 [temp.alias] Alias templates

Extend paragraph 2 to enable reflection of alias template specializations.

2 When a A template-id that refers to the specialization of an alias template, it is equivalent to is a typedef-name for a type alias whose underlying entity is the associated type obtained by substitution of its template-arguments for the template-parameters in the defining-type-id of the alias template.

13.8.1 [temp.res.general] General

Extend paragraph 4 to define what it means for a splice-specifier to appear in a type-only context. Also using-enum-declarators to the list of type-only contexts, as it allows the typename to be elided from a splice-type-specifier in non-dependent contexts.

4 A qualified or unqualified name is said to be in a type-only context if it is the terminal name of

  • (4.1) a typename-specifier, type-requirement, nested-name-specifier, elaborated-type-specifier, class-or-decltype, using-enum-declarator or
  • (4.2) […]
    • (4.4.6) parameter-declaration of a (non-type) template-parameter.

A splice-specifier or splice-specialization-specifier ([basic.splice]) is said to be in a type-only context if a hypothetical qualified name appearing in the same position would be in a type-only context.

Example 5:
template<class T> T::R f();
template<class T> void f(T::R);   // ill-formed, no diagnostic required: attempt to
                                  // declare a `void` variable template
enum class Enum { A, B, C };

template<class T> struct S {
  using Ptr = PtrTraits<T>::Ptr;  // OK, in a defining-type-id
  using Alias = [:^^int];         // OK, in a defining-type-id
  T::R f(T::P p) {                // OK, class scope
    return static_cast<T::R>(p);  // OK, type-id of a `static_cast`
  }
  auto g() -> S<T*>::Ptr;         // OK, trailing-return-type
  auto h() -> [:^^S:]<T*>;        // OK, trailing-return-type
  using enum [:^^Enum:];          // OK, using-enum-declarator
};
template<typename T> void f() {
  void (*pf)(T::X);               // variable `pf` of type `void*` initialized
                                  // with `T::X`
  void g(T::X);                   // error: `T::X` at block scope does not denote
                                  // a type (attempt to declare a `void` variable)
}
— end example ]

13.8.3.2 [temp.dep.type] Dependent types

Account for dependent splice-specifiers in paragraph 7.

7 An initializer is dependent if any constituent expression (6.9.1 [intro.execution]) of the initializer is type-dependent. A placeholder type (9.2.9.7.1 [dcl.spec.auto.general]) is dependent if it designates a type deduced from a dependent initializer or if its type-constraint (if any) contains a dependent splice-specifier ([temp.dep.splice]).

Apply a drive-by fix to paragraph 8 to account for placeholders for deduced class types whose template is dependent, while extending the definition to apply to splice-specifiers.

8 A placeholder for a deduced class type (9.2.9.8 [dcl.type.class.deduct]) is dependent if

  • (8.1) it has a dependent initializer, or
  • (8.2) it has a dependent template-name or a dependent splice-specifier, or
  • (8.3) it refers to an alias template that is a member of the current instantiation and whose defining-type-id is dependent after class template argument deduction (12.2.2.9 [over.match.class.deduct]) and substitution (13.7.8 [temp.alias]).

Account for dependent splice-type-specifiers in paragraph 10:

10 A type is dependent if it is

  • (10.1) a template parameter,
  • (10.2)
  • (10.11) denoted by a simple-template-id in which either the template name is a template parameter or any of the template arguments is a dependent type or an expression that is type-dependent or value-dependent or is a pack expansion,121
  • (10.12) a pack-index-specifier, or
  • (10.13) denoted by decltype(expression), where expression is type-dependent., or
  • (10.14) denoted by a splice-type-specifier in which either the splice-specifier is dependent or any of the template arguments is a dependent type or an expression that is type-dependent or value-dependent or is a pack expansion.

13.8.3.3 [temp.dep.expr] Type-dependent expressions

Add to the list of never-type-dependent expression forms in paragraph 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 form splice-specifier or template splice-specifier < template-argument-listopt > is type-dependent if

  • (9.1) the splice-specifier is dependent, or
  • (9.2) the optional template-argument-list contains a dependent type argument, a value-dependent non-type argument, a dependent template template argument, or a dependent splice-specifier.

13.8.3.4 [temp.dep.constexpr] Value-dependent expressions

Add at the end of 13.8.3.4 [temp.dep.constexpr]/2 (before the note):

2 An id-expression is value-dependent if:

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 its nested-name-specifier (if any) is dependent or if it has a dependent type-id, a dependent qualified-namespace-specifier, a dependent template-name, or a value-dependent or type-dependent id-expression.

Add a new paragraph after 13.8.3.4 [temp.dep.constexpr]/5:

6 A primary-expression of the form splice-specifier or template splice-specifier < template-argument-listopt > is value-dependent if

  • (6.1) the splice-specifier is dependent, or
  • (6.2) the optional template-argument-list contains a dependent type argument, a value-dependent non-type argument, a dependent template template argument, or a dependent splice-template-argument.

13.8.3.4+ [temp.dep.splice] Dependent splice specifiers

Add a new subsection of 13.8.3 [temp.dep] following 13.8.3.4 [temp.dep.constexpr], and renumber accordingly.

Dependent splice specifiers [temp.dep.splice]

1 A splice-specifier is dependent if its converted constant-expression is value-dependent. A splice-specialization-specifier or splice-scope-specifier is dependent if its splice-template-argument is dependent.

2

Example 1:
template <auto T, auto NS, auto C, auto V>
void fn() {
  using a = [:T:]<0>;  // [:T:] and [:T:]<V> are dependent

  static_assert(template [:C:]<typename [:NS:]::template TCls<0>>);
    // [:C:] and [:NS:] are dependent
}

namespace NS {
template <auto V> struct TCls {};
template <typename> concept Concept = requires { requires true; };
}

int main() {
  static constexpr int v = 1;
  fn<^^NS::TCls, ^^NS, ^^NS::Concept, ^^v>();
}
— end example ]

13.9.1 [temp.spec.general] General

Add a note after paragraph 1 enumerating other constructs that are subject to instantiation.

1 The act of instantiating a function, a variable, a class, a member of a class template, or a member template is referred to as template instantiation.

Note 1: The following constructs are also separately subject to instantiation:

  • (1.1) Default arguments in template specializations and members of class templates,
  • (1.2) Default template arguments,
  • (1.3) Default member initializers in template specializations,
  • (1.4) noexcept-specifiers of template specializations and members of class templates, and
  • (1.5) type-constraints and requires-clauses of template specializations and member functions.
— end note ]

13.9.4 [temp.expl.spec] Explicit specialization

Modify paragraph 9 to allow splice-specialization-specifiers to be used like incompletely-defined classes.

9 A simple-template-id or splice-specialization-specifier that names designates a class template explicit specialization that has been declared but not defined can be used exactly like the names of other incompletely-defined classes (6.8 [basic.types]).

13.10.3.1 [temp.deduct.general] General

Cover splice-specialization-specifiers in paragraph 2:

2 When an explicit template argument list is specified, if the given template-id or splice-specialization-specifier is not valid (13.3 [temp.names]), type deduction fails. Otherwise, the specified template argument values are substituted for the corresponding template parameters as specified below.

13.10.3.2 [temp.deduct.call] Deducing template arguments from a function call

Modify paragraph 4.3 to treat parameter types of function templates that are specified using splice-specialization-specifiers the same as parameter types that are specified using simple-template-ids.

  • (4.3) If P is a class and P has the form simple-template-id or typenameopt splice-specialization-specifier, then the transformed A can be a derived class D of the deduced A. Likewise, if P is a pointer to a class of the form simple-template-id or typenameopt splice-specialization-specifier, the transformed A can be a pointer to a derived class D pointed to by the deduced A. However, if there is a class C that is a (direct or indirect) base class of D and derived (directly or indirectly) from a class B and that would be a valid deduced A, the deduced A cannot be B1 or pointer to B, respectively.

13.10.3.6 [temp.deduct.type] Deducing template arguments from a type

Modify paragraph 20 to clarify that the construct enclosing a template argument might also be a splice-specialization-specifier.

20 If P has a form that contains <i>, and if the type of i differs from the type of the corresponding template parameter of the template named by the enclosing simple-template-id or splice-specialization-specifier, deduction fails. If P has a form that contains [i], and if the type of i is not an integral type, deduction fails.123 If P has a form that includes noexcept(i) and the type of i is not bool, deduction fails.

15.2 [cpp.cond] Conditional inclusion

Extend paragraph 9 to clarify that splice-specifiers 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 or lambda-expression appears in the controlling constant expression.

5.2 Library

16.3.2.4 [structure.specifications] Detailed specifications

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]).

4 […] Next, the semantics of the code sequence are determined by the Constraints, Mandates, Constant When, Preconditions, Effects, Synchronization, Postconditions, Returns, Throws, Complexity, Remarks, and Error conditions specified for the function invocations contained in the code sequence. […]

16.4.5.2.1 [namespace.std] Namespace std

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. […]

6a 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: For example, std::meta::members_of might not return reflections of standard functions that an implementation handles through an extra-linguistic mechanism. — end note ]

6b Let C denote a standard library class or class template specialization. It is unspecified if or how a reflection value can be formed to any private member of C, or what the names of such members may be.

7 A translation unit shall not declare namespace std to be an inline namespace ([namespace.def]).

21.3.3 [meta.type.synop] Header <type_traits> synopsis

Add 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;

21.3.5.2 [meta.unary.cat] Primary type categories

Add the is_reflection primary type category to the table in paragraph 3:

Template Condition Comments
template <class T>
struct is_void;
T is void
template <class T>
struct is_reflection;

T is std::meta::info


[meta.reflection.synop] Header <meta> synopsis

Add a new subsection in 21 [meta] after 21.3 [type.traits]:

Header <meta> synopsis

#include <initializer_list>

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 symbol_of(operators op);
  consteval u8string_view u8symbol_of(operators op);

  // [meta.reflection.names], reflection names and locations
  consteval bool has_identifier(info r);

  consteval string_view identifier_of(info r);
  consteval u8string_view u8identifier_of(info r);

  consteval string_view display_string_of(info r);
  consteval u8string_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_bases(info type);
  consteval vector<info> get_public_static_data_members(info type);
  consteval vector<info> get_public_nonstatic_data_members(info type);

  // [meta.reflection.layout], reflection layout queries
  struct member_offset {
    ptrdiff_t bytes;
    ptrdiff_t bits;
    constexpr ptrdiff_t total_bits() const;
    auto operator<=>(member_offset const&) const = default;
  };
  consteval member_offset 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(const T& value);
  template<class T>
    consteval info reflect_object(T& object);
  template<class T>
    consteval info reflect_function(T& fn);

  // [meta.reflection.define.aggregate], class definition generation
  struct data_member_options;
  consteval info data_member_spec(info type,
                                  data_member_options options);
  consteval bool is_data_member_spec(info r);
  template <reflection_range R = initializer_list<info>>
  consteval info define_aggregate(info type_class, R&&);

  // [meta.reflection.unary.cat], primary type categories
  consteval bool is_void_type(info type);
  consteval bool is_null_pointer_type(info type);
  consteval bool is_integral_type(info type);
  consteval bool is_floating_point_type(info type);
  consteval bool is_array_type(info type);
  consteval bool is_pointer_type(info type);
  consteval bool is_lvalue_reference_type(info type);
  consteval bool is_rvalue_reference_type(info type);
  consteval bool is_member_object_pointer_type(info type);
  consteval bool is_member_function_pointer_type(info type);
  consteval bool is_enum_type(info type);
  consteval bool is_union_type(info type);
  consteval bool is_class_type(info type);
  consteval bool is_function_type(info type);
  consteval bool is_reflection_type(info type);

  // [meta.reflection.unary.comp], composite type categories
  consteval bool is_reference_type(info type);
  consteval bool is_arithmetic_type(info type);
  consteval bool is_fundamental_type(info type);
  consteval bool is_object_type(info type);
  consteval bool is_scalar_type(info type);
  consteval bool is_compound_type(info type);
  consteval bool is_member_pointer_type(info type);

  // [meta.reflection unary.prop], type properties
  consteval bool is_const_type(info type);
  consteval bool is_volatile_type(info type);
  consteval bool is_trivially_copyable_type(info type);
  consteval bool is_standard_layout_type(info type);
  consteval bool is_empty_type(info type);
  consteval bool is_polymorphic_type(info type);
  consteval bool is_abstract_type(info type);
  consteval bool is_final_type(info type);
  consteval bool is_aggregate_type(info type);
  consteval bool is_signed_type(info type);
  consteval bool is_unsigned_type(info type);
  consteval bool is_bounded_array_type(info type);
  consteval bool is_unbounded_array_type(info type);
  consteval bool is_scoped_enum_type(info type);

  template <reflection_range R = initializer_list<info>>
    consteval bool is_constructible_type(info type, R&& type_args);
  consteval bool is_default_constructible_type(info type);
  consteval bool is_copy_constructible_type(info type);
  consteval bool is_move_constructible_type(info type);

  consteval bool is_assignable_type(info type_dst, info type_src);
  consteval bool is_copy_assignable_type(info type);
  consteval bool is_move_assignable_type(info type);

  consteval bool is_swappable_with_type(info type_dst, info type_src);
  consteval bool is_swappable_type(info type);

  consteval bool is_destructible_type(info type);

  template <reflection_range R = initializer_list<info>>
    consteval bool is_trivially_constructible_type(info type, R&& type_args);
  consteval bool is_trivially_default_constructible_type(info type);
  consteval bool is_trivially_copy_constructible_type(info type);
  consteval bool is_trivially_move_constructible_type(info type);

  consteval bool is_trivially_assignable_type(info type_dst, info type_src);
  consteval bool is_trivially_copy_assignable_type(info type);
  consteval bool is_trivially_move_assignable_type(info type);
  consteval bool is_trivially_destructible_type(info type);

  template <reflection_range R = initializer_list<info>>
    consteval bool is_nothrow_constructible_type(info type, R&& type_args);
  consteval bool is_nothrow_default_constructible_type(info type);
  consteval bool is_nothrow_copy_constructible_type(info type);
  consteval bool is_nothrow_move_constructible_type(info type);

  consteval bool is_nothrow_assignable_type(info type_dst, info type_src);
  consteval bool is_nothrow_copy_assignable_type(info type);
  consteval bool is_nothrow_move_assignable_type(info type);

  consteval bool is_nothrow_swappable_with_type(info type_dst, info type_src);
  consteval bool is_nothrow_swappable_type(info type);

  consteval bool is_nothrow_destructible_type(info type);

  consteval bool is_implicit_lifetime_type(info type);

  consteval bool has_virtual_destructor(info type);

  consteval bool has_unique_object_representations(info type);

  consteval bool reference_constructs_from_temporary(info type_dst, info type_src);
  consteval bool reference_converts_from_temporary(info type_dst, info type_src);

  // [meta.reflection.unary.prop.query], type property queries
  consteval size_t rank(info type);
  consteval size_t extent(info type, unsigned i = 0);

  // [meta.reflection.rel], type relations
  consteval bool is_same_type(info type1, info type2);
  consteval bool is_base_of_type(info type_base, info type_derived);
  consteval bool is_virtual_base_of_type(info type_base, info type_derived);
  consteval bool is_convertible_type(info type_src, info type_dst);
  consteval bool is_nothrow_convertible_type(info type_src, info type_dst);
  consteval bool is_layout_compatible_type(info type1, info type2);
  consteval bool is_pointer_interconvertible_base_of_type(info type_base, info type_derived);

  template <reflection_range R = initializer_list<info>>
    consteval bool is_invocable_type(info type, R&& type_args);
  template <reflection_range R = initializer_list<info>>
    consteval bool is_invocable_r_type(info type_result, info type, R&& type_args);

  template <reflection_range R = initializer_list<info>>
    consteval bool is_nothrow_invocable_type(info type, R&& type_args);
  template <reflection_range R = initializer_list<info>>
    consteval bool is_nothrow_invocable_r_type(info type_result, info type, R&& type_args);

  // [meta.reflection.trans.cv], const-volatile modifications
  consteval info remove_const(info type);
  consteval info remove_volatile(info type);
  consteval info remove_cv(info type);
  consteval info add_const(info type);
  consteval info add_volatile(info type);
  consteval info add_cv(info type);

  // [meta.reflection.trans.ref], reference modifications
  consteval info remove_reference(info type);
  consteval info add_lvalue_reference(info type);
  consteval info add_rvalue_reference(info type);

  // [meta.reflection.trans.sign], sign modifications
  consteval info make_signed(info type);
  consteval info make_unsigned(info type);

  // [meta.reflection.trans.arr], array modifications
  consteval info remove_extent(info type);
  consteval info remove_all_extents(info type);

  // [meta.reflection.trans.ptr], pointer modifications
  consteval info remove_pointer(info type);
  consteval info add_pointer(info type);

  // [meta.reflection.trans.other], other transformations
  consteval info remove_cvref(info type);
  consteval info decay(info type);
  template <reflection_range R = initializer_list<info>>
    consteval info common_type(R&& type_args);
  template <reflection_range R = initializer_list<info>>
    consteval info common_reference(R&& type_args);
  consteval info type_underlying_type(info type);
  template <reflection_range R = initializer_list<info>>
    `consteval info invoke_result(info type, R&& type_args);
  consteval info unwrap_reference(info type);
  consteval info unwrap_ref_decay(info type);

  // [meta.reflection.tuple.variant], tuple and variant queries
  consteval size_t tuple_size(info type);
  consteval info tuple_element(size_t index, info type);

  consteval size_t variant_size(info type);
  consteval info variant_alternative(size_t index, info type);
}

1 Each function, and each instantiation of each function template, specified in this header is a designated addressable function ([namespace.std]).

2 The behavior of any function specified in namespace std::meta is implementation-defined when a reflection of a construct not otherwise specified by this document is provided as an argument.

Note 1: Values of type std::meta::info may represent implementation-defined constructs (6.8.2 [basic.fundamental]). — end note ]

Note 2: The behavior of many of the functions specified in namespace std::meta have semantics that would be affected by the completeness of class types represented by reflection arguments ([temp.inst]). For such functions, for any reflection r such that dealias(r) represents a specialization of a templated class with a reachable definition, the specialization is implicitly instantiated.

Example 1:
template <class T>
struct X {
  T mem;
};

static_assert(size_of(^^X<int>) == sizeof(int)); // instantiates X<int>
— end example ]
— end note ]

3 Any function in namespace std::meta that whose return type is string_view or u8string_view returns an object V such that V.data()[V.size()] == '\0'.

Example 2:
struct C { };

constexpr string_view sv = identifier_of(^^C);
static_assert(sv == "C");
static_assert(sv.data()[0] == 'C');
static_assert(sv.data()[1] == '\0');
— end example ]

[meta.reflection.operators] Operator representations

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.

[ Drafting note: The names here are chosen after the punctuation marks, not the semantic operation, and we are sticking with the Unicode names — or resorting to the secondary name when the primary name is not well known (e.g. solidus -> slash) ]

Table 1: Enum class operators [meta.reflection.operators]
Constant
Corresponding operator-function-id
Operator symbol name
op_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_exclamation 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_exclamation_equals operator!= !=
op_less operator< <
op_greater operator> >
op_less_equals operator<= <=
op_greater_equals operator>= >=
op_spaceship operator<=> <=>
op_ampersand_ampersand 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 whose corresponding operator-function-id is the unqualified name of the entity represented by r.

consteval string_view symbol_of(operators op);
consteval u8string_view u8symbol_of(operators op);

4 Constant When: The value of op corresponds to one of the enumerators in operators.

5 Returns: string_view or u8string_view containing the characters of the operator symbol name corresponding to op, respectively encoded with the ordinary literal encoding or with UTF-8.

[meta.reflection.names] Reflection names and locations

consteval bool has_identifier(info r);

1 Returns:

  • (1.1) If r is an unnamed entity other than a class that has a typedef name for linkage purposes (9.2.4 [dcl.typedef]), then false.
  • (1.2) Otherwise, if r represents a class type C, then true when either the class-name of C is an identifier or C has a typedef name for linkage purposes. Otherwise, false.
  • (1.3) Otherwise, if r represents a function, then true if the function is not a function template specialization, constructor, destructor, operator function, or conversion function. Otherwise, false.
  • (1.4) Otherwise, if r represents a function template, then true if r does not represent a constructor template, operator function template, or conversion function template. Otherwise, false.
  • (1.5) Otherwise, if r represents variable or a type alias, then !has_template_arguments(r).
  • (1.6) Otherwise, if r represents a structured binding, enumerator, non-static data member, template, namespace, or namespace alias, then true. Otherwise, false.
  • (1.7) Otherwise, if r represents a direct base class relationship, then has_identifier(type_of(r)).
  • (1.8) Otherwise, r represents a data member description (T, N, A, W, NUA) (11.4.1 [class.mem.general]); N != -1.
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) is true and the identifier that would be returned (see below) is representable by E.

4 Returns:

  • (4.1) If r represents a literal operator or literal operator template, then the ud-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, then the identifier introduced by the declaration of that entity.
  • (4.4) Otherwise, if r represents a direct base class relationship, then identifier_of(type_of(r)) or u8identifier_of(type_of(r)), respectively.
  • (4.5) Otherwise, r represents a data member description (T, N, A, W, NUA) (11.4.1 [class.mem.general]); a string or u8string respectively containing the identifier N encoded with E.
consteval string_view display_string_of(info r);
consteval u8string_view u8display_string_of(info r);

5 Returns: An implementation-defined string_view or u8string_view, respectively.

6 Recommended practice: Where possible, implementations should return a string 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 data member description, then source_location{}. Otherwise, an implementation-defined source_location value.

8 Recommended practice: If r represents an entity, name, or direct base class relationship that was introduced by a declaration, implementations should return a value corresponding to a declaration of the represented construct that is reachable from the evaluation construct. If there are multiple such declarations and one is a definition, a value corresponding to the definition is preferred.

[meta.reflection.queries] Reflection queries

consteval bool is_public(info r);
consteval bool is_protected(info r);
consteval bool is_private(info r);

1 Returns: true if r represents a class member or direct base class relationship that is public, protected, or private, respectively. Otherwise, false.

consteval bool is_virtual(info r);

2 Returns: true if r represents either a virtual member function or a direct base class relationship that is virtual. Otherwise, false.

consteval bool is_pure_virtual(info r);
consteval bool is_override(info r);

3 Returns: true if r 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 if r 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 if r represents a function that is a deleted function ([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 if r 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 if r represents a member function that is declared explicit. Otherwise, false. Note 1: If r represents a member function template that is declared explicit, is_explicit(r) is still false because in general such queries for templates cannot be answered. — end note ]

consteval bool is_noexcept(info r);

8 Returns: true if r represents a noexcept function type or a function or member function with a non-throwing exception specification ([except.spec]). Otherwise, false. Note 2: If r represents a function template that is declared noexcept, is_noexcept(r) is still false because in general such queries for templates cannot be answered. — end note ]

consteval bool is_bit_field(info r);

9 Returns: true if r represents a bit-field, or if r represents a data member description (T, N, A, W, NUA) (11.4.1 [class.mem.general]) for which W is not -1. Otherwise, false.

consteval bool is_enumerator(info r);

10 Returns: true if r represents an enumerator. Otherwise, false.

consteval bool is_const(info r);
consteval bool is_volatile(info r);

11 Returns: true if r 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 if r represents a mutable 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 if r 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 if r 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 if r 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 Returns: true if is_type(r) is true and there is some point in the evaluation context from which the type represented by dealias(r) is not an incomplete type ([basic.types]). Otherwise, false.

consteval bool has_complete_definition(info r);

17 Returns: true if r represents a function, class type, or enumeration type, such that no entities not already declared may be introduced within the scope of the entity represented by r. Otherwise false.

consteval bool is_namespace(info r);

18 Returns: true if r represents a namespace or namespace alias. Otherwise, false.

consteval bool is_variable(info r);

19 Returns: true if r represents a variable. Otherwise, false.

consteval bool is_type(info r);

20 Returns: true if r represents an entity whose underlying entity is a type. Otherwise, false.

consteval bool is_type_alias(info r);
consteval bool is_namespace_alias(info r);

21 Returns: true if r represents a type alias or namespace alias, respectively Note 3: A specialization of an alias template is a type alias — end note ]. Otherwise, false.

consteval bool is_function(info r);

22 Returns: true if r 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);

23 Returns: true if r 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);

24 Returns: true if r represents a function that is a special member function ([special]), a constructor, a default constructor, a copy constructor, a move constructor, an assignment operator, a copy assignment operator, a move assignment operator, or a destructor, respectively. Otherwise, false.

consteval bool is_template(info r);

25 Returns: true if r represents a function template, class template, variable template, alias template, or concept. Otherwise, false.

26 Note 4: A template specialization is not a template. is_template(^^std::vector) is true but is_template(^^std::vector<int>) is false. — 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);

27 Returns: true if r 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);

28 Returns: true if r 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);

29 Returns: true if r represents a value or object, respectively. Otherwise, false.

consteval bool is_structured_binding(info r);

30 Returns: true if r 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);

31 Returns: true if r represents a class member, namespace member, non-static data member, static member, or direct base class relationship, respectively. Otherwise, false.

consteval bool has_default_member_initializer(info r);

32 Returns: true if r represents a non-static data member that has a default member initializer. Otherwise, false.

consteval info type_of(info r);

33 Constant When: r represents a value, object, variable, function that is not a constructor or destructor, enumerator, non-static data member, bit-field, direct base class relationship, or data member description.

34 Returns: If r represents an entity, object, or value, then a reflection of the type of what is represented by r. Otherwise, if r represents a direct base class relationship, then a reflection of the type of the direct base class. Otherwise, for a data member description (T, N, A, W, NUA) (11.4.1 [class.mem.general]), a reflection of the type T.

consteval info object_of(info r);

35 Constant When: r is a reflection representing either an object or a variable denoting an object with static storage duration ([expr.const]).

36 Returns: If r is a reflection of a variable, then a reflection of the object denoted by the variable. Otherwise, r.

Example 1:
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
— end example ]
consteval info value_of(info r);

37 Constant When: r is a reflection representing

  • (37.1) either an object or variable, usable in constant expressions from some point in the evaluation context ([expr.const]), whose type is a structural type ([temp.type]),
  • (37.2) an enumerator, or
  • (37.3) a value.

38 Returns:

  • (38.1) If r is a reflection of an object o, or a reflection of a variable which designates an object o, then a reflection of the value held by o. The reflected value has type type_of(o), with the cv-qualifiers removed if this is a scalar type
  • (38.2) Otherwise, if r is a reflection of an enumerator, then a reflection of the value of the enumerator.
  • (38.3) Otherwise, r.
Example 2:
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
— end example ]
consteval info parent_of(info r);

39 Constant When: r represents a variable, structured binding, function, enumerator, class, class member, bit-field, template, namespace or namespace alias (other than ::), type alias, or direct base class relationship.

40 Returns: If r represents a non-static data member that is a direct member of an anonymous union, then a reflection representing the innermost enclosing anonymous union. Otherwise, a reflection of the class, function, or namespace that is the target scope ([basic.scope.scope]) of the first declaration of what is represented by r.

consteval info dealias(info r);

41 Returns: A reflection representing the underlying entity of r.

42

Example 3:
using X = int;
using Y = X;
static_assert(dealias(^^int) == ^^int);
static_assert(dealias(^^X) == ^^int);
static_assert(dealias(^^Y) == ^^int);
— end example ]
consteval info template_of(info r);
consteval vector<info> template_arguments_of(info r);

43 Constant When: has_template_arguments(r) is true.

44 Returns: A reflection of the primary template of r, and the reflections of the template arguments of the specialization represented by r, respectively.

45

Example 4:
template <class T, class U=T> struct Pair { };
template <class T> struct Pair<char, T> { };
template <class T> using PairPtr = Pair<T*>;

static_assert(template_of(^^Pair<int>) == ^^Pair);
static_assert(template_of(^^Pair<char, char>) == ^^Pair);
static_assert(template_arguments_of(^^Pair<int>).size() == 2);

static_assert(template_of(^^PairPtr<int>) == ^^PairPtr);
static_assert(template_arguments_of(^^PairPtr<int>).size() == 1);
— end example ]

[meta.reflection.member.queries], Reflection member queries

consteval vector<info> members_of(info r);

1 Constant When: r is a reflection representing either a class type that is complete from some point in the evaluation context or a namespace.

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 type alias,
  • 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 unless it is a prospective destructor that is not a selected destructor ([class.dtor]),
  • a non-static data member or unnamed bit-field, other than members of an anonymous union that is directly or indirectly members-of-representable,
  • a namespace, or
  • a namespace alias.

Note 1: Counterexamples of members-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-reachable from a point P if there exists a declaration D of M that is reachable from P, and either M is not TU-local or D is declared in the translation unit containing P.

4 Returns: A vector containing reflections of all members-of-representable members of the entity represented by r that are members-of-reachable from some point in the evaluation context ([expr.const]). If E represents a class C, then the vector also contains reflections representing all unnamed bit-fields declared within the member-specification of C. 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. Implicitly-declared special members appear after any user-declared members. — end note ]

consteval vector<info> bases_of(info type);

5 Constant When: dealias(type) is a reflection representing a complete class type.

6 Returns: Let C be the type represented by dealias(type). A vector containing the reflections of all the direct base class relationships, if any, of C. The direct base class relationships are indexed in the order in which the corresponding base classes appear in the base-specifier-list of C.

consteval vector<info> static_data_members_of(info type);

7 Constant When: dealias(type) represents a complete class type.

8 Returns: A vector containing each element e of members_of(type) such that is_variable(e) is true, in order.

consteval vector<info> nonstatic_data_members_of(info type);

9 Constant When: dealias(type) represents a complete class type.

10 Returns: A vector containing each element e of members_of(type) such that is_nonstatic_data_member(e) is true, in order.

consteval vector<info> enumerators_of(info type_enum);

11 Constant When: dealias(type_enum) represents an enumeration type and has_complete_definition(dealias(type_enum)) is true.

12 Returns: A vector containing the reflections of each enumerator of the enumeration represented by dealias(type_enum), in the order in which they are declared.

consteval vector<info> get_public_members(info type);

13 Constant When: dealias(type) represents a complete class type.

14 Returns: A vector containing each element e of members_of(type) such that is_public(e) is true, in order.

consteval vector<info> get_public_bases(info type);

15 Constant When: dealias(type) represents a complete class type.

16 Returns: A vector containing each element e of bases_of(type) such that is_public(e) is true, in order.

consteval vector<info> get_public_static_data_members(info type);

17 Constant When: dealias(type) represents a complete class type.

18 Returns: A vector containing each element e of static_data_members_of(type) such that is_public(e) is true, in order.

consteval vector<info> get_public_nonstatic_data_members(info type);

19 Constant When: dealias(type) represents a complete class type.

20 Returns: A vector containing each element e of nonstatic_data_members_of(type) such that is_public(e) is true, in order.

[meta.reflection.layout] Reflection layout queries

constexpr ptrdiff_t member_offset::total_bits() const;

1 Returns: bytes * CHAR_BIT + bits.

consteval member_offset offset_of(info r);

2 Constant When: r represents a non-static data member, unnamed bit-field, or direct base class relationship other than a virtual base class of an abstract class.

3 Let V be the offset in bits from the beginning of a complete object of type parent_of(r) to the subobject associated with the entity represented by r.

4 Returns: {V / CHAR_BIT, V % CHAR_BIT}.

consteval size_t size_of(info r);

5 Constant When: dealias(r) is a reflection of a type, object, value, variable of non-reference type, non-static data member, direct base class relationship, or data member description. If dealias(r) represents a type T, there is a point within the evaluation context from which T is not incomplete.

6 Returns: If r represents a non-static data member whose corresponding subobject has type T, or a data member description (T, N, A, W, NUA) (11.4.1 [class.mem.general]), then sizeof(T). Otherwise, if dealias(r) represents a type T, then sizeof(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: dealias(r) is a reflection representing a type, object, variable, non-static data member that is not a bit-field, direct base class relationship, or data member description. If dealias(r) represents a type T, there is a point within the evaluation context from which T is not incomplete.

8 Returns:

  • (8.1) If dealias(r) represents a type, variable, or object, then the alignment requirement of the entity or object.
  • (8.2) Otherwise, if r represents a direct base class relationship, then alignment_of(type_of(r)).
  • (8.3) Otherwise, if r represents a non-static data member, then the alignment requirement of the subobject associated with the represented entity within any object of type parent_of(r).
  • (8.4) Otherwise, r represents a data member description (T, N, A, W, NUA) (11.4.1 [class.mem.general]). The value A.
consteval size_t bit_size_of(info r);

9 Constant When: dealias(r) is a reflection of a type, object, value, variable of non-reference type, non-static data member, unnamed bit-field, direct base class relationship, or data member description. If dealias(r) represents a type T, there is a point within the evaluation context from which T is not incomplete.

10 Returns: If r represents a non-static data member that is a bit-field or unnamed bit-field with width W, or a data member description (T, N, A, W, NUA) (11.4.1 [class.mem.general]), then W. Otherwise, CHAR_BIT * size_of(r).

[meta.reflection.extract] Value extraction

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 type U that is usable in constant expressions from some point in the evaluation context and is_convertible_v<remove_reference_t<U>(*)[], remove_reference_t<T>(*)[]> is true.

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 class C with type X, then when T is X C::* and r does not represent a bit-field.
  • (6.2) Otherwise, if r represents an implicit object member function of class C with type F or F noexcept, then when T is F C::*.
  • (6.3) Otherwise, r represents a function, static member function, or explicit object member function of function type F or F noexcept, then when T is F*.

7 Returns:

  • (7.1) If T is a pointer type, then a pointer value pointing to the entity represented by r.
  • (7.2) Otherwise, a pointer-to-member 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 that r represents.

9 Constant When:

  • (9.1) U is a pointer type, T and U are similar types ([conv.qual]), and is_convertible_v<U, T> is true,
  • (9.2) U is not a pointer type and the cv-unqualified types of T and U are the same, or
  • (9.3) U is a closure type, T is a function pointer type, and the value that r represents is convertible to T.

10 Returns: the value that r represents converted to T.

template <class T>
  consteval T extract(info r);

11 Effects:

  • 12 If T is a reference type, then equivalent to return extract-ref<T>(r);
  • 13 Otherwise, if r represents a function, non-static data member, or member function, equivalent to return extract-member-or-function<T>(r);
  • 14 Otherwise, equivalent to return extract-value<T>(value_of(r))

[meta.reflection.substitute] Reflection substitution

template <class R>
concept reflection_range =
  ranges::input_range<R> &&
  same_as<ranges::range_value_t<R>, info> &&
  same_as<remove_cvref_t<ranges::range_reference_t<R>>, info>;
template <reflection_range R = initializer_list<info>>
consteval bool can_substitute(info templ, R&& arguments);

1 Constant When: templ represents a template and every reflection in arguments represents a construct usable as a template argument ([temp.arg]).

2 Let Z be the template represented by templ and let Args... be the sequence of entities, values, and objects represented by the elements of arguments.

3 Returns: true if Z<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) is true.

6 Let Z be the template represented by templ and let Args... be the sequence of entities, values, and objects represented by the elements of arguments.

7 Returns: ^^Z<Args...>.

8 Note 1: Z<Args..> is not instantiated. — end note ]

[meta.reflection.result] Expression result reflection

template <typename T>
  consteval info reflect_value(const T& expr);

1 Mandates: T is a structural type that is neither a reference type nor an array type.

2 Constant When: Any value computed by expr having pointer type, or every subobject of the value computed by expr having pointer or reference type, is the address of or refers to an object or function 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 of T.

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 function 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, where fn is the function designated by expr.

[meta.reflection.define.aggregate] Reflection class definition generation

1 The classes data_member_options and name_type are consteval-only types ([basic.types.general]), and are not a structural types ([temp.param]).

struct data_member_options {
  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> bit_width;
  bool no_unique_address = false;
};
template <class T> requires constructible_from<u8string, T>
consteval data_member_options::name_type(T&& value);

2 Effects: Initializes contents with u8string(value).

template<class T> requires constructible_from<string, T>
consteval data_member_options::name_type(T&& value);

3 Effects: Initializes contents with string(value).

Note 1: name_type provides a simple inner class that can be implicitly constructed from anything convertible to string or u8string. This allows a data_member_spec to accept an ordinary string literal (or string_view, string, etc) or a UTF-8 string literal (or u8string_view, u8string, etc) equally well.

constexpr auto mem1 = data_member_spec(^^int, {.name="ordinary_literal_encoding"});
constexpr auto mem2 = data_member_spec(^^int, {.name=u8"utf8_encoding"});
— end note ]
consteval info data_member_spec(info type,
                                data_member_options options);

4 Constant When:

  • (4.1) dealias(type) represents a type cv T where T is either an object type or a reference type;
  • (4.2) if options.name contains a value, then:
    • (4.2.1) holds_alternative<u8string>(options.name->contents) is true and get<u8string>(options.name->contents) contains a valid identifier when interpreted with UTF-8, or
    • (4.2.2) holds_alternative<string>(options.name->contents) is true and get<string>(options.name->contents) contains a valid identifier when interpreted with the ordinary literal encoding;
  • (4.3) otherwise, if options.name does not contain a value, then options.bit_width contains a value;
  • (4.4) if options.alignment contains a value, it is an alignment value ([basic.align]) not less than alignment_of(type); and
  • (4.5) if options.bit_width contains a value V, then
    • (4.5.1) is_integral_type(type) || is_enumeration_type(type) is true,
    • (4.5.2) options.alignment does not contain a value,
    • (4.5.3) options.no_unique_address is false, and
    • (4.5.4) if V == 0 then options.name does not contain a value.

5 Returns: A reflection of a data member description (T, N, A, W, NUA) (11.4.1 [class.mem.general]) where

  • (5.1) T is the type or type alias represented by type,
  • (5.2) N is either the identifier encoded by options.name or -1 if options.name is empty,
  • (5.3) A is either the alignment value held by options.alignment or -1 if options.alignment is empty,
  • (5.4) W is either the value held by options.bit_width or -1 if options.bit_width is empty, and
  • (5.5) NUA is the value held by options.no_unique_address.

6 Note 2: The returned reflection value is primarily useful in conjunction with define_aggregate. Certain other functions in std::meta (e.g., type_of, identifier_of) can also be used to query the characteristics indicated by the arguments provided to data_member_spec. — end note ]

consteval bool is_data_member_spec(info r);

7 Returns: true if r represents a data member description. Otherwise, false.

  template <reflection_range R = initializer_list<info>>
  consteval info define_aggregate(info class_type, R&& mdescrs);

8 Constant When: Letting C be the class represented by class_type and rK be the Kth reflection value in mdescrs,

  • (8.1) If C is a complete type from some point in the evaluation context, then
    • the reachable definition of C is an injected declaration produced by an evaluation of define_aggregate,
    • C has as many data members as mdescrs has elements, and
    • each Kth reflection value in mdescrs describes a data member with all of the same properties as the Kth data member of C.
  • (8.2) is_data_member_spec(rK) is true for every rK in mdescrs,
  • (8.3) the type represented by type_of(rK) is a complete type for every rK in mdescrs, and
  • (8.4) for every pair 0 ≤ K < L < mdescrs.size(), if has_identifier(rK) && has_identifier(rL), then either u8identifier_of(rK) != u8identifier_of(rL) or u8identifier_of(rK) == u8"_". Note 3: Every provided identifier that is not "_" must be unique. — end note ]

Note 4: C could be a class template specialization for which there is no reachable definition. — end note ]

9 Let {tk} be a sequence of reflections and {ok} be a sequence of data_member_options values such that

data_member_spec(tk, ok) == rk

for every rk in mdescrs.

10 Effects: Produces an injected declaration D ([expr.const]) that provides a definition for C with properties as follows:

  • (10.1) The target scope of D is the scope to which C belongs ([basic.scope.scope]).
  • (10.2) The locus of D follows immediately after the manifestly constant-evaluated expression currently under evaluation.
  • (10.3) If C is a specialization of a class template T, then D is is an explicit specialization of T.
  • (10.4) D contains a public non-static data member or unnamed bit-field corresponding to each reflection value rK in mdescrs. For every other rL in mdescrs such that K < L, the declaration of rK precedes the declaration of rL.
  • (10.5) A non-static data member or unnamed bit-field corresponding to each rK is declared with the type or type alias represented by tK.
  • (10.6) A non-static data member corresponding to a reflection rK for which oK.no_unique_address is true is declared with the attribute [[no_unique_address]].
  • (10.7) A non-static data member or unnamed bit-field corresponding to a reflection rK for which oK.bit_width contains a value is declared as a bit-field whose width is that value.
  • (10.8) A non-static data member corresponding to a reflection rK for which oK.alignment contains a value is declared with the alignment-specifier alignas(oK.alignment).
  • (10.9) A non-static data member or unnamed bit-field corresponding to a reflection rK is declared with a name determined as follows:
    • If oK.name does not contain a value, an unnamed bit-field is declared.
    • 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.
  • (10.10) If C is a union type for which any of its members are not trivially default constructible, then D has a user-provided default constructor which has no effect.
  • (10.11) If C is a union type for which any of its members are not trivially destructible, then D has a user-provided destructor which has no effect.

11 Returns: class_type.

[meta.reflection.unary] Unary type traits

1 Subclause [meta.reflection.unary] contains consteval functions that may be used to query the properties of a type at compile time.

2 For each function taking an argument of type meta::info whose name contains type, a call to the function is a non-constant library call (3.34 [defns.nonconst.libcall]) if that argument is not a reflection of a type or type alias. For each function taking an argument named type_args, a call to the function is a non-constant library call if any meta::info in that range is not a reflection of a type or a type alias.

[meta.reflection.unary.cat] Primary type categories

1 For any type or type alias T, for each function std::meta::TRAIT_type defined in this clause, std::meta::TRAIT_type(^^T) equals the value of the corresponding unary type trait std::TRAIT_v<T> as specified in 21.3.5.2 [meta.unary.cat].

consteval bool is_void_type(info type);
consteval bool is_null_pointer_type(info type);
consteval bool is_integral_type(info type);
consteval bool is_floating_point_type(info type);
consteval bool is_array_type(info type);
consteval bool is_pointer_type(info type);
consteval bool is_lvalue_reference_type(info type);
consteval bool is_rvalue_reference_type(info type);
consteval bool is_member_object_pointer_type(info type);
consteval bool is_member_function_pointer_type(info type);
consteval bool is_enum_type(info type);
consteval bool is_union_type(info type);
consteval bool is_class_type(info type);
consteval bool is_function_type(info type);
consteval bool is_reflection_type(info type);

2

Example 1:
namespace std::meta {
  consteval bool is_void_type(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;
  }
}
— end example ]

[meta.reflection.unary.comp] Composite type categories

1 For any type or type alias T, for each function std::meta::TRAIT_type defined in this clause, std::meta::TRAIT_type(^^T) equals the value of the corresponding unary type trait std::TRAIT_v<T> as specified in 21.3.5.3 [meta.unary.comp].

consteval bool is_reference_type(info type);
consteval bool is_arithmetic_type(info type);
consteval bool is_fundamental_type(info type);
consteval bool is_object_type(info type);
consteval bool is_scalar_type(info type);
consteval bool is_compound_type(info type);
consteval bool is_member_pointer_type(info type);

[meta.reflection.unary.prop] Type properties

1 For any type or type alias T, for each function std::meta::UNARY-TRAIT_type or std::meta::UNARY-TRAIT defined in this clause with signature bool(std::meta::info), std::meta::UNARY-TRAIT_type(^^T) or std::meta::UNARY-TRAIT(^^T) equals the value of the corresponding type property std::UNARY-TRAIT_v<T> as specified in 21.3.5.4 [meta.unary.prop].

2 For any types or type aliases T and U, for each function std::meta::BINARY-TRAIT_type or std::meta::BINARY-TYPE defined in this clause with signature bool(std::meta::info, std::meta::info), std::meta::BINARY-TRAIT_type(^^T, ^^U) or std::meta::BINARY-TRAIT(^^T, ^^U) equals the value of the corresponding type property std::BINARY-TRAIT_v<T, U> as specified in 21.3.5.4 [meta.unary.prop].

3 For any type or type alias T, pack of types or type aliases U..., and range r such that ranges::to<vector>(r) == vector{^^U...} is true, for each function template std::meta::VARIADIC-TRAIT_type defined in this clause, std::meta::VARIADIC-TRAIT_type(^^T, r) equals the value of the corresponding type property std::VARIADIC-TRAIT_v<T, U...> as specified in 21.3.5.4 [meta.unary.prop].

consteval bool is_const_type(info type);
consteval bool is_volatile_type(info type);
consteval bool is_trivially_copyable_type(info type);
consteval bool is_standard_layout_type(info type);
consteval bool is_empty_type(info type);
consteval bool is_polymorphic_type(info type);
consteval bool is_abstract_type(info type);
consteval bool is_final_type(info type);
consteval bool is_aggregate_type(info type);
consteval bool is_signed_type(info type);
consteval bool is_unsigned_type(info type);
consteval bool is_bounded_array_type(info type);
consteval bool is_unbounded_array_type(info type);
consteval bool is_scoped_enum_type(info type);

template <reflection_range R = initializer_list<info>>
consteval bool is_constructible_type(info type, R&& type_args);
consteval bool is_default_constructible_type(info type);
consteval bool is_copy_constructible_type(info type);
consteval bool is_move_constructible_type(info type);

consteval bool is_assignable_type(info type_dst, info type_src);
consteval bool is_copy_assignable_type(info type);
consteval bool is_move_assignable_type(info type);

consteval bool is_swappable_with_type(info type_dst, info type_src);
consteval bool is_swappable_type(info type);

consteval bool is_destructible_type(info type);

template <reflection_range R = initializer_list<info>>
consteval bool is_trivially_constructible_type(info type, R&& type_args);
consteval bool is_trivially_default_constructible_type(info type);
consteval bool is_trivially_copy_constructible_type(info type);
consteval bool is_trivially_move_constructible_type(info type);

consteval bool is_trivially_assignable_type(info type_dst, info type_src);
consteval bool is_trivially_copy_assignable_type(info type);
consteval bool is_trivially_move_assignable_type(info type);
consteval bool is_trivially_destructible_type(info type);

template <reflection_range R = initializer_list<info>>
consteval bool is_nothrow_constructible_type(info type, R&& type_args);
consteval bool is_nothrow_default_constructible_type(info type);
consteval bool is_nothrow_copy_constructible_type(info type);
consteval bool is_nothrow_move_constructible_type(info type);

consteval bool is_nothrow_assignable_type(info type_dst, info type_src);
consteval bool is_nothrow_copy_assignable_type(info type);
consteval bool is_nothrow_move_assignable_type(info type);

consteval bool is_nothrow_swappable_with_type(info type_dst, info type_src);
consteval bool is_nothrow_swappable_type(info type);

consteval bool is_nothrow_destructible_type(info type);

consteval bool is_implicit_lifetime_type(info type);

consteval bool has_virtual_destructor(info type);

consteval bool has_unique_object_representations(info type);

consteval bool reference_constructs_from_temporary(info type_dst, info type_src);
consteval bool reference_converts_from_temporary(info type_dst, info type_src);

[meta.reflection.unary.prop.query] Type property queries

consteval size_t rank(info type);

1 Effects: Equivalent to return std::rank_v<T>, where T is the type represented by type.

consteval size_t extent(info type, unsigned i = 0);

2 Effects: Equivalent to return std::extent_v<T, i>, where T is the type represented by type.

[meta.reflection.rel], Type relations

1 The consteval functions specified in this clause may be used to query relationships between types at compile time.

2 For any types or type aliases T and U, for each function std::meta::REL_type defined in this clause with signature bool(std::meta::info, std::meta::info), std::meta::REL_type(^^T, ^^U) equals the value of the corresponding type relation std::REL_v<T, U> as specified in 21.3.7 [meta.rel].

3 For any type or type alias T, pack of types or type aliases U..., and range r such that ranges::to<vector>(r) == vector{^^U...} is true, for each binary function template std::meta::VARIADIC-REL_type, std::meta::VARIADIC-REL_type(^^T, r) equals the value of the corresponding type relation std::VARIADIC-REL_v<T, U...> as specified in 21.3.7 [meta.rel].

4 For any types or type aliases T and R, pack of types or type aliases U..., and range r such that ranges::to<vector>(r) == vector{^^U...} is true, for each ternary function template std::meta::VARIADIC-REL-R_type defined in this clause, std::meta::VARIADIC-REL-R(^^R, ^^T, r)_type equals the value of the corresponding type relation std::VARIADIC-REL-R_v<R, T, U...> as specified in 21.3.7 [meta.rel].

consteval bool is_same_type(info type1, info type2);
consteval bool is_base_of_type(info type_base, info type_derived);
consteval bool is_virtual_base_of_type(info type_base, info type_derived);
consteval bool is_convertible_type(info type_src, info type_dst);
consteval bool is_nothrow_convertible_type(info type_src, info type_dst);
consteval bool is_layout_compatible_type(info type1, info type2);
consteval bool is_pointer_interconvertible_base_of_type(info type_base, info type_derived);

template <reflection_range R = initializer_list<info>>
consteval bool is_invocable_type(info type, R&& type_args);
template <reflection_range R = initializer_list<info>>
consteval bool is_invocable_r_type(info type_result, info type, R&& type_args);

template <reflection_range R = initializer_list<info>>
consteval bool is_nothrow_invocable_type(info type, R&& type_args);
template <reflection_range R = initializer_list<info>>
consteval bool is_nothrow_invocable_r_type(info type_result, info type, R&& type_args);

5 Note 1: If t is a reflection of the type int and u is a reflection of an alias to the type int, then t == u is false but is_same_type(t, u) is true. t == dealias(u) is also true. — end note ].

[meta.reflection.trans], Transformations between types

1 Subclause [meta.reflection.trans] contains consteval functions that may be used to transform one type to another following some predefined rule.

[meta.reflection.trans.cv], Const-volatile modifications

1 For any type or type alias T, for each function std::meta::MOD defined in this clause, std::meta::MOD(^^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.2 [meta.trans.cv].

consteval info remove_const(info type);
consteval info remove_volatile(info type);
consteval info remove_cv(info type);
consteval info add_const(info type);
consteval info add_volatile(info type);
consteval info add_cv(info type);

[meta.reflection.trans.ref], Reference modifications

1 For any type or type alias T, for each function std::meta::MOD defined in this clause, std::meta::MOD(^^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.3 [meta.trans.ref].

consteval info remove_reference(info type);
consteval info add_lvalue_reference(info type);
consteval info add_rvalue_reference(info type);

[meta.reflection.trans.sign], Sign modifications

1 For any type or type alias T, for each function std::meta::MOD defined in this clause, std::meta::MOD(^^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.4 [meta.trans.sign].

consteval info make_signed(info type);
consteval info make_unsigned(info type);

[meta.reflection.trans.arr], Array modifications

1 For any type or type alias T, for each function std::meta::MOD defined in this clause, std::meta::MOD(^^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.5 [meta.trans.arr].

consteval info remove_extent(info type);
consteval info remove_all_extents(info type);

[meta.reflection.trans.ptr], Pointer modifications

1 For any type or type alias T, for each function std::meta::MOD defined in this clause, std::meta::MOD(^^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.6 [meta.trans.ptr].

consteval info remove_pointer(info type);
consteval info add_pointer(info type);

[meta.reflection.trans.other], Other transformations

[ Editor's note: There are four transformations that are deliberately omitted here. type_identity and enable_if are not useful, conditional(cond, t, f) would just be a long way of writing cond ? t : f, and basic_common_reference is a class template intended to be specialized and not directly invoked. ]

1 For any type or type alias T, for each function std::meta::type_MOD defined in this clause with signature std::meta::info(std::meta::info), std::meta::type_MOD(^^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.7 [meta.trans.other].

2 For any pack of types or type aliases T... and range r such that ranges::to<vector>(r) == vector{^^T...} is true, for each unary function template std::meta::VARIADIC-MOD defined in this clause, std::meta::VARIADIC-MOD(r) returns the reflection of the corresponding type std::VARIADIC-MOD_t<T...> as specified in 21.3.8.7 [meta.trans.other].

3 For any type or type alias T, pack of types or type aliases U..., and range r such that ranges::to<vector>(r) == vector{^^U...} is true, std::meta::invoke_result(^^T, r) returns the reflection of the corresponding type std::invoke_result_t<T, U...> (21.3.8.7 [meta.trans.other]).

consteval info remove_cvref(info type);
consteval info decay(info type);
template <reflection_range R = initializer_list<info>>
consteval info common_type(R&& type_args);
template <reflection_range R = initializer_list<info>>
consteval info common_reference(R&& type_args);
consteval info underlying_type(info type);
template <reflection_range R = initializer_list<info>>
consteval info invoke_result(info type, R&& type_args);
consteval info unwrap_reference(info type);
consteval info unwrap_ref_decay(info type);

4

Example 1:
// example implementation
consteval info unwrap_reference(info type) {
  type = dealias(type);
  if (has_template_arguments(type) && template_of(type) == ^^reference_wrapper) {
    return add_lvalue_reference(template_arguments_of(type)[0]);
  } else {
    return type;
  }
}
— end example ]

[meta.reflection.tuple.variant], Tuple and Variant Queries

1 For any type or type alias T, for each function std::meta::UNARY-TRAIT defined in this clause with the signature size_t(std::meta::info), std::meta::UNARY-TRAIT(^^T) equals the value of the corresponding property std::UNARY-TRAIT_v<T> as defined in 22.4 [tuple] or 22.6 [variant].

2 For any type or type alias T and value I, for each function std::meta::BINARY-TRAIT defined in this clause with the signature info(size_t, std::meta::info), std::meta::BINARY-TRAIT(I, ^^T) returns a reflection representing the type std::BINARY-TRAIT_t<I, T> as defined in 22.4 [tuple] or 22.6 [variant].

consteval size_t tuple_size(info type);
consteval info tuple_element(size_t index, info type);

consteval size_t variant_size(info type);
consteval info variant_alternative(size_t index, info type);

22.15.3 [bit.cast] Function template 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 of To and From are types T such that:

  • (3.1) is_union_v<T> is false;
  • (3.2) is_pointer_v<T> is false;
  • (3.3) is_member_pointer_v<T> is false;
  • (3.π) is_reflection_v<T> is false;
  • (3.4) is_volatile_v<T> is false; and
  • (3.5) T has no non-static data members of reference type.

5.3 Feature-Test Macro

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>

6 References

[N3980] H. Hinnant, V. Falco, J. Byteway. 2014-05-24. Types don’t know #.
https://wg21.link/n3980
[P0784R7] Daveed Vandevoorde, Peter Dimov,Louis Dionne, Nina Ranns, Richard Smith, Daveed Vandevoorde. 2019-07-22. More constexpr containers.
https://wg21.link/p0784r7
[P1061R5] Barry Revzin, Jonathan Wakely. 2023-05-18. Structured Bindings can introduce a Pack.
https://wg21.link/p1061r5
[P1240R0] Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2018-10-08. Scalable Reflection in C++.
https://wg21.link/p1240r0
[P1240R2] Daveed Vandevoorde, Wyatt Childers, Andrew Sutton, Faisal Vali. 2022-01-14. Scalable Reflection.
https://wg21.link/p1240r2
[P1306R2] Dan Katz, Andrew Sutton, Sam Goodrick, Daveed Vandevoorde. 2024-05-07. Expansion statements.
https://wg21.link/p1306r2
[P1887R1] Corentin Jabot. 2020-01-13. Reflection on attributes.
https://wg21.link/p1887r1
[P1974R0] Jeff Snyder, Louis Dionne, Daveed Vandevoorde. 2020-05-15. Non-transient constexpr allocation using propconst.
https://wg21.link/p1974r0
[P2237R0] Andrew Sutton. 2020-10-15. Metaprogramming.
https://wg21.link/p2237r0
[P2670R1] Barry Revzin. 2023-02-03. Non-transient constexpr allocation.
https://wg21.link/p2670r1
[P2758R1] Barry Revzin. 2023-12-09. Emitting messages at compile time.
https://wg21.link/p2758r1
[P2996R0] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2023-10-15. Reflection for C++26.
https://wg21.link/p2996r0
[P2996R1] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2023-12-18. Reflection for C++26.
https://wg21.link/p2996r1
[P2996R2] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-02-15. Reflection for C++26.
https://wg21.link/p2996r2
[P2996R3] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-05-22. Reflection for C++26.
https://wg21.link/p2996r3
[P2996R4] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-06-26. Reflection for C++26.
https://wg21.link/p2996r4
[P2996R5] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-08-14. Reflection for C++26.
https://wg21.link/p2996r5
[P2996R6] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-10-10. Reflection for C++26.
https://wg21.link/p2996r6
[P2996R7] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-10-13. Reflection for C++26.
https://wg21.link/p2996r7
[P3068R1] Hana Dusíková. 2024-03-30. Allowing exception throwing in constant-evaluation.
https://wg21.link/p3068r1
[P3096R2] Adam Lach, Walter Genovese. 2024-07-16. Function Parameter Reflection in Reflection for C++26.
https://wg21.link/p3096r2
[P3293R1] Barry Revzin, Peter Dimov, Dan Katz, Daveed Vandevoorde. 2024-10-13. Splicing a base class subobject.
https://wg21.link/p3293r1
[P3294R2] Barry Revzin, Andrei Alexandrescu, Daveed Vandevoorde. 2024-10-15. Code Injection with Token Sequences.
https://wg21.link/p3294r2
[P3295R0] Ben Craig. 2024-05-21. Freestanding constexpr containers and constexpr exception types.
https://wg21.link/p3295r0
[P3381R0] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-09-17. Syntax for Reflection.
https://wg21.link/p3381r0