Reflection for C++26

Document #: P2996R3
Date: 2024-05-22
Project: Programming Language C++
Audience: EWG, LEWG
Reply-to: Wyatt Childers
<>
Peter Dimov
<>
Dan Katz
<>
Barry Revzin
<>
Andrew Sutton
<>
Faisal Vali
<>
Daveed Vandevoorde
<>

Contents

1 Revision History

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_type 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_result(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::name_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::name_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 denotes a member function — overload resolution.

This proposal includes a number of consteval “metafunctions” that enable the introspection of various language constructs. Among those metafunctions is std::meta::nonstatic_data_members_of which returns a vector of reflection values that describe the nonstatic 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::name_of, which returns a std::string_view describing the unqualified name of an entity denoted by a given reflection value. With such a facility, we could conceivably access nonstatic 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 (name_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_result(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]), .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::name_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::name_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::name_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) == name_of(dm);
      });

    if (it == args.end()) {
      // no option provided, use default
      continue;
    } else if (it + 1 == args.end()) {
      std::print(stderr, "Option {} is missing a value\n", *it);
      std::exit(EXIT_FAILURE);
    }

    using T = typename[:type_of(dm):];
    auto iss = std::ispanstream(it[1]);
    if (iss >> opts.[:dm:]; !iss) {
      std::print(stderr, "Failed to parse option {} into a {}\n", *it, display_name_of(^T));
      std::exit(EXIT_FAILURE);
    }
  }
  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_class(^storage, {data_member_spec(^Ts)...})));
  storage data;

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

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

template<std::size_t I, typename... Ts>
  struct std::tuple_element<I, Tuple<Ts...>> {
    static constexpr std::array types = {^Ts...};
    using type = [: types[I] :];
  };

consteval std::meta::info get_nth_field(std::meta::info r, std::size_t n) {
  return nonstatic_data_members_of(r)[n];
}

template<std::size_t I, typename... Ts>
  constexpr auto get(Tuple<Ts...> &t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>>& {
    return t.data.[:get_nth_field(^decltype(t.data), I):];
  }
// Similarly for other value categories...

This example uses a “magic” std::meta::define_class template along with member reflection through the nonstatic_data_members_of metafunction to implement a std::tuple-like type without the usual complex and costly template metaprogramming tricks that that involves when these facilities are not available. define_class takes a reflection for an incomplete class or union plus a vector of nonstatic 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_class to create on the fly a type with one member for each Ts..., we can implement a variant that simply defines a union instead of a struct. One difference here is how the destructor of a union is currently defined:

union U1 {
  int i;
  char c;
};

union U2 {
  int i;
  std::string s;
};

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

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_class for a union have this behavior, then we can implement a variant in a much more straightforward way than in current implementations. This is not a complete implementation of std::variant (and cheats using libstdc++ internals, and also uses Boost.Mp11’s mp_with_index) but should demonstrate the idea:

template <typename... Ts>
class Variant {
    union Storage;
    struct Empty { };

    static_assert(is_type(define_class(^Storage, {
        data_member_spec(^Empty, {.name="empty"}),
        data_member_spec(^Ts)...
    })));

    static constexpr std::array<std::meta::info, sizeof...(Ts)> types = {^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<[: types[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 [P3293R0].

On Compiler Explorer: EDG, Clang.

3.10 Struct to Struct of Arrays

#include <meta>
#include <array>

template <typename T, std::size_t N>
struct struct_of_arrays_impl;

consteval auto make_struct_of_arrays(std::meta::info type,
                                     std::meta::info N) -> std::meta::info {
  std::vector<std::meta::info> old_members = nonstatic_data_members_of(type);
  std::vector<std::meta::info> new_members = {};
  for (std::meta::info member : old_members) {
    auto type_array = substitute(^std::array, {type_of(member), N });
    auto mem_descr = data_member_spec(type_array, {.name = name_of(member)});
    new_members.push_back(mem_descr);
  }
  return std::meta::define_class(
    substitute(^struct_of_arrays_impl, {type, N}),
    new_members);
}

template <typename T, size_t N>
using struct_of_arrays = [: make_struct_of_arrays(^T, ^N) :];

Example:

struct point {
  float x;
  float y;
  float z;
};

using points = struct_of_arrays<point, 30>;
// equivalent to:
// struct points {
//   std::array<float, 30> x;
//   std::array<float, 30> y;
//   std::array<float, 30> z;
// };

Again, the combination of nonstatic_data_members_of and define_class is put to good use.

On Compiler Explorer: EDG, Clang.

3.11 Parsing Command-Line Options II

Now that we’ve seen a couple examples of using std::meta::define_class to create a type, we can create a more sophisticated command-line parser example.

This is the opening example for clap (Rust’s Command Line Argument Parser):

struct Args : Clap {
  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=name_of(member)}));
  }
  return define_class(opts, new_members);
}

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

    // 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] == name_of(sm)[0])
              || (cur.use_long && arg.starts_with("--") && arg.substr(2) == name_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", name_of(sm));
          std::exit(EXIT_FAILURE);
        }
      } else if (it + 1 == cmdline.end()) {
        std::print(stderr, "Option {} for {} is missing a value\n", *it, name_of(sm));
        std::exit(EXIT_FAILURE);
      }

      // 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], name_of(sm), display_name_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(), "{}{{", name_of(^T));

    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();
      out = std::format_to(out, ".{}={}", name_of(mem), 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::type_remove_cvref)
                    | std::ranges::to<std::vector>());
}

template <typename To, typename From, std::meta::info ... members>
constexpr auto struct_to_tuple_helper(From const& from) -> To {
  return To(from.[:members:]...);
}

template<typename From>
consteval auto get_struct_to_tuple_helper() {
  using To = [: type_struct_to_tuple(^From): ];

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

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

  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({type_tuple_size(type_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_result("x"),
                 ^double, std::meta::reflect_result("y")>()

We do not currently support splicing string literals, and the pair approach follows the similar pattern already shown with define_class (given a suitable fixed_string type):

template <class T, fixed_string Name>
struct pair {
    static constexpr auto name() -> std::string_view { return Name.view(); }
    using type = T;
};

template <class... Tags>
consteval auto make_named_tuple(std::meta::info type, Tags... tags) {
    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_class(type, nsdms);
}

struct R;
static_assert(is_type(make_named_tuple(^R, pair<int, "x">{}, pair<double, "y">{})));

static_assert(type_of(nonstatic_data_members_of(^R)[0]) == ^int);
static_assert(type_of(nonstatic_data_members_of(^R)[1]) == ^double);

int main() {
    [[maybe_unused]] auto r = R{.x=1, .y=2.0};
}

On Compiler Explorer: EDG, Clang.

Alternatively, can side-step the question of non-type template parameters entirely by keeping everything in the value domain:

consteval auto make_named_tuple(std::meta::info type,
                                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_class(type, nsdms);
}

struct R;
static_assert(is_type(make_named_tuple(^R, {{^int, "x"}, {^double, "y"}})));

static_assert(type_of(nonstatic_data_members_of(^R)[0]) == ^int);
static_assert(type_of(nonstatic_data_members_of(^R)[1]) == ^double);

int main() {
    [[maybe_unused]] auto r = R{.x=1, .y=2.0};
}

On Compiler Explorer: EDG and Clang (the EDG and Clang implementations differ only in Clang having the updated data_member_spec API that returns an info).

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

    // Define 'Helper<k>' and return its index.
    define_class(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.

3.18 Emulating typeful reflection

Although we believe a single opaque std::meta::info type to be the best and most scalable foundation for reflection, we acknowledge the desire expressed by SG7 for future support for “typeful reflection”. The following demonstrates one possible means of assembling a typeful reflection library, in which different classes of reflections are represented by distinct types, on top of the facilities proposed here.

// Represents a 'std::meta::info' constrained by a predicate.
template <std::meta::info Pred>
  requires (type_of(std::meta::reflect_result([:Pred:](^int))) == ^bool)
struct metatype {
  std::meta::info value;

  // Construction is ill-formed unless predicate is satisfied.
  consteval metatype(std::meta::info r) : value(r) {
    if (![:Pred:](r))
      throw "Reflection is not a member of this metatype";
  }

  // Cast to 'std::meta::info' allows values of this type to be spliced.
  consteval operator std::meta::info() const { return value; }

  static consteval bool check(std::meta::info r) { return [:Pred:](r); }
};

// Type representing a "failure to match" any known metatypes.
struct unmatched {
  consteval unmatched(std::meta::info) {}
  static consteval bool check(std::meta::info) { return true; }
};

// Returns the given reflection "enriched" with a more descriptive type.
template <typename... Choices>
consteval std::meta::info enrich(std::meta::info r) {
  // Because we control the type, we know that the constructor taking info is
  // the first constructor. The copy/move constructors are added at the }, so
  // will be the last ones in the list.
  std::array ctors = {members_of(^Choices, std::meta::is_constructor)[0]...,
                      members_of(^unmatched, std::meta::is_constructor)[0]};
  std::array checks = {^Choices::check..., ^unmatched::check};

  std::meta::info choice;
  for (auto [check, ctor] : std::views::zip(checks, ctors))
    if (extract<bool>(reflect_invoke(check, {reflect_result(r)})))
      return reflect_invoke(ctor, {reflect_result(r)});

  std::unreachable();
}

We can leverage this machinery to select different function overloads based on the “type” of reflection provided as an argument.

using type_t = metatype<^std::meta::type_is>;
using template_t = metatype<^std::meta::is_template>;

// Example of a function overloaded for different "types" of reflections.
void PrintKind(type_t) { std::println("type"); }
void PrintKind(template_t) { std::println("template"); }
void PrintKind(unmatched) { std::println("unknown kind"); }

int main() {
  // Classifies any reflection as one of: Type, Function, or Unmatched.
  auto enrich = [](std::meta::info r) { return ::enrich<type_t,
                                                        template_t>(r); };

  // Demonstration of using 'enrich' to select an overload.
  PrintKind([:enrich(^metatype):]);                    // "template"
  PrintKind([:enrich(^type_t):]);                      // "type"
  PrintKind([:enrich(std::meta::reflect_result(3):]);  // "unknown kind"
}

Note that the metatype class can be generalized to wrap values of any literal type, or to wrap multiple values of possibly different types. This has been used, for instance, to select compile-time overloads based on: whether two integers share the same parity, the presence or absence of a value in an optional, the type of the value held by a variant or an any, or the syntactic form of a compile-time string.

Achieving the same in C++23, with the same generality, would require spelling the argument(s) twice: first to obtain a “classification tag” to use as a template argument, and again to call the function, i.e.,

Printer::PrintKind<classify(^int)>(^int).
// or worse...
fn<classify(Arg1, Arg2, Arg3)>(Arg1, Arg2, Arg3).

On Compiler Explorer: 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). One way to obtain such reflections is using the std::meta::reflect_result metafunction, which returns a reflection of the result of once evaluating its argument. The std::meta::value_of metafunction can also be used to obtain a reflection of the value stored by an entity (if the entity is usable in constant expressions). While it’s possible to support direct reflection of expression results (e.g., ^fn()), we aren’t convinced that this syntax provided enough value to justify its introduction at this time.

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.

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

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 reflects a class template, then it is a non-type template parameter of deduced type, but if R reflects a concept, it is a type template parameter. There is no other circumstance in the language for which it is not possible to decide at parse time whether a template parameter is a type or a non-type, and we don’t wish to introduce one for this use case.

The most obvious solution would be to introduce a concept [:R:] syntax that requires that R reflect a concept, and while this could be added going forward, we weren’t convinced of its value at this time - especially since the above can easily be rewritten:

template <std::meta::info R> struct Outer {
  template <typename T> requires template [:R:]<T> { /* ... */ };
};

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 reflects 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]);

For any other expression expr, the value ^expr is a reflection of the result of the expression. The expression is ill-formed if expr is a parenthesized expression.

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_result<const int &>(i))  // A variable is indistinguishable
                                                                // from the object it designates.
static_assert(^i != ^42);  // A reflection of an entity is not the same as one of its value.

4.3.2 Templates specialized by reflections

Nontype template arguments of type std::meta::info are permitted (and frequently useful!), but a specialized template whose argument reflects an entity local to a translation unit must itself necessarily have internal linkage. For example:

template<auto R> struct S {};

extern int x;
static int y;

S<^x> sx;  // S<^x> has external name linkage.
S<^y> sy;  // S<^y> has internal name linkage.

4.3.3 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::name_of(^S);  // Okay.
std::string name1 = name_of(^S);             // Also okay.

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

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

4.4 Metafunctions

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

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_class metafunction that provides a definition for a given class. Clearly, we want the effect of calling that metafunction to be “prompt” in a lexical-order sense. For example:

#include <meta>
struct S;

void g() {
  static_assert(is_type(define_class(^S, {})));
  S s;  // S should be defined at this point.
}

Hence this proposal also introduces constraints on constant evaluation as follows…

First, we identify a subset of manifestly constant-evaluated expressions and conversions characterized by the fact that their evaluation must occur and must succeed in a valid C++ program: We call these plainly constant-evaluated. We require that a programmer can count on those evaluations occurring exactly once and completing at translation time.

Second, we sequence plainly constant-evaluated expressions and conversions within the lexical order. Specifically, we require that the evaluation of a plainly constant-evaluated expression or conversion occurs before the implementation checks the validity of source constructs lexically following that expression or conversion.

Those constraints are mostly intuitive, but they are a significant change to the underlying principles of the current standard in this respect.

[P2758R1] (“Emitting messages at compile time”) also has to deal with side effects during constant evaluation. However, those effects (“output”) are of a slightly different nature in the sense that they can be buffered until a manifestly constant-evaluated expression/conversion has completed. “Buffering” a class type completion is not practical (e.g., because other metafunctions may well depend on the completed class type). Still, we are not aware of incompatibilities between our proposal and [P2758R1].

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 reflected template for us to 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. 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 = span<info const>>
    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::type_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::type_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 = span<info const>>
consteval auto substitute(info tmpl, R&& args) -> info {
    if constexpr (ranges::sized_range<R> && ranges::contiguous_range<R>) {
        auto as_span = span<info const>(args);
        return __builtin_substitute(tmpl, as_span.data(), as_span.size());
    } 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:

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 string literal encoding” (which may or may not be UTF-8) are often used. We therefore need mechanisms to produce the corresponding ordinary string types as well.

Orthogonal to the character representation is the data structure used to traffic in source text. An implementation can easily have at least three potential representations of reflected source text:

  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 name_of were a (consteval) metafunction returning a std::string value, the following simple example would not work:

#include <iostream>
#include <meta>
int main() {
  int hello_world = 42;
  std::cout << name_of(^hello_world) << "\n";  // Doesn't work if name_of produces a std::string.
}

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

For now, however, we propose that queries like name_of do produce “string view” results. For example:

template<typename T = std::u8string_view>
  requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
consteval T name_of(info);

(At the time of this writing, the implementations implement name_of as an ordinary function returning an ordinary std::string_view.)

We could potentially extend that API to also allow string value types:

template<typename T = std::u8string_view>
  requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view) ||
            ^T == dealias(^std::string) || ^T == dealias(^std::u8string))
consteval T name_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 name_of<std::string_view>(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)).

We propose #3 to strike a balance between usability and portability, specifically with the universal-character-names of the form \u{ hex-number } as the alternative character presentation.

We also propose that APIs that consume source text (currently, that is only done via std::meta::data_member_options_t) also accept such alternative presentations.

4.4.6 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.7 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
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval auto name_of(info r) -> T;
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval auto qualified_name_of(info r) -> T;
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval auto display_name_of(info r) -> T;
  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;

  // value queries
  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
  template<typename ...Fs>
    consteval auto members_of(info type_class, Fs ...filters) -> vector<info>;
  template<typename ...Fs>
    consteval auto bases_of(info type_class, Fs ...filters) -> vector<info>;
  consteval auto static_data_members_of(info type_class) -> vector<info>;
  consteval auto nonstatic_data_members_of(info type_class) -> vector<info>;
  consteval auto subobjects_of(info type_class) -> vector<info>;
  consteval auto enumerators_of(info type_enum) -> vector<info>;

  template<typename ...Fs>
    consteval auto accessible_members_of(info type_class, Fs ...filters) -> vector<info>;
  template<typename ...Fs>
    consteval auto accessible_bases_of(info type_class, Fs ...filters) -> vector<info>;
  consteval auto accessible_static_data_members_of(info type_class) -> vector<info>;
  consteval auto accessible_nonstatic_data_members_of(info type_class) -> vector<info>;
  consteval auto accessible_subobjects_of(info type_class) -> vector<info>;

  // substitute
  template <reflection_range R = span<info const>>
  consteval auto can_substitute(info templ, R&& args) -> bool;
  template <reflection_range R = span<info const>>
  consteval auto substitute(info templ, R&& args) -> info;

  // reflect_invoke
  template <reflection_range R = span<info const>>
  consteval auto reflect_invoke(info target, R&& args) -> info;
  template <reflection_range R1 = span<info const>, reflection_range R2 = span<info const>>
  consteval auto reflect_invoke(info target, R1&& tmpl_args, R2&& args) -> info;

  // reflect
  template<typename T>
    consteval auto reflect_result(T value) -> info;

  // extract
  template<typename T>
    consteval auto extract(info) -> T;

  // test_type
  consteval auto test_type(info templ, info type) -> bool;
  template <reflection_range R = span<info const>>
  consteval auto test_types(info templ, R&& types) -> bool;

  // 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_accessible(info r) -> bool;
  consteval auto is_virtual(info r) -> bool;
  consteval auto is_pure_virtual(info entity) -> bool;
  consteval auto is_override(info entity) -> bool;
  consteval auto is_deleted(info entity) -> bool;
  consteval auto is_defaulted(info entity) -> bool;
  consteval auto is_explicit(info entity) -> bool;
  consteval auto is_noexcept(info entity) -> bool;
  consteval auto is_bit_field(info entity) -> bool;
  consteval auto is_const(info r) -> bool;
  consteval auto is_volatile(info r) -> bool;
  consteval auto is_final(info r) -> bool;
  consteval auto has_static_storage_duration(info r) -> bool;
  consteval auto has_internal_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 entity) -> bool;
  consteval auto is_namespace_member(info entity) -> bool;
  consteval auto is_nonstatic_data_member(info entity) -> bool;
  consteval auto is_static_member(info entity) -> bool;
  consteval auto is_base(info entity) -> bool;
  consteval auto is_namespace(info entity) -> bool;
  consteval auto is_function(info entity) -> bool;
  consteval auto is_variable(info entity) -> bool;
  consteval auto is_type(info entity) -> bool;
  consteval auto is_alias(info entity) -> bool;
  consteval auto is_incomplete_type(info entity) -> bool;
  consteval auto is_template(info entity) -> bool;
  consteval auto is_function_template(info entity) -> bool;
  consteval auto is_variable_template(info entity) -> bool;
  consteval auto is_class_template(info entity) -> bool;
  consteval auto is_alias_template(info entity) -> bool;
  consteval auto is_concept(info entity) -> bool;
  consteval auto is_structured_binding(info entity) -> bool;
  consteval auto is_value(info entity) -> bool;
  consteval auto is_object(info entity) -> bool;
  consteval auto has_template_arguments(info r) -> bool;
  consteval auto is_constructor(info r) -> bool;
  consteval auto is_destructor(info r) -> bool;
  consteval auto is_special_member(info r) -> bool;
  consteval auto is_user_provided(info r) -> bool;

  // define_class
  struct data_member_options_t;
  consteval auto data_member_spec(info type_class,
                                  data_member_options_t options = {}) -> info;
  template <reflection_range R = span<info const>>
  consteval auto define_class(info type_class, R&&) -> info;

  // data layout
  consteval auto offset_of(info entity) -> size_t;
  consteval auto size_of(info entity) -> size_t;
  consteval auto alignment_of(info entity) -> size_t;
  consteval auto bit_offset_of(info entity) -> size_t;
  consteval auto bit_size_of(info entity) -> size_t;

}

4.4.8 name_of, display_name_of, source_location_of

namespace std::meta {
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval auto name_of(info) -> T;
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval auto qualified_name_of(info) -> T;
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval auto display_name_of(info) -> T;
  consteval auto source_location_of(info r) -> source_location;
}

If a string_view is returned, its content consist of characters of the basic source character set only; other source text is rendered as universal character names of the form \u{ simple-hexadecimal-digit-sequence }).

Given a reflection r that designates a declared entity X, name_of<S>(r) and qualified_name_of<S>(r) return a string view of type S holding the unqualified and qualified name of X, respectively. For all other reflections, an empty string view is produced. For template instances, the name does not include the template argument list.

Given a reflection r, display_name_of(r) returns an unspecified non-empty string view. Implementations are encouraged to produce text that is helpful in identifying the reflected construct.

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.9 type_of, parent_of, dealias

namespace std::meta {
  consteval auto type_of(info r) -> info;
  consteval auto parent_of(info r) -> info;
  consteval auto dealias(info r) -> info;
}

If r is a reflection designating a typed entity, type_of(r) is a reflection designating its type. If r is already a type, type_of(r) is not a constant expression. This can be used to implement the C typeof feature (which works on both types and expressions and strips qualifiers):

consteval auto type_doof(std::meta::info r) -> std::meta::info {
  return type_remove_cvref(is_type(r) ? r : type_of(r));
}

#define typeof(e) [: type_doof(^e) :]

If r designates a member of a class or namespace, parent_of(r) is a reflection designating its immediately enclosing class or (possibly inline or anonymous) namespace.

If r designates an alias, dealias(r) designates the underlying entity. Otherwise, dealias(r) produces r. dealias is recursive - it strips all aliases:

using X = int;
using Y = X;
static_assert(dealias(^int) == ^int);
static_assert(dealias(^X) == ^int);
static_assert(dealias(^Y) == ^int);

4.4.10 value_of

namespace std::meta {
  consteval auto value_of(info r) -> info;
}

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.11 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.12 members_of, static_data_members_of, nonstatic_data_members_of, bases_of, enumerators_of, subobjects_of

namespace std::meta {
  template<typename ...Fs>
    consteval auto members_of(info type_class, Fs ...filters) -> vector<info>;

  template<typename ...Fs>
    consteval auto bases_of(info type_class, Fs ...filters) -> vector<info>;

  consteval auto static_data_members_of(info type_class) -> vector<info> {
    return members_of(type_class, is_variable);
  }

  consteval auto nonstatic_data_members_of(info type_class) -> vector<info> {
    return members_of(type_class, is_nonstatic_data_member);
  }

  consteval auto subobjects_of(info type_class) -> vector<info> {
    auto subobjects = bases_of(type_class);
    subobjects.append_range(nonstatic_data_members_of(type_class));
    return subobjects;
  }

  consteval auto enumerators_of(info type_enum) -> vector<info>;

  template<typename ...Fs>
    consteval auto accessible_members_of(info type_class, Fs ...filters) -> vector<info>;
  template<typename ...Fs>
    consteval auto accessible_bases_of(info type_class, Fs ...filters) -> vector<info>;
  consteval auto accessible_static_data_members_of(info type_class) -> vector<info>;
  consteval auto accessible_nonstatic_data_members_of(info type_class) -> vector<info>;
  consteval auto accessible_subobjects_of(info type_class) -> vector<info>;
}

The template members_of returns a vector of reflections representing the direct members of the class type represented by its first argument. Any nonstatic data members appear in declaration order within that vector. Anonymous unions appear as a nonstatic data member of corresponding union type. Reflections of structured bindings shall not appear in the returned vector. If any Filters... argument is specified, a member is dropped from the result if any filter applied to that members reflection returns false. E.g., members_of(^C, std::meta::type_is) will only return types nested in the definition of C and members_of(^C, std::meta::type_is, std::meta::is_variable) will return an empty vector since a member cannot be both a type and a variable.

The template bases_of returns the direct base classes of the class type represented by its first argument, in declaration order.

enumerators_of returns the enumerator constants of the indicated enumeration type in declaration order.

subobjects_of returns the base class subobjects and the non-static data members of a type, in declaration order. Note that the term subobject also includes array elements, which we are excluding here. Such reflections would currently be of minimal use since you could not splice them with access (e.g. arr.[:elem:] is not supported), so would need some more thought first.

Each variant named accessible_meow_of simply returns the result of meow_of filtered on is_accessible. Note that this might change to be is_accessible_from(e, context) rather than simply is_accessible(e).

4.4.13 substitute

namespace std::meta {
  template <reflection_range R = span<info const>>
  consteval auto can_substitute(info templ, R&& args) -> bool;
  template <reflection_range R = span<info const>>
  consteval auto substitute(info templ, R&& args) -> info;
}

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.14 reflect_invoke

namespace std::meta {
  template <reflection_range R = span<info const>>
  consteval auto reflect_invoke(info target, R&& args) -> info;
  template <reflection_range R1 = span<info const>, reflection_range R2 = span<info const>>
  consteval auto reflect_invoke(info target, R1&& tmpl_args, R2&& args) -> info;
}

These metafunctions produces a reflection of the value returned by a call expression.

For the first overload: Letting F be the entity reflected by target, and A0, A1, ..., AN be the sequence of entities reflected by the values held by args: if the expression F(A0, A1, ..., AN) is a well-formed constant expression evaluating to a type that is not void, and if every value in args is a reflection of a value or object usable in constant expressions, then reflect_invoke(target, args) evaluates to a reflection of the result of F(A0, A1, ..., AN). For all other invocations, reflect_invoke(target, args) is not a constant expression.

The second overload behaves the same as the first overload, except instead of evaluating F(A0, A1, ..., AN), we require that F be a reflection of a template and evaluate F<T0, T1, ..., TM>(A0, A1, ..., AN). This allows evaluating reflect_invoke(^std::get, {reflect_value(0)}, {e}) to evaluate to, approximately, ^std::get<0>([: e :]).

A few possible extensions for reflect_invoke have been discussed among the authors. Given the advent of constant evaluations with side-effects, it may be worth allowing void-returning functions, but this would require some representation of “a returned value of type void”. Construction of runtime call expressions is another exciting possibility. Both extensions require more thought and implementation experience, and we are not proposing either at this time.

4.4.15 reflect_result<T>

namespace std::meta {
  template<typename T> consteval auto reflect_result(T expr) -> info;
}

If expr does not have structural type, then reflect_result(expr) fails to be a constant expression.

Otherwise, if T is of reference or pointer type, or for each subobject of expr having reference or pointer type if T is of class type, if the reference or pointer value designates an entity that is not a permitted result ([expr.const]), then reflect_result(expr) fails to be a constant expression.

Otherwise, reflect_result(expr) produces a reflection of the result of static_cast<T>(expr).

4.4.16 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 nonstatic 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 reflected by its operand.

4.4.17 test_type, test_types

namespace std::meta {
  consteval auto test_type(info templ, info type) -> bool {
    return test_types(templ, {type});
  }

  template <reflection_range R = span<info const>>
  consteval auto test_types(info templ, R&& types) -> bool {
    return extract<bool>(substitute(templ, (R&&)types));
  }
}

This utility translates existing metaprogramming predicates (expressed as constexpr variable templates or concept templates) to the reflection domain. For example:

struct S {};
static_assert(test_type(^std::is_class_v, ^S));

An implementation is permitted to recognize standard predicate templates and implement test_type without actually instantiating the predicate template. In fact, that is recommended practice.

4.4.18 data_member_spec, define_class

namespace std::meta {
  struct data_member_options_t {
    optional<string_view> name;
    bool is_static = false;
    optional<int> alignment;
    optional<int> width;
  };
  consteval auto data_member_spec(info type,
                                  data_member_options_t options = {}) -> info;
  template <reflection_range R = span<info const>>
  consteval auto define_class(info type_class, R&&) -> info;
}

data_member_spec returns a reflection of a description of a data member of given type. Optional alignment, bit-field-width, static-ness, and name can be provided as well. If no name is provided, the name of the data member is unspecified. If is_static is true, the data member is declared static.

define_class takes the reflection of an incomplete class/struct/union type and a range of reflections of data member descriptions and it completes the given class type with data members as described (in the given order). The given reflection is returned. For now, only data member reflections are supported (via data_member_spec) but the API takes in a range of info anticipating expanding this in the near future.

For example:

union U;
static_assert(is_type(define_class(^U, {
  data_member_spec(^int),
  data_member_spec(^char),
  data_member_spec(^double),
})));

// U is now defined to the equivalent of
// union U {
//   int _0;
//   char _1;
//   double _2;
// };

template<typename T> struct S;
constexpr auto U = define_class(^S<int>, {
  data_member_spec(^int, {.name="i", .align=64}),
  data_member_spec(^int, {.name="j", .align=64}),
});

// S<int> is now defined to the equivalent of
// template<> struct S<int> {
//   alignas(64) int i;
//   alignas(64) int j;
// };

When defining a union, if one of the alternatives has a non-trivial destructor, the defined union will still have a destructor provided - that simply does nothing. This allows implementing variant without having to further extend support in define_class for member functions.

If type_class is a reflection of a type that already has a definition, or which is in the process of being defined, the call to define_class is not a constant expression.

4.4.19 Data Layout Reflection

namespace std::meta {
  consteval auto offset_of(info entity) -> size_t;
  consteval auto size_of(info entity) -> size_t;
  consteval auto alignment_of(info entity) -> size_t;

  consteval auto bit_offset_of(info entity) -> size_t;
  consteval auto bit_size_of(info entity) -> 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(bit_offset_of(^Msg::a) == 0);
static_assert(bit_offset_of(^Msg::b) == 2);
static_assert(bit_offset_of(^Msg::c) == 2);
static_assert(bit_offset_of(^Msg::d) == 3);

static_assert(bit_size_of(^Msg::a) == 10);
static_assert(bit_size_of(^Msg::b) == 8);
static_assert(bit_size_of(^Msg::c) == 25);
static_assert(bit_size_of(^Msg::d) == 21);

consteval auto total_bit_offset_of(std::meta::info m) -> size_t {
    return offset_of(m) * 8 + bit_offset_of(m);
}

static_assert(total_bit_offset_of(^Msg::a) == 0);
static_assert(total_bit_offset_of(^Msg::b) == 10);
static_assert(total_bit_offset_of(^Msg::c) == 18);
static_assert(total_bit_offset_of(^Msg::d) == 43);

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

Direct
Indirect
std::meta::type_remove_cvref(type)
std::meta::substitute(^std::remove_cvref_t, {type})
std::meta::type_is_const(type)
std::meta::extract<bool>(std::meta::substitute(^std::is_const_v, {type}))
std::meta::test_type(^std::is_const_v, type)

Having std::meta::meow for every trait std::meow is more straightforward and will likely be faster to compile, though means we will have a much larger library API. There are quite a few traits in 21 [meta] - but it should be easy enough to specify all of them. So we’re doing it.

Now, one thing that came up is that the straightforward thing we want to do is to simply add a std::meta::meow for every trait std::meow and word it appropriately. That’s what the current wording in this revision does. However, we’ve run into a conflict. The standard library type traits are all type traits - they only accept types. As such, their names are simply things like std::is_pointer, std::is_const, std::is_lvalue_reference, and so forth. Renaming it to std::type_is_pointer, for instance, would be a waste of characters since there’s nothing else the argument could be save for a type. But this is no longer the case. Consider std::meta::is_function(e), which is currently actually specified twice in our wording having two different meanings:

  1. A consteval function equivalent of the type trait std::is_function<T>, such that std::meta::is_function(e) mandates that e reflect a type and checks if that type is a function type. This is the same category of type trait as the ones mentioned above.
  2. A new kind of reflection query std::meta::is_function(e) which asks if e is the reflection of a function (as opposed to a type or a namespace or a template, etc.). This is the same category of query as std::meta::is_template or std::meta::is_concept or std::meta::is_namespace.

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

Probably the most straightforward choice would be to either prefix or suffix all of the type traits with _type. We think prefix is a little bit better because it groups all the type traits together and perhaps make it clearer that the argument(s) must be types. That is: std::is_pointer<T> because std::meta::type_is_pointer(^T), std::is_arithmetic<T> becomes std::meta::type_is_arithmetic(^T), and so forth. The advantage of this approach is that it very likely just works, also opening the door to making a more general std::meta::is_const(e) that checks not just if e is a const-qualified type but also if it’s a const-qualified object or a const-qualified member, etc. The disadvantage is that the suffixed names would not be familiar - we’re much more familiar with the name is_copy_constructible than we would be with type_is_copy_constructible.

That said, it’s not too much added mental overhead to remember type_is_copy_constructible and this avoids have to remember which type traits have the suffix and which don’t. Not to mention that many of the type traits read as if they would accept objects just fine (e.g. is_trivially_copyable). So we propose that simply all the type traits be suffixed with *_type.

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_incomplete_type(^T), if one translation unit #includes a forward declaration of T, another #includes a complete definition of T, and they both afterwards #include "cls.h", the result will be an ODR violation.

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

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

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

5 Proposed Wording

5.1 Language

5.2 [lex.phases] Phases of translation

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

7 Whitespace characters separating tokens are no longer significant. Each preprocessing token is converted into a token (5.6). The resulting tokens constitute a translation unit and are syntactically and semantically analyzed and translated. Plainly constant-evaluated expressions ([expr.const]) appearing outside template declarations are evaluated in lexical order. Diagnosable rules (4.1.1 [intro.compliance.general]) that apply to constructs whose syntactic end point occurs lexically after the syntactic end point of a plainly constant-evaluated expression X are considered in a context where X has been evaluated. […]

8 […] All the required instantiations are performed to produce instantiation units. Plainly constant-evaluated expressions ([expr.const]) appearing in those instantiation units are evaluated in lexical order as part of the instantiation process. Diagnosable rules (4.1.1 [intro.compliance.general]) that apply to constructs whose syntactic end point occurs lexically after the syntactic end point of a plainly constant-evaluated expression X are considered in a context where X has been evaluated. […]

5.4 [lex.pptoken] Preprocessing tokens

Add a bullet after 5.4 [lex.pptoken] bullet (3.2):

— Otherwise, if the next three characters are <:: and the subsequent character is neither : nor >, the < is treated as a preprocessing token by itself and not as the first character of the alternative token <:.

— Otherwise, if the next three characters are [:: and the subsequent character is not : or if the next three characters are [:>, the [ is treated as a preprocessing token by itself and not as the first character of the preprocessing token [:.

5.12 [lex.operators] Operators and punctuators

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

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

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

Add a bullet to paragraph 3 of 6.5.4 [basic.lookup.argdep] as follows [ Editor's note: this must precede the fundamental type bullet, because meta::info is a fundamental type ]:

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

  • (3.0) If T is std::meta::info, its associated set of entities is the singleton containing the function std::meta::type_is.
  • (3.1) If T is a fundamental type, its associated set of entities is empty.
  • (3.2) If T is a class type …

6.5.5.1 [basic.lookup.qual.general] General

Extend 6.5.5.1 [basic.lookup.qual.general]/1-2 to cover splice-name-qualifer:

1 Lookup of an identifier followed by a ​::​ scope resolution operator considers only namespaces, types, and templates whose specializations are types. If a name, template-id, or computed-type-specifier, or splice-name-qualifier is followed by a ​::​, it shall designate a namespace, class, enumeration, or dependent type, and the ​::​ is never interpreted as a complete nested-name-specifier.

2 A member-qualified name is the (unique) component name ([expr.prim.id.unqual]), if any, of

  • (2.1) an unqualified-id or
  • (2.2) a nested-name-specifier of the form type-name :: or, namespace-name ::, or splice-name-qualifier ::

in the id-expression of a class member access expression ([expr.ref]). […]

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), enumeration types, pointer types, pointer-to-member types (6.8.4), std::meta::info, std::nullptr_t, and cv-qualified (6.8.5) versions of these types are collectively called scalar types.

Add a new paragraph at the end of 6.8.1 [basic.types.general] as follows:

* A consteval-only type is one of the following:

  • std::meta::info, or
  • a pointer or reference to a consteval-only type, or
  • an (possibly multi-dimensional) array of a consteval-only type, or
  • a pointer-to-member type to a class C of type M where either C or M is a consteval-only type, or
  • a function type with a consteval-only return type or a consteval-only parameter type, or
  • a class type with a consteval-only base class type or consteval-only non-static data member type.

An object of consteval-only type shall either end its lifetime during the evaluation of a manifestly constant-evaluated expression or conversion (7.7 [expr.const]), or be a constexpr variable that is not odr-used (6.3 [basic.def.odr]).

6.8.2 [basic.fundamental] Fundamental types

Add a new paragraph before the last paragraph of 6.8.2 [basic.fundamental] as follows:

* A value of type std::meta::info is called a reflection and represents a language element such as a type, a value, an object, a non-static data member, etc. An expression convertible to std::meta::info is said to reflect the language element represented by the resulting value; the language element is said to be reflected by the expression. sizeof(std::meta::info) shall be equal to sizeof(void*). Note 1: Reflections are only meaningful during translation. The notion of consteval-only types (see 6.8.1 [basic.types.general]) exists to diagnose attempts at using such values outside the translation process. — end note ]

7.5 [expr.prim] Primary expressions

Change the grammar for primary-expression in 7.5 [expr.prim] as follows:

  primary-expression:
     literal
     this
     ( expression )
     id-expression
     lambda-expression
     fold-expression
     requires-expression
+    [: constant-expression :]
+    template[: constant-expression :] < template-argument-listopt >

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

Add a production to the grammar for nested-name-specifier as follows:

  nested-name-specifier:
      ::
      type-name ::
      namespace-name ::
      computed-type-specifier ::
+     splice-name-qualifier ::
      nested-name-specifier identifier ::
      nested-name-specifier templateopt simple-template-id ::
+
+ splice-name-qualifier:
+     [: constant-expression :]

Extend 7.5.4.3 [expr.prim.id.qual]/1 to also cover splices:

1 The component names of a qualified-id are those of its nested-name-specifier and unqualified-id. The component names of a nested-name-specifier are its identifier (if any) and those of its type-name, namespace-name, simple-template-id, and/or nested-name-specifier, and/or the type-name or namespace-name of the entity reflected by the constant-expression of its splice-name-qualifier. For a nested-name-specifier having a splice-name-qualifier with a constant-expression that reflects the global namespace, the component names are the same as for ::. The constant-expression of a splice-name-qualifier shall be a reflection of either a type-name, namespace-name, or the global namespace.

Extend 7.5.4.3 [expr.prim.id.qual]/3 to also cover splices:

3 The nested-name-specifier ::​ nominates the global namespace. A nested-name-specifier with a computed-type-specifier nominates the type denoted by the computed-type-specifier, which shall be a class or enumeration type. A nested-name-specifier with a splice-name-qualifier nominates the entity reflected by the constant-expression of the splice-name-qualifier. 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 by N without A. T shall be a class template.

7.5.8* [expr.prim.splice] Expression splicing

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

Expression Splicing [expr.prim.splice]

1 For a primary-expression of the form [: constant-expression :] or template[: constant-expression :] < template-argument-listopt > the constant-expression shall be a converted constant expression (7.7 [expr.const]) of type std::meta::info.

2 For a primary-expression of the form template[: constant-expression :] < template-argument-listopt > the converted constant-expression shall evaluate to a reflection for a concept, variable template, class template, alias template, or function template that is not a constructor template or destructor template. The meaning of such a construct is identical to that of a primary-expression of the form template-name < template-argument-listopt > where template-name denotes the reflected template or concept (ignoring access checking on the template-name).

3 For a primary-expression of the form [: constant-expression :] where the converted constant-expression evaluates to a reflection for an object, a function, an enumerator, or a structured binding, the meaning of the expression is identical to that of a primary-expression of the form id-expression that would denote the reflected entity (ignoring access checking).

4 Otherwise, for a primary-expression of the form [: constant-expression :] the converted constant-expression shall evaluate to a reflection of a value, and the expression shall be a prvalue whose evaluation computes the reflected value.

7.6.2.1 [expr.unary.general] General

Change 7.6.2.1 [expr.unary.general] paragraph 1 to add productions for the new operator:

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

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

+ reflect-expression:
+    ^ ::
+    ^ namespace-name
+    ^ nested-name-specifieropt template-name
+    ^ nested-name-specifieropt concept-name
+    ^ type-id
+    ^ id-expression

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]

1 The unary ^ operator (called the reflection operator) produces a prvalue — called a reflection — whose type is the reflection type (i.e., std::meta::info). That reflection represents its operand.

2 Every value of type std::meta::info is either a reflection of some operand or a null reflection value.

3

Example 1:
static_assert(is_type(^int()));    // ^ applies to the type-id "int()"

template<bool> struct X {};
bool operator<(std::meta::info, X<false>);
consteval void g(std::meta::info r, X<false> xv) {
  if (r == ^int && true);    // error: ^ applies to the type-id "int&&"
  if (r == (^int) && true);  // OK
  if (^X < xv);       // error: < starts template argument list
  if ((^X) < xv);     // OK
}
— end example ]

4 When applied to ::, the reflection operator produces a reflection for the global namespace. When applied to a namespace-name, the reflection operator produces a reflection for the indicated namespace or namespace alias.

5 When applied to a template-name, the reflection operator produces a reflection for the indicated template.

6 When applied to a concept-name, the reflection operator produces a reflection for the indicated concept.

7 When applied to a type-id, the reflection operator produces a reflection for the indicated type or type alias.

8 When applied to an id-expression (7.5.4 [expr.prim.id]), the reflection operator produces a reflection of the variable, function, enumerator constant, or non-static member designated by the operand. The id-expression is not evaluated.

  • (8.1) If this id-expression names an overload set S, and if the assignment of S to an invented variable of type const auto (9.2.9.6.2 [dcl.type.auto.deduct]) would select a unique candidate function F from S, the result is a reflection of F. Otherwise, the expression ^S is ill-formed.
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 R = ^fn<char>;     // OK
constexpr auto S = ^fn<int>;      // error: cannot reflect an overload set

constexpr auto r = ^std::vector;  // OK
— end example ]

7.6.10 [expr.eq] Equality Operators

Extend 7.6.10 [expr.eq]/2 to also handle `std::meta::info:

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

Add a new paragraph between 7.6.10 [expr.eq]/5 and /6:

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

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

  • (*.1) If both operands are null reflection values, then they compare equal.
  • (*.2) Otherwise, if one operand is a null reflection value, then they compare unequal.
  • (*.3) Otherwise, if one operand is a reflection of a namespace alias, alias template, or type alias and the other operand is not a reflection of the same kind of alias, they compare unequal. Note 1: A reflection of a type and a reflection of an alias to that same type do not compare equal. — end note ]
  • (*.4) Otherwise, if both operands are reflections of a namespace alias, alias template, or type alias, then they compare equal if their reflected aliases share the same name, are declared within the same enclosing scope, and alias the same underlying entity.
  • (*.5) Otherwise, if neither operand is a reflection of a value, then they compare equal if they are reflections of the same entity.
  • (*.6) Otherwise, if one operand is a reflection of a value and the other is not, then they compare unequal.
  • (*.7) Otherwise, if both operands are reflections of values, then they compare equally if and only if the reflected values are template-argument-equivalent (13.6 [temp.type]).
  • Otherwise the result is unspecified.

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

Add a new paragraph after the definition of manifestly constant-evaluated 7.7 [expr.const]/20:

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

9.2.4 [dcl.typedef] The typedef specifier

Introduce the term “type alias” to 9.2.4 [dcl.typedef]:

1 […] A name declared with the typedef specifier becomes a typedef-name. A typedef-name names the type associated with the identifier ([dcl.decl]) or simple-template-id ([temp.pre]); a typedef-name is thus a synonym for another type. A typedef-name does not introduce a new type the way a class declaration ([class.name]) or enum declaration ([dcl.enum]) does.

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

* A type alias is either a name declared with the typedef specifier or a name introduced by an alias-declaration.

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

Change paragraphs 6-9 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 initialied to a null reflection value;
  • (6.1) if T is a scalar type ([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 T is std::meta::info, default-initialization of T would invoke a user-provided constructor of T (not inherited from a base class), or if

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 the reflection operator, 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 ;

+ splice-enum-name:
+    [: constant-expression :]
+
  using-enum-declarator:
     nested-name-specifieropt identifier
     nested-name-specifieropt simple-template-id
+    splice-enum-name

Modify paragraph 1 of 9.7.2 [enum.udecl] as follows:

1 A using-enum-declarator not consisting of a splice-enum-name names the set of declarations found by lookup (6.5.3 [basic.lookup.unqual], 6.5.5 [basic.lookup.qual]) for the using-enum-declarator. A using-enum-declarator containing a splice-enum-name names the entity reflected by the constant-expression. The using-enum-declarator shall designate a non-dependent type with a reachable enum-specifier.

9.8.4 [namespace.udir] Using namespace directive

Modify the grammar for using-directive as follows:

+ splice-namespace-name:
+    [: constant-expression :]
+
+ namespace-declarator:
+    nested-name-specifieropt namespace-name
+    splice-namespace-name
+
  using-directive:
-    attribute-specifier-seqopt using namespace $nested-name-specifieropt namespace-name
+    attribute-specifier-seqopt using namespace namespace-declarator

Add the following to paragraph 1 of 9.8.4 [namespace.udir], prior to the note:

1 A using-directive shall not appear in class scope, but may appear in namespace scope or in block scope. A namespace-declarator not consisting of a splice-namespace-name nominates the namespace found by lookup (6.5.3 [basic.lookup.unqual], 6.5.5 [basic.lookup.qual]) and shall not contain a dependent nested-name-specifier. A namespace-declarator consisting of a splice-namespace-name shall contain a non-dependent constant-expression that reflects a namespace or namespace alias, and nominates the entity reflected by the constant-expression.

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 ]

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.3 [temp.names] Names of template specializations

Modify the grammar for template-argument as follows:

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

Add a paragraph after paragraph 3 of 13.3 [temp.names]:

* A < is also interpreted as the delimiter of a template-argument-list if it follows a splicer of the form template[: constant-expression :].

13.4.1 [temp.arg.general] General

Adjust paragraph 3 of [temp.arg.general] to not apply to splice template arguments:

3 In a template-argument which does not contain a splice-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. In a template-argument containing a splice-template-argument, an ambiguity between a splice-template-argument and an expression is resolved to a splice-template-argument.

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. A template-argument having a splice-template-argument for such a template-parameter is treated as if it were a type-id nominating the type reflected by the constant-expression of the splice-template-argument.

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

Extend 13.4.3 [temp.arg.nontype]/2 to cover splice template arguments:

2 The value of a non-type template-parameter P of (possibly deduced) type T is determined from its template argument A as follows. If T is not a class type and A is notneither a braced-init-list nor a splice-template-argument, A shall be a converted constant expression ([expr.const]) of type T; the value of P is A (as converted).

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 id-expression, or a splice-template-argument. A template-argument for a template template-parameter having a splice-template-argument is treated as an id-expression nominating the class template or alias template reflected by the constant-expression of the splice-template-argument.

13.6 [temp.type] Type equivalence

Extend template-argument-equivalent to handle std::meta::info:

2 Two values are template-argument-equivalent if they are of the same type and

  • (2.1) they are of integral type and their values are the same, or
  • (2.2) they are of floating-point type and their values are identical, or
  • (2.3) they are of type std​::​nullptr_t, or
  • (2.*) they are of type std::meta::info and they compare equal, or
  • (2.4) they are of enumeration type and their values are the same, or
  • (2.5) […]

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

Add to the list of never-type-dependent expression forms in 13.8.3.3 [temp.dep.expr]/4:

     literal
     sizeof unary-expression
     sizeof ( type-id )
     sizeof ... ( identifier )
     alignof ( type-id )
     typeid ( expression )
     typeid ( type-id )
     ::opt delete cast-expression
     ::opt delete [ ] cast-expression
     throw assignment-expressionopt
     noexcept ( expression )
     requires-expression
+    reflect-expression

Add a new paragraph at the end of 13.8.3.3 [temp.dep.expr]:

9 A primary-expression of the form [: constant-expression :] or template[: constant-expression :] < template-argument-listopt > is type-dependent if the constant-expression is value-dependent or if the optional template-argument-list contains a value-dependent nontype or template argument, or a dependent type argument.

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 the operand of the reflection operator is a type-dependent or value-dependent expression or if that operand is a dependent type-id, a dependent namespace-name, or a dependent template-name.

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

6 A primary-expression of the form [: constant-expression :] or template[: constant-expression :] < template-argument-listopt > is value-dependent if the constant-expression is value-dependent or if the optional template-argument-list contains a value-dependent nontype or template argument, or a dependent type argument.

5.2 Library

[meta] Header <meta> synopsis

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

Header <meta> synopsis

namespace std::meta {
  using info = decltype(^::);

  // [meta.reflection.names], reflection names and locations
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval T name_of(info r);
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval T qualified_name_of(info r);
  template<typename T = std::u8string_view>
    requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
    consteval T display_name_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_accessible(info r);
  consteval bool is_virtual(info r);
  consteval bool is_pure_virtual(info r);
  consteval bool is_override(info r);
  consteval bool is_deleted(info r);
  consteval bool is_defaulted(info r);
  consteval bool is_explicit(info r);
  consteval bool is_noexcept(info r);
  consteval bool is_bit_field(info r);
  consteval bool is_const(info r);
  consteval bool is_volatile(info r);
  consteval bool is_final(info r);
  consteval bool has_static_storage_duration(info r);
  consteval bool has_internal_linkage(info r);
  consteval bool has_external_linkage(info r);
  consteval bool has_linkage(info r);

  consteval bool is_namespace(info r);
  consteval bool is_function(info r);
  consteval bool is_variable(info r);
  consteval bool is_type(info r);
  consteval bool is_alias(info r);
  consteval bool is_incomplete_type(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_concept(info r);
  consteval bool is_value(info r);
  consteval bool is_object(info r);
  consteval bool is_structured_binding(info r);
  consteval bool has_template_arguments(info r);
  consteval bool is_class_member(info entity);
  consteval bool is_namespace_member(info entity);
  consteval bool is_nonstatic_data_member(info r);
  consteval bool is_static_member(info r);
  consteval bool is_base(info r);
  consteval bool is_constructor(info r);
  consteval bool is_destructor(info r);
  consteval bool is_special_member(info r);
  consteval bool is_user_provided(info r);

  consteval info type_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
  template<class... Fs>
    consteval vector<info> members_of(info type, Fs... filters);
  template<class... Fs>
    consteval vector<info> accessible_members_of(info type, Fs... filters);
  template<class... Fs>
    consteval vector<info> bases_of(info type, Fs... filters);
  template<class... Fs>
    consteval vector<info> accessible_bases_of(info type, Fs... filters);
  consteval vector<info> static_data_members_of(info type);
  consteval vector<info> accessible_static_data_members_of(info type);
  consteval vector<info> nonstatic_data_members_of(info type);
  consteval vector<info> accessible_nonstatic_data_members_of(info type);
  consteval vector<info> subobjects_of(info type);
  consteval vector<info> accessible_subobjects_of(info type);
  consteval vector<info> enumerators_of(info type_enum);

  // [meta.reflection.layout], reflection layout queries
  consteval size_t offset_of(info entity);
  consteval size_t size_of(info entity);
  consteval size_t alignment_of(info entity);
  consteval size_t bit_offset_of(info entity);
  consteval size_t bit_size_of(info entity);

  // [meta.reflection.substitute], reflection substitution
  template <class R>
  concept reflection_range = see below;

  template <reflection_range R = span<info const>>
  consteval bool can_substitute(info templ, R&& arguments);
  template <reflection_range R = span<info const>>
  consteval info substitute(info templ, R&& arguments);

  // [meta.reflection.unary.cat], primary type categories
  consteval bool type_is_void(info type);
  consteval bool type_is_null_pointer(info type);
  consteval bool type_is_integral(info type);
  consteval bool type_is_floating_point(info type);
  consteval bool type_is_array(info type);
  consteval bool type_is_pointer(info type);
  consteval bool type_is_lvalue_reference(info type);
  consteval bool type_is_rvalue_reference(info type);
  consteval bool type_is_member_object_pointer(info type);
  consteval bool type_is_member_function_pointer(info type);
  consteval bool type_is_enum(info type);
  consteval bool type_is_union(info type);
  consteval bool type_is_class(info type);
  consteval bool type_is_function(info type);

  // [meta.reflection.unary.comp], composite type categories
  consteval bool type_is_reference(info type);
  consteval bool type_is_arithmetic(info type);
  consteval bool type_is_fundamental(info type);
  consteval bool type_is_object(info type);
  consteval bool type_is_scalar(info type);
  consteval bool type_is_compound(info type);
  consteval bool type_is_member_pointer(info type);

  // [meta.reflection unary.prop], type properties
  consteval bool type_is_const(info type);
  consteval bool type_is_volatile(info type);
  consteval bool type_is_trivial(info type);
  consteval bool type_is_trivially_copyable(info type);
  consteval bool type_is_standard_layout(info type);
  consteval bool type_is_empty(info type);
  consteval bool type_is_polymorphic(info type);
  consteval bool type_is_abstract(info type);
  consteval bool type_is_final(info type);
  consteval bool type_is_aggregate(info type);
  consteval bool type_is_signed(info type);
  consteval bool type_is_unsigned(info type);
  consteval bool type_is_bounded_array(info type);
  consteval bool type_is_unbounded_array(info type);
  consteval bool type_is_scoped_enum(info type);

  template <reflection_range R = span<info const>>
  consteval bool type_is_constructible(info type, R&& type_args);
  consteval bool type_is_default_constructible(info type);
  consteval bool type_is_copy_constructible(info type);
  consteval bool type_is_move_constructible(info type);

  consteval bool type_is_assignable(info type_dst, info type_src);
  consteval bool type_is_copy_assignable(info type);
  consteval bool type_is_move_assignable(info type);

  consteval bool type_is_swappable_with(info type_dst, info type_src);
  consteval bool type_is_swappable(info type);

  consteval bool type_is_destructible(info type);

  template <reflection_range R = span<info const>>
  consteval bool type_is_trivially_constructible(info type, R&& type_args);
  consteval bool type_is_trivially_default_constructible(info type);
  consteval bool type_is_trivially_copy_constructible(info type);
  consteval bool type_is_trivially_move_constructible(info type);

  consteval bool type_is_trivially_assignable(info type_dst, info type_src);
  consteval bool type_is_trivially_copy_assignable(info type);
  consteval bool type_is_trivially_move_assignable(info type);
  consteval bool type_is_trivially_destructible(info type);

  template <reflection_range R = span<info const>>
  consteval bool type_is_nothrow_constructible(info type, R&& type_args);
  consteval bool type_is_nothrow_default_constructible(info type);
  consteval bool type_is_nothrow_copy_constructible(info type);
  consteval bool type_is_nothrow_move_constructible(info type);

  consteval bool type_is_nothrow_assignable(info type_dst, info type_src);
  consteval bool type_is_nothrow_copy_assignable(info type);
  consteval bool type_is_nothrow_move_assignable(info type);

  consteval bool type_is_nothrow_swappable_with(info type_dst, info type_src);
  consteval bool type_is_nothrow_swappable(info type);

  consteval bool type_is_nothrow_destructible(info type);

  consteval bool type_is_implicit_lifetime(info type);

  consteval bool type_has_virtual_destructor(info type);

  consteval bool type_has_unique_object_representations(info type);

  consteval bool type_reference_constructs_from_temporary(info type_dst, info type_src);
  consteval bool type_reference_converts_from_temporary(info type_dst, info type_src);

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

  // [meta.reflection.rel], type relations
  consteval bool type_is_same(info type1, info type2);
  consteval bool type_is_base_of(info type_base, info type_derived);
  consteval bool type_is_convertible(info type_src, info type_dst);
  consteval bool type_is_nothrow_convertible(info type_src, info type_dst);
  consteval bool type_is_layout_compatible(info type1, info type2);
  consteval bool type_is_pointer_interconvertible_base_of(info type_base, info type_derived);

  template <reflection_range R = span<info const>>
  consteval bool type_is_invocable(info type, R&& type_args);
  template <reflection_range R = span<info const>>
  consteval bool type_is_invocable_r(info type_result, info type, R&& type_args);

  template <reflection_range R = span<info const>>
  consteval bool type_is_nothrow_invocable(info type, R&& type_args);
  template <reflection_range R = span<info const>>
  consteval bool type_is_nothrow_invocable_r(info type_result, info type, R&& type_args);

  // [meta.reflection.trans.cv], const-volatile modifications
  consteval info type_remove_const(info type);
  consteval info type_remove_volatile(info type);
  consteval info type_remove_cv(info type);
  consteval info type_add_const(info type);
  consteval info type_add_volatile(info type);
  consteval info type_add_cv(info type);

  // [meta.reflection.trans.ref], reference modifications
  consteval info type_remove_reference(info type);
  consteval info type_add_lvalue_reference(info type);
  consteval info type_add_rvalue_reference(info type);

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

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

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

  // [meta.reflection.trans.other], other transformations
  consteval info type_remove_cvref(info type);
  consteval info type_decay(info type);
  template <reflection_range R = span<info const>>
  consteval info type_common_type(R&& type_args);
  template <reflection_range R = span<info const>>
  consteval info type_common_reference(R&& type_args);
  consteval info type_underlying_type(info type);
  template <reflection_range R = span<info const>>
  consteval info type_invoke_result(info type, R&& type_args);
  consteval info type_unwrap_reference(info type);
  consteval info type_unwrap_ref_decay(info type);
}

[meta.reflection.names] Reflection names and locations

template<typename T = std::u8string_view>
  requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
  consteval T name_of(info r);
template<typename T = std::u8string_view>
  requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
  consteval T qualified_name_of(info r);

1 Returns: If r designates a declared entity X, then the unqualified and qualified names of X, respectively. Otherwise, an empty string_view or u8string_view.

template<typename T = std::u8string_view>
  requires (^T == dealias(^std::string_view) || ^T == dealias(^std::u8string_view))
  consteval T display_name_of(info r);

2 Returns: An implementation-defined string suitable for identifying the reflected construct.

consteval source_location source_location_of(info r);

3 Returns: An implementation-defined source_location corresponding to the reflected construct.

[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 designates a class member or base class that is public, protected, or private, respectively. Otherwise, false.

consteval bool is_accessible(info r);

2 Returns: true if r designates a class member or base class that is accessible at the point of the immediate invocation ([expr.const]) that resulted in the evaluation of is_accessible(r). Otherwise, false.

consteval bool is_virtual(info r);

3 Returns: true if r designates a either a virtual member function or a virtual base class. Otherwise, false.

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

4 Returns: true if r designates a member function that is pure virtual or overrides another member function, respectively. Otherwise, false.

consteval bool is_deleted(info r);

5 Returns: true if r designates a function or member function that is defined as deleted. Otherwise, false.

consteval bool is_defaulted(info r);

6 Returns: true if r designates a member function that is defined as defaulted. Otherwise, false.

consteval bool is_explicit(info r);

7 Returns: true if r designates a member function that is declared explicit. Otherwise, false.

consteval bool is_noexcept(info r);

8 Returns: true if r designates a member function that is declared noexcept, a closure type of a non-generic lambda whose call operator is declared noexcept, or a value of such a type. Otherwise, false.

consteval bool is_bit_field(info r);

9 Returns: true if r designates a bit-field. Otherwise, false.

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

10 Returns: true if r designates a const or volatile type (respectively), a const- or volatile-qualified member function type (respectively), or an object or function with such a type. Otherwise, false.

consteval bool is_final(info r);

11 Returns: true if r designates a final class or a final member function. Otherwise, false.

consteval bool has_static_storage_duration(info r);

12 Returns: true if r designates an object that has static storage duration. Otherwise, false.

consteval bool has_internal_linkage(info r);
consteval bool has_external_linkage(info r);
consteval bool has_linkage(info r);

13 Returns: true if r designates an entity that has internal linkage, external linkage, or any linkage, respectively ([basic.link]). Otherwise, false.

consteval bool is_namespace(info r);

14 Returns: true if r designates a namespace or namespace alias. Otherwise, false.

consteval bool is_function(info r);

15 Returns: true if r designates a function or member function. Otherwise, false.

consteval bool is_variable(info r);

16 Returns: true if r designates a variable. Otherwise, false.

consteval bool is_type(info r);

17 Returns: true if r designates a type or a type alias. Otherwise, false.

consteval bool is_alias(info r);

18 Returns: true if r designates a type alias, alias template, or namespace alias. Otherwise, false.

consteval bool is_incomplete_type(info r);

19 Mandates: r is a reflection designating a type.

20 Returns: false if the type designated by dealias(r) is a complete class type. Otherwise, true.

21 Effects: If dealias(r) designates a class template specialization with a reachable definition, the specialization is instantiated.

consteval bool is_template(info r);

22 Returns: true if r designates a function template, class template, variable template, or alias template. Otherwise, false.

23 Note 1: 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_concept(info r);
consteval bool is_structured_binding(info r);
consteval bool is_value(info r);

24 Returns: true if r designates a function template, class template, variable template, alias template, concept, structured binding, or value respectively. Otherwise, false.

consteval bool is_object(info r);

25 Returns: true if r designates an object. Otherwise, false.

consteval bool has_template_arguments(info r);

26 Returns: true if r designates an instantiation of a function template, variable template, class template, or an alias template. Otherwise, false.

consteval bool is_class_member(info entity);
consteval bool is_namespace_member(info entity);
consteval bool is_nonstatic_data_member(info r);
consteval bool is_static_member(info r);
consteval bool is_base(info r);
consteval bool is_constructor(info r);
consteval bool is_destructor(info r);
consteval bool is_special_member(info r);

27 Returns: true if r designates a class member, namespace member, non-static data member, static member, base class member, constructor, destructor, or special member, respectively. Otherwise, false.

consteval boo is_user_provided(info r);

28 Mandates: r designates a function.

29 Returns: true if r designates a user-provided (9.5.2 [dcl.fct.def.default]) function. Otherwise, false.

consteval info type_of(info r);

30 Mandates: r designates a typed entity. r does not designate a constructor or destructor.

31 Returns: A reflection of the type of that entity. If every declaration of that entity was declared with the same type alias (but not a template parameter substituted by a type alias), the reflection returned is for that alias. Otherwise, if some declaration of that entity was declared with an alias it is unspecified whether the reflection returned is for that alias or for the type underlying that alias. Otherwise, the reflection returned shall not be a type alias reflection.

consteval info parent_of(info r);

32 Mandates: r designates a member of a class or a namespace.

33 Returns: A reflection of the that entity’s immediately enclosing class or namespace.

consteval info dealias(info r);

34 Returns: If r designates a type alias or a namespace alias, a reflection designating the underlying entity. Otherwise, r.

35

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

36 Mandates: has_template_arguments(r) is true.

37 Returns: A reflection of the template of r, and the reflections of the template arguments of the specialization designated by r, respectively.

38

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

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

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

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

template<class... Fs>
  consteval vector<info> members_of(info r, Fs... filters);

1 Mandates: r is a reflection designating either a complete class type or a namespace and (std::predicate<Fs, info> && ...) is true.

2 Returns: A vector containing the reflections of all the direct members m of the entity, excluding any structured bindings, designated by r such that (filters(m) && ...) is true. Non-static data members are indexed in the order in which they are declared, but the order of other kinds of members is unspecified. Note 1: Base classes are not members. — end note ]

3 Effects: If dealias(type) designates a class template specialization with a reachable definition, the specialization is instantiated.

template<class... Fs>
  consteval vector<info> accessible_members_of(info type, Fs... filters);

4 Mandates: type is a reflection designating a complete class type.

5 Effects: Equivalent to: return members_of(type, is_accessible, filters...);

template<class... Fs>
  consteval vector<info> bases_of(info type, Fs... filters);

6 Mandates: type is a reflection designating a complete class type and (std::predicate<Fs, info> && ...) is true.

7 Returns: Let C be the type designated by type. A vector containing the reflections of all the direct base classes b, if any, of C such that (filters(b) && ...) is true. The base classes are indexed in the order in which they appear in the base-specifier-list of C.

8 Effects: If dealias(type) designates a class template specialization with a reachable definition, the specialization is instantiated.

template<class... Fs>
  consteval vector<info> accessible_bases_of(info type, Fs... filters);

9 Mandates: type is a reflection designating a complete class type.

10 Effects: Equivalent to: return bases_of(r, is_accessible, filters...);

consteval vector<info> static_data_members_of(info type);

11 Mandates: type is a reflection designating a complete class type.

12 Effects: Equivalent to: return members_of(type, is_variable);

consteval vector<info> accessible_static_data_members_of(info type);

13 Mandates: type is a reflection designating a complete class type.

14 Effects: Equivalent to: return members_of(type, is_variable, is_accessible);

consteval vector<info> nonstatic_data_members_of(info type);

15 Mandates: type is a reflection designating a complete class type.

16 Effects: Equivalent to: return members_of(type, is_nonstatic_data_member);

consteval vector<info> accessible_nonstatic_data_members_of(info type);

17 Mandates: type is a reflection designating a complete class type.

18 Effects: Equivalent to: return members_of(type, is_nonstatic_data_member, is_accessible);

consteval vector<info> subobjects_of(info type);

19 Mandates: type is a reflection designating a complete class type.

20 Returns: A vector containing all the reflections in bases_of(type) followed by all the reflections in nonstatic_data_members_of(type).

21 Effects: If dealias(type) designates a class template specialization with a reachable definition, the specialization is instantiated.

consteval vector<info> accessible_subobjects_of(info type);

22 Mandates: type is a reflection designating a complete class type.

23 Returns: A vector containing all the reflections in accessible_bases_of(type) followed by all the reflections in accessible_nonstatic_data_members_of(type).

24 Effects: If dealias(type) designates a class template specialization with a reachable definition, the specialization is instantiated.

consteval vector<info> enumerators_of(info type_enum);

25 Mandates: type_enum is a reflection designating an enumeration.

26 Returns: A vector containing the reflections of each enumerator of the enumeration designated by type_enum, in the order in which they are declared.

[meta.reflection.layout] Reflection layout queries

consteval size_t offset_of(info entity);
consteval size_t size_of(info entity);
consteval size_t alignment_of(info entity);
consteval size_t bit_offset_of(info entity);
consteval size_t bit_size_of(info entity);

[meta.reflection.substitute] Reflection substitution

template <class R>
concept reflection_range =
  ranges::input_range<R> && same_as<ranges::range_value_t<R>, info>;
template <reflection_range R = span<info const>>
consteval bool can_substitute(info templ, R&& arguments);

1 Mandates: templ designates a template.

2 Let Z be the template designated by templ and let Args... be the sequence of entities or expressions designated 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 = span<info const>>
consteval info substitute(info templ, R&& arguments);

5 Mandates: can_substitute(templ, arguments) is true.

6 Let Z be the template designated by templ and let Args... be the sequence of entities or expressions designated by the elements of arguments.

7 Returns: ^Z<Args...>.

[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.35 [defns.nonconst.libcall]) if that argument is not a reflection of a type or type alias. For each function taking an argument of type span<const meta::info> named type_args, a call to the function is a non-constant library call if any meta::info in that span is not a reflection of a type or a type alias.

[meta.reflection.unary.cat] Primary type categories

1 For any type 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 type_is_void(info type);
consteval bool type_is_null_pointer(info type);
consteval bool type_is_integral(info type);
consteval bool type_is_floating_point(info type);
consteval bool type_is_array(info type);
consteval bool type_is_pointer(info type);
consteval bool type_is_lvalue_reference(info type);
consteval bool type_is_rvalue_reference(info type);
consteval bool type_is_member_object_pointer(info type);
consteval bool type_is_member_function_pointer(info type);
consteval bool type_is_enum(info type);
consteval bool type_is_union(info type);
consteval bool type_is_class(info type);
consteval bool type_is_function(info type);

2

Example 1:
namespace std::meta {
  consteval bool type_is_void(info type) {
    // one example implementation
    return extract<bool>(substitute(^is_void_v, {type}));

    // another example implementation
    type = dealias(type);
    return type == ^void
        || type == ^const void
        || type == ^volatile void
        || type == ^const volatile void;
  }
}
— end example ]

[meta.reflection.unary.comp] Composite type categories

1 For any type 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 type_is_reference(info type);
consteval bool type_is_arithmetic(info type);
consteval bool type_is_fundamental(info type);
consteval bool type_is_object(info type);
consteval bool type_is_scalar(info type);
consteval bool type_is_compound(info type);
consteval bool type_is_member_pointer(info type);

[meta.reflection.unary.prop] Type properties

1 For any type T, for each function std::meta::UNARY-TRAIT_type defined in this clause with signature bool(std::meta::info), std::meta::UNARY-TRAIT_type(^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 T and U, for each function std::meta::BINARY-TRAIT_type defined in this clause with signature bool(std::meta::info, std::meta::info), std::meta::BINARY-TRAIT_type(^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 T and pack of types U..., for each function template std::meta::VARIADIC-TRAIT_type defined in this clause, std::meta::VARIADIC-TRAIT_type(^T, {^U...}) 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 type_is_const(info type);
consteval bool type_is_volatile(info type);
consteval bool type_is_trivial(info type);
consteval bool type_is_trivially_copyable(info type);
consteval bool type_is_standard_layout(info type);
consteval bool type_is_empty(info type);
consteval bool type_is_polymorphic(info type);
consteval bool type_is_abstract(info type);
consteval bool type_is_final(info type);
consteval bool type_is_aggregate(info type);
consteval bool type_is_signed(info type);
consteval bool type_is_unsigned(info type);
consteval bool type_is_bounded_array(info type);
consteval bool type_is_unbounded_array(info type);
consteval bool type_is_scoped_enum(info type);

template <reflection_range R = span<info const>>
consteval bool type_is_constructible(info type, R&& type_args);
consteval bool type_is_default_constructible(info type);
consteval bool type_is_copy_constructible(info type);
consteval bool type_is_move_constructible(info type);

consteval bool type_is_assignable(info type_dst, info type_src);
consteval bool type_is_copy_assignable(info type);
consteval bool type_is_move_assignable(info type);

consteval bool type_is_swappable_with(info type_dst, info type_src);
consteval bool type_is_swappable(info type);

consteval bool type_is_destructible(info type);

template <reflection_range R = span<info const>>
consteval bool type_is_trivially_constructible(info type, R&& type_args);
consteval bool type_is_trivially_default_constructible(info type);
consteval bool type_is_trivially_copy_constructible(info type);
consteval bool type_is_trivially_move_constructible(info type);

consteval bool type_is_trivially_assignable(info type_dst, info type_src);
consteval bool type_is_trivially_copy_assignable(info type);
consteval bool type_is_trivially_move_assignable(info type);
consteval bool type_is_trivially_destructible(info type);

template <reflection_range R = span<info const>>
consteval bool type_is_nothrow_constructible(info type, R&& type_args);
consteval bool type_is_nothrow_default_constructible(info type);
consteval bool type_is_nothrow_copy_constructible(info type);
consteval bool type_is_nothrow_move_constructible(info type);

consteval bool type_is_nothrow_assignable(info type_dst, info type_src);
consteval bool type_is_nothrow_copy_assignable(info type);
consteval bool type_is_nothrow_move_assignable(info type);

consteval bool type_is_nothrow_swappable_with(info type_dst, info type_src);
consteval bool type_is_nothrow_swappable(info type);

consteval bool type_is_nothrow_destructible(info type);

consteval bool type_is_implicit_lifetime(info type);

consteval bool type_has_virtual_destructor(info type);

consteval bool type_has_unique_object_representations(info type);

consteval bool type_reference_constructs_from_temporary(info type_dst, info type_src);
consteval bool type_reference_converts_from_temporary(info type_dst, info type_src);

[meta.reflection.unary.prop.query] Type property queries

1 For any type T, for each function std::meta::PROP_type defined in this clause with signature size_t(std::meta::info), std::meta::PROP_type(^T) equals the value of the corresponding type property std::PROP_v<T> as specified in 21.3.6 [meta.unary.prop.query].

2 For any type T and unsigned integer value I, std::meta::type_extent(^T, I) equals std::extent_v<T, I> ([meta.unary.prop.query]).

consteval size_t type_alignment_of(info type);
consteval size_t type_rank(info type);
consteval size_t type_extent(info type, unsigned i = 0);

[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 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 T and pack of types U..., for each binary function template std::meta::VARIADIC-REL_type, std::meta::VARIADIC-REL_type(^T, {^U...}) 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 T and R and pack of types U..., for each ternary function template std::meta::VARIADIC-REL-R_type defined in this clause, std::meta::VARIADIC-REL-R_type(^R, ^T, {^U...}) 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 type_is_same(info type1, info type2);
consteval bool type_is_base_of(info type_base, info type_derived);
consteval bool type_is_convertible(info type_src, info type_dst);
consteval bool type_is_nothrow_convertible(info type_src, info type_dst);
consteval bool type_is_layout_compatible(info type1, info type2);
consteval bool type_is_pointer_interconvertible_base_of(info type_base, info type_derived);

template <reflection_range R = span<info const>>
consteval bool type_is_invocable(info type, R&& type_args);
template <reflection_range R = span<info const>>
consteval bool type_is_invocable_r(info type_result, info type, R&& type_args);

template <reflection_range R = span<info const>>
consteval bool type_is_nothrow_invocable(info type, R&& type_args);
template <reflection_range R = span<info const>>
consteval bool type_is_nothrow_invocable_r(info type_result, info type, R&& type_args);

5 Note 1: If t is a reflection of the type int and u is a reflection of an alias to the type int, then t == u is false but is_same(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 T, for each function std::meta::MOD_type defined in this clause, std::meta::MOD_type(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.2 [meta.trans.cv].

consteval info type_remove_const(info type);
consteval info type_remove_volatile(info type);
consteval info type_remove_cv(info type);
consteval info type_add_const(info type);
consteval info type_add_volatile(info type);
consteval info type_add_cv(info type);

[meta.reflection.trans.ref], Reference modifications

1 For any type T, for each function std::meta::MOD_type defined in this clause, std::meta::MOD_type(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.3 [meta.trans.ref].

consteval info type_remove_reference(info type);
consteval info type_add_lvalue_reference(info type);
consteval info type_add_rvalue_reference(info type);

[meta.reflection.trans.sign], Sign modifications

1 For any type T, for each function std::meta::MOD_type defined in this clause, std::meta::MOD_type(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.4 [meta.trans.sign].

consteval info type_make_signed(info type);
consteval info type_make_unsigned(info type);

[meta.reflection.trans.arr], Array modifications

1 For any type T, for each function std::meta::MOD_type defined in this clause, std::meta::MOD_type(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.5 [meta.trans.arr].

consteval info type_remove_extent(info type);
consteval info type_remove_all_extents(info type);

[meta.reflection.trans.ptr], Pointer modifications

1 For any type T, for each function std::meta::MOD_type defined in this clause, std::meta::MOD_type(^T) returns the reflection of the corresponding type std::MOD_t<T> as specified in 21.3.8.6 [meta.trans.ptr].

consteval info type_remove_pointer(info type);
consteval info type_add_pointer(info type);

[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 T, for each function std::meta::MOD_type defined in this clause with signature std::meta::info(std::meta::info), std::meta::MOD_type(^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 T..., for each unary function template std::meta::VARIADIC-MOD_type defined in this clause, std::meta::VARIADIC-MOD_type({^T...}) 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 T and pack of types U..., std::meta::type_invoke_result(^T, {^U...}) returns the reflection of the corresponding type std::invoke_result_t<T, U...> (21.3.8.7 [meta.trans.other]).

consteval info type_remove_cvref(info type);
consteval info type_decay(info type);
template <reflection_range R = span<info const>>
consteval info type_common_type(R&& type_args);
template <reflection_range R = span<info const>>
consteval info type_common_reference(R&& type_args);
consteval info type_underlying_type(info type);
template <reflection_range R = span<info const>>
consteval info type_invoke_result(info type, R&& type_args);
consteval info type_unwrap_reference(info type);
consteval info type_unwrap_ref_decay(info type);

4

Example 1:
// example implementation
consteval info type_unwrap_reference(info type) {
  type = dealias(type);
  if (has_template_arguments(type) && template_of(type) == ^reference_wrapper) {
    return type_add_lvalue_reference(template_arguments_of(type)[0]);
  } else {
    return type;
  }
}
— end example ]

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] Andrew Sutton, Sam Goodrick, Daveed Vandevoorde, and Dan Katz. 2024-05-07. Expansion statements.
https://wg21.link/p1306r2
[P1887R1] Corentin Jabot. 2020-01-13. Reflection on attributes.
https://wg21.link/p1887r1
[P1974R0] Jeff Snyder, Louis Dionne, Daveed Vandevoorde. 2020-05-15. Non-transient constexpr allocation using propconst.
https://wg21.link/p1974r0
[P2237R0] Andrew Sutton. 2020-10-15. Metaprogramming.
https://wg21.link/p2237r0
[P2670R1] Barry Revzin. 2023-02-03. Non-transient constexpr allocation.
https://wg21.link/p2670r1
[P2758R1] Barry Revzin. 2023-12-09. Emitting messages at compile time.
https://wg21.link/p2758r1
[P2996R0] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2023-10-15. Reflection for C++26.
https://wg21.link/p2996r0
[P2996R1] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2023-12-18. Reflection for C++26.
https://wg21.link/p2996r1
[P2996R2] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-02-15. Reflection for C++26.
https://wg21.link/p2996r2
[P3068R1] Hana Dusíková. 2024-03-18. Allowing exception throwing in constant-evaluation.
https://wg21.link/p3293r0
[P3096R1] Adam Lach and Walter Genovese. 2024-04-29. Function Parameter Reflection in Reflection for C++26.
https://wg21.link/p3096r1
[P3293R0] Peter Dimov, Dan Katz, Barry Revzin, and Daveed Vandevoorde. 2024-05-19. Splicing a base class subobject.
https://wg21.link/p3293r0
[P3295R0] Ben Craig. 2024-05-18. Freestanding constexpr containers and constexpr exception types.
https://wg21.link/p3295r0