This proposal allows encapsulated types, including
string_view and span, to be used with
define_static_array, and to be conveyed from compile-time code
to run-time code.
The current compile-time/run-time bridge of reflection requires using raw pointers and separate lengths for object graphs, or any nested arrays. C++ programmers shouldn't have to use such low-level facilities for that purpose. This proposal aims to fix that problem, allowing the use of better abstractions for that purpose.
This paper proposes a new class-property-specifier,
structural, used the same way as final:
class Widget structural {
int id;
std::string_view name;
public:
constexpr Widget(int i, std::string_view n) : id(i), name(n) {}
constexpr int get_id() const { return id; }
constexpr std::string_view get_name() const { return name; }
};
A class marked structural is a structural type even if it
has private or protected bases or non-static data members, provided that
everything else the current rules require still holds. That is, the class
is a literal type, no member is mutable, and the type of every
base and non-static data member is itself structural. This paper also proposes
marking std::span and std::basic_string_view
structural, and changing std::define_static_string to return a
std::basic_string_view instead of a pointer, the same way
std::define_static_array already returns a
std::span.
Together, these changes fix the most visible remaining problem with
C++26 Reflection: you can't use std::define_static_array to
convey a compile-time-computed array of encapsulated class objects to run
time, nor an array of objects that contain string_views or
spans. The proposal is deliberately small. It is not intended
to solve every problem with constant template parameters of class type.
P4340R0 provides more
functionality at the cost of being less straightforward. It's not much more
complicated to use its proposed default form, but that's still considered
too elaborate for the simple cases, by the author of this paper. The two
are compatible.
Since C++20, a class type can be the type of a constant template parameter if it is a structural type ([temp.param]/12). For a class, that means all of its bases and non-static data members must be public and non-mutable, and their types must be structural too, recursively.
C++26 Reflection builds on the same notion. std::meta::reflect_constant,
std::meta::reflect_constant_array,
std::define_static_object and
std::define_static_array all require structural types. That
is what allows a value computed during constant evaluation to be promoted
to an object with static storage duration, which run-time code can then
read.
The public-only requirement is a poor fit for arguably idiomatic C++ code, for one major reason, and two more detailed reasons. The major reason is that Reflection Should Not Require De-encapsulation. The two more detailed reasons are:
define_static_array today, however simple its members are,
unless every member is made public.std::string_view or a std::span.
Both are, conceptually, a pointer and a length, which are perfectly
structural, but the members are private. Those are exactly the
vocabulary types that reflection-generated data is made of. Names come
back from std::define_static_string, and nested sequences come
back from std::define_static_array, which already returns a
span.Consider a program that reflects over the classes in a namespace and builds, at compile time, a graph that it walks at run time. This is a real use case, from someone writing a reflection-based code generator intended to generate wrappers for types in whichever program. The full example is at https://godbolt.org/z/cfhsjhr71; here is its core:
class FunctionParameterData structural {
std::string_view param_type;
public:
constexpr FunctionParameterData(std::string_view pt) : param_type(pt) {}
std::string_view get_param_type() const { return param_type; }
};
class MemberFunctionData structural {
std::string_view name;
std::string_view return_type;
std::span<const FunctionParameterData> params;
public:
constexpr MemberFunctionData(std::string_view n, std::string_view ret,
std::span<const FunctionParameterData> parms)
: name(n), return_type(ret), params(parms) {}
// getters ...
};
class ClassData structural {
std::string_view name;
std::span<const MemberFunctionData> members;
public:
// constructor and getters ...
};
consteval std::span<const FunctionParameterData> get_params(std::meta::info fun) {
std::vector<FunctionParameterData> ret;
for (auto x : std::meta::parameters_of(fun))
ret.push_back(FunctionParameterData(
std::define_static_string(std::meta::display_string_of(type_of(x)))));
return std::define_static_array(ret);
}
// get_members() and get_classes() follow the same pattern ...
int main() {
constexpr std::span data = get_classes();
for (auto&& x : data) {
std::cout << "Found class with name " << x.get_name() << '\n';
for (auto&& mem : x.get_members()) {
std::cout << "member " << mem.get_name() << '\n';
for (auto&& param : mem.get_params())
std::cout << "param " << param.get_param_type() << '\n';
}
}
}
This is how a C++ programmer would naturally write it: three
encapsulated classes, each holding string_views and a
span of the next level down. With the proposed changes, it
compiles and prints the expected graph. Without the structural
marking, the first define_static_array is rejected:
error: 'reflect_constant_array' argument with 'std::ranges::range_value_t<std::vector<FunctionParameterData> >'
{aka 'FunctionParameterData'} which is not a structural type
note: 'FunctionParameterData::param_type' is not public
Making the data public doesn't help, because the members are
string_views and spans, which aren't structural
either. With the C++26 rules, the user has to replace each
string_view with a raw const char* and each
span with a pointer and a separate length. They then have to
make every member public, and rebuild the views by hand at run time. Every
level of the data structure adds the same chore.
The two promotion functions are also inconsistent with each other.
std::define_static_array returns a span, which
knows its length. std::define_static_string returns a bare
const CharT*. The length is known at the point of the call,
but it is thrown away, and every caller has to recover it from the
terminator. The example above does exactly that: each
define_static_string result is immediately turned back into a
string_view by a constructor that scans for the terminator.
Once string_view is structural, it is the natural result type:
it's what users store, it can be a constant template argument directly, and
it matches define_static_array.
The table below shows a three-level nested structure (a class, its member functions, and their parameters) built at compile time and walked at run time, for this class:
struct Widget { void resize(int w, int h); int area() const; };
constexpr auto ctx = std::meta::access_context::unprivileged();
using namespace std::meta;
Both versions print the same output. On the left is what C++26 requires
today: every member public, each string_view replaced by a
const char*, and each span replaced by a pointer
and a separate count. Run-time code then rebuilds every span by hand. On
the right is the same program with this proposal: encapsulated classes that
hold string_views and spans directly. Without the
structural marking, the right-hand version is rejected with the
error shown above.
| C++26 today | With this proposal |
|---|---|
struct ParamData { // every member public
const char* type; // not std::string_view
};
struct FunctionData {
const char* name;
const ParamData* params; // not std::span
std::size_t param_count;
};
struct ClassData {
const char* name;
const FunctionData* functions;
std::size_t function_count;
};
consteval ClassData make_class(std::meta::info cls) {
std::vector<FunctionData> fns;
for (auto f : members_of(cls, ctx))
if (is_function(f) && has_identifier(f)) {
std::vector<ParamData> ps;
for (auto p : parameters_of(f))
ps.push_back({std::define_static_string(
display_string_of(type_of(p)))});
auto sp = std::define_static_array(ps);
fns.push_back({std::define_static_string(identifier_of(f)),
sp.data(), sp.size()});
}
auto sf = std::define_static_array(fns);
return {std::define_static_string(identifier_of(cls)),
sf.data(), sf.size()};
}
int main() {
constexpr ClassData c = make_class(^^Widget);
std::cout << c.name << '\n';
for (auto& f : std::span(c.functions, c.function_count)) {
std::cout << " " << f.name << '\n';
for (auto& p : std::span(f.params, f.param_count))
std::cout << " " << p.type << '\n';
}
}
|
class ParamData structural {
std::string_view type;
public:
constexpr ParamData(std::string_view t) : type(t) {}
constexpr std::string_view get_type() const { return type; }
};
class FunctionData structural {
std::string_view name;
std::span<const ParamData> params;
public:
constexpr FunctionData(std::string_view n, std::span<const ParamData> p)
: name(n), params(p) {}
constexpr std::string_view get_name() const { return name; }
constexpr std::span<const ParamData> get_params() const { return params; }
};
class ClassData structural {
std::string_view name;
std::span<const FunctionData> functions;
public:
constexpr ClassData(std::string_view n, std::span<const FunctionData> f)
: name(n), functions(f) {}
constexpr std::string_view get_name() const { return name; }
constexpr std::span<const FunctionData> get_functions() const { return functions; }
};
consteval ClassData make_class(std::meta::info cls) {
std::vector<FunctionData> fns;
for (auto f : members_of(cls, ctx))
if (is_function(f) && has_identifier(f)) {
std::vector<ParamData> ps;
for (auto p : parameters_of(f))
ps.push_back({std::define_static_string(
display_string_of(type_of(p)))});
fns.push_back({std::define_static_string(identifier_of(f)),
std::define_static_array(ps)});
}
return {std::define_static_string(identifier_of(cls)),
std::define_static_array(fns)};
}
int main() {
constexpr ClassData c = make_class(^^Widget);
std::cout << c.get_name() << '\n';
for (auto& f : c.get_functions()) {
std::cout << " " << f.get_name() << '\n';
for (auto& p : f.get_params())
std::cout << " " << p.get_type() << '\n';
}
}
|
Output of both:
Widget
resize
int
int
area
structural class-property-specifierstructural becomes a second class-property-specifier,
next to final. It is a context-sensitive identifier, not a
keyword, and is recognized in exactly the same position and by the same
disambiguation rule as final. The two can be combined, in
either order.
Its only effect is on the definition of structural type. A class marked
structural is exempt from the requirement that its own bases
and non-static data members be public. Nothing else changes:
mutable.structural. Marking a
derived class doesn't exempt its bases.Template-argument-equivalence ([temp.type]) is already defined subobject by subobject, without regard to access, so it needs no change. Two values of a structural class type are template-argument-equivalent exactly when their corresponding subobjects are, just as for a struct with public members. Likewise, the Reflection facilities that require structural types work with such classes unchanged.
A union can be marked structural too. A union can have
private members, although an anonymous union can't, and a union whose
members are all public and of structural type is already a structural type
today. Marking a union structural works exactly as it does for
a class: the public-access requirement is lifted, and everything else still
applies. Template-argument-equivalence for a union is already defined by
its active member ([temp.type]), and that doesn't change.
Marking a class structural when it doesn't meet the remaining
requirements makes the class definition ill-formed. The author has stated
an intent, and a class that silently isn't structural after all would only
be found out later, at some distant use, with a less helpful diagnostic.
For a class template, each specialization is checked when it is
instantiated. So Box<int> is fine, while
Box<T> for a non-structural T is diagnosed
at the point of instantiation.
This paper deliberately doesn't propose a conditional form, along the
lines of structural(constant-expression), for wrapper
templates that want to be structural exactly when their contents are. That
is left intentionally as a possible future extension, if it turns out to be
needed. Adding it later would be a pure extension: plain
structural would simply mean structural(true), so
no code written against this proposal would change meaning.
structural means for the class authorBy writing structural, the class author is promising that
the values of the class's subobjects are its value, as far as template
argument identity is concerned. Two objects whose subobjects are
template-argument-equivalent are the same template argument. The template
parameter object is copy-initialized from the template argument
([temp.arg.nontype]), so the class's copy constructor maintains its
invariants, as it does for any other copy. That is the same promise a
struct with public members makes today,
just without having to expose the members to everyone. For classes like
span and string_view, which are a pointer and a
length, that is exactly right.
The promise is not right for every class. It doesn't hold for a class
in which equal values can have different representations, such as a
fraction that is reduced to lowest terms only lazily, or for one whose
storage within its lifetime holds data that isn't part of the value, such
as a cached hash. Those classes need more than member-wise identity, and
simply shouldn't be marked structural; see "What this
proposal doesn't do" below.
Many classes that look like they need more are fine, though. A class that normalizes its value on construction, such as a fraction kept in lowest terms by its constructor, has equal subobjects for equal values, so member-wise identity is exactly value identity. Storage that isn't part of the value is also fine if it consists of inactive union elemental subobjects ([intro.object]), which template-argument-equivalence ignores ([temp.type]): for example, a small-string buffer held in a union, whose elements past the string's length are never brought within their lifetime.
An alternative would be to treat a class with private data as structural automatically, whenever its members happen to meet the other requirements. The author of this paper firmly thinks that heuristics are just bad for determining intent; just because private data looks structural doesn't mean that it is structural semantically. Such heuristics would be nothing more than guesswork, and incorrect guesses could have unpredictable results. The opt-in context-sensitive keyword is considered a very clear expression of intent, with no such guesswork problems.
Some might think it's not good language design to have this shortcut and the more elaborate solution proposed in P4340, and have users decide which to use, and some might not like yet-another context-sensitive keyword. There's no objective fact that this paper can state either way; the author prefers having the context-sensitive keyword forever over having to worry about C++26 programs having to use de-encapsulated types for reflection, and staying that way forever.
Another alternative would be to use an attribute. That has some attractive properties; older language versions would just not make the type structural, and there are no class-type constant template parameters in languages older than C++20 anyway. Here the author prefers a strong statement of semantic intent, and if that requires macroing away a context-sensitive keyword not recognized by an older language, that seems better than having an attribute with all of its maybe-semantic-maybe-not properties. The preference isn't strong, and doesn't have an objective fact rationale. If the committee says with strong preference that an attribute is how we should do this, the author will accept that and regret it later, but will live with it. The author does see this as a semantic property stronger than what attributes seem to be expressing, though. Furthermore, ignoring the attribute in the current language version would make otherwise-valid uses of values of such types as constant template arguments ill-formed, and that's not what ignoring an attribute should result in.
std::span and std::basic_string_viewThis paper proposes marking std::span and
std::basic_string_view structural. Both are
already, conceptually, a pointer and a size. With this change they can be
used as constant template parameters and, more importantly, as members of
the encapsulated classes that Reflection code produces. Template argument
identity for them is pointer identity plus size, not content equality,
which is the same as what a defaulted customization would give under
P4340R0.
Implementations need some latitude here. A library may store the
extent of a span in a private helper type, which then also
needs to be structural. libstdc++ does this; the implementation marks its helper
structural as well. The library wording should say that these
types are structural, rather than exactly how the implementation achieves
that.
std::define_static_string returns a basic_string_viewThis paper proposes changing the return type of
std::define_static_string from
const ranges::range_value_t<R>* to
basic_string_view<ranges::range_value_t<R>>. The
promoted storage is unchanged: it is still the null-terminated array that
std::meta::reflect_constant_string produces, so
.data() is still a pointer to a null-terminated string with
static storage duration. The view's length is that of the null-terminated
string, so it stops at the first null character. Embedded null characters
don't need any special treatment: the current const CharT*
result can't represent them either, because callers can only ever use it
as a null-terminated string. This also matches the usual convention for
string literals.
With string_view structural, the result can be used directly
as a constant template argument:
template<std::string_view S> struct X { /* ... */ };
X<std::define_static_string(name)> x; // OK
template<const char* P> struct Y { /* ... */ };
Y<std::define_static_string(name).data()> y; // the pointer, if needed
This is a breaking change against C++26 as specified: code that stores
the result in a const CharT*, or passes it to a pointer
template parameter, needs .data(). Code that initializes a
string_view from the result, as the example above does, is
unaffected. In the implementation, four existing call sites needed
.data() added: three tests in GCC's reflection test suite and
one use inside libstdc++. Because define_static_string is new
in C++26, there is little existing code to break, and the window for making
the change cheaply is now.
This paper proposes making this change for C++26, together with the rest of the proposal. Failing that, this paper proposes it for C++29, as a defect report against C++26. Note that this is strictly against the author's own policy of "Never Backport Features; that dilutes what a particular standard of the language means and makes it harder to cope with DR variance". The author grudgingly considers this an improvement important enough to violate that policy with a feature-backporting DR. The author considers not doing that feature-backporting worse.
P4340R0, "Extending constant
template parameter support by customizing
std::meta::reflect_constant", proposes that a class can opt
in to being a constant template parameter by declaring a
consteval member function reflect_constant. The
function returns a std::meta::info that reflects the
template parameter object for that value. The function can be
user-written or = default.
P4340R0 solves a larger problem than this paper: normalization that
isn't done on construction, values whose representation contains non-value
data within its lifetime, string literals as
template arguments, and a path towards std::vector and
std::string once non-transient allocation exists. This paper
considers that work valuable. This paper's claim is narrower: the problem that actually
blocks Reflection users today (encapsulated data, and
string_view/span members) doesn't need any of
that machinery. A one-word opt-in solves it, and that gives far more bang
for the buck:
consteval auto reflect_constant() const -> std::meta::info = default;which must be written inside every class that wants it, names a Reflection type in the class's interface, and is a special member function with its own defaulting and deletion rules. The proposed spelling is one word in the class head:
class Widget structural { ... };
For the non-defaulted case, P4340R0 requires the class author to write
constant-evaluation code using substitute,
reflect_constant and a helper variable template. The author
must also uphold several new semantic requirements: the result must have
static storage duration, have the same linkage as the class, have type
T const, and the function must be deterministic and
idempotent. None of that is needed for the cases this paper targets.structural doesn't introduce
a customization point, a new kind of special member function, or new
template-argument-equivalence rules. It reuses the existing, well-understood
C++20 definition of structural type and template-argument-equivalence, and
only lifts the public-only restriction for classes that ask for it. P4340R0
changes how template-argument-equivalence is determined for class types.
That is a deeper change, and it has to be integrated with mangling, linkage
and ODR.structural and it
behaves like the equivalent struct with public members" is a one-sentence
explanation, and users already understand the struct case.final), one flag on the class,
its propagation through template instantiation and type variants, and the
one function that decides whether a type is structural. It amounts to
roughly sixty lines of compiler changes, plus marking two library types.reflect_constant and
structural produce the same thing: member-wise template
parameter objects and member-wise equivalence. That is the case nearly
every library type in P4340R0's list falls into, as do the
Reflection-generated data structures shown above. structural
handles it without the class mentioning Reflection at all.The two proposals compose. If P4340R0, or something like it, is adopted
later for the harder cases, structural remains the concise
spelling for the common case. It could even be specified as being
equivalent to a defaulted customization. Nothing in this proposal closes
the door on normalization or custom serialization.
Adopting a P4340R0-style customization later, on top of this proposal,
is not by itself an ABI break. A constant template argument of class type
is mangled from the values of its subobjects, whether they are public or
not. A structural class, and a class with a defaulted
customization under P4340R0, both get member-wise template parameter
objects and member-wise template-argument-equivalence. Their
specializations therefore keep the same mangled names. P4340R0 adds rules
only for types that customize, so classes that stay structural
are unaffected.
Changing the template-argument identity of an existing type is an ABI
break, by whichever mechanism and on top of whichever proposal. For
example, suppose std::string_view later gained a customization
that compares contents instead of pointer and length. Specializations for
views with equal contents but different pointers, currently distinct, would
merge. The mangled names of every specialization with a
string_view argument would change, and so would the
.data() of the template parameter object. The same break would
follow from starting with P4340R0's defaulted customization for
string_view and changing it later.
The identity of std::string_view and std::span
as template arguments is therefore decided when they first become usable as
constant template arguments, by this paper or any other. Marking them
structural fixes their identity as pointer plus length.
P4340R0 makes the same commitment: it proposes defaulted customizations for
std::string_view and std::span, which give them
exactly this member-wise, pointer-plus-length identity, and it argues for
keeping string_view distinct from std::string,
which compares contents. On this point the two papers don't differ. For the
strings that Reflection code produces, the two notions of identity
coincide: the result of define_static_string refers to a
template parameter object, and no two template parameter objects have
template-argument-equivalent values ([temp.param]). So equal strings
promoted this way share storage and are the same template argument. Pointer
identity and content identity disagree only for views into other storage,
such as a substring of a larger static array.
Regardless of practical uses, real or imagined, a
string_view doesn't own the string, so the contents are not
its value. Furthermore, due to the existing reality of
extern buffers, the contents may not even be visible in the
translation unit that uses the view as a template argument:
extern const char key[16]; // defined, and possibly changed, in another TU
template<std::string_view K> struct Cipher { /* ... */ };
using C = Cipher<std::string_view(key, sizeof key)>; // OK
This is a valid constant template argument, and the specialization is
identified by the buffer's address and the length alone. Under content
identity, the identity of C would depend on characters that
this translation unit can't see, and that could change by recompiling a
different translation unit.
structural isn't intended as a complete solution for every
use of constant template parameters of class type. In particular, it
doesn't provide:
std::string and std::vector.Types that need these shouldn't be marked structural, and
remain candidates for a P4340R0-style mechanism.
The library part of this proposal is also intentionally limited to the
bare minimum needed to fix Reflection: marking std::span and
std::basic_string_view structural, and changing
the return type of std::define_static_string. This paper
doesn't propose marking any other library types, such as
std::optional, std::expected,
std::tuple, std::reference_wrapper or the
<chrono> types. Whether and how to mark them can be
decided separately, type by type, once the language feature is in place.
Keeping the library scope minimal keeps this a small fix that can be
applied to C++26.
The proposal is implemented in
a GCC branch, available on Compiler
Explorer as the "x86-64 gcc (P4324 contracts, and profiles)" compiler,
under
-std=c++26 -freflection. The example above compiles and runs there. The
implementation also marks std::span (and its internal extent
storage) and std::basic_string_view structural
in libstdc++. The tests cover:
define_static_array of encapsulated classes, of
spans of spans, and of classes holding
string_views, read back by run-time code;structural, including
template-argument-equivalence by active member;structural remains an ordinary identifier before C++26.The implementation also changes std::define_static_string
to return a std::basic_string_view. Its tests cover the
result's length and contents, identity of the underlying storage via
.data(), use of the result as a string_view
constant template argument, and the null-character truncation rule.
The wording is relative to the current working draft. It is an initial attempt.
Modify the grammar in [class.pre]/1 as follows:
class-property-specifier:
final
structural
Modify [class.pre]/5 as follows:
Each class-property-specifier shall appear at most once within a single class-property-specifier-seq. Whenever a class-key is followed by a class-head-name,
the identifiera sequence of one or more identifiers that could be class-property-specifiers, and a colon or left brace,finalthe identifier iseach identifier in that sequence is interpreted as a class-property-specifier.[Example 2:
struct A; struct A final {}; // OK, definition of struct A, // not value-initialization of variable final struct X { struct C { constexpr operator int() { return 5; } }; struct B final : C{}; // OK, definition of nested class B, // not declaration of a bit-field member final }; class S structural { // OK, definition of class S int i; }; struct T final structural : X {}; // OK, both identifiers are class-property-specifiers— end example]
Add a new paragraph after [class.pre]/6:
If a class is marked with the class-property-specifier
structuraland that class is not a structural type ([temp.param]), the program is ill-formed. [Note: A class markedstructuralcan be a structural type even if it has non-public base classes or non-public non-static data members ([temp.param]). — end note]
Modify [temp.param]/12 as follows:
A structural type is one of the following:
- (12.1) a scalar type, or
- (12.2) an lvalue reference type, or
- (12.3) a literal class type with the following properties:
- (12.3.1) all base classes and non-static data members are
public and non-mutablenon-mutableand, unless the class is marked with the class-property-specifierstructural([class.pre]), public and- (12.3.2) the types of all base classes and non-static data members are structural types or (possibly multidimensional) arrays thereof.
[Example:
class P structural { int v; public: constexpr P(int i) : v(i) {} }; class Q { int v; public: constexpr Q(int i) : v(i) {} }; template<P> struct A {}; // OK, P is a structural type template<Q> struct B {}; // error: Q is not a structural type class R structural { // error: R is marked structural but is not a structural type, Q q; // because Q is not a structural type };— end example]
Modify the class template definition of basic_string_view
in [string.view.template.general]:
template<class charT, class traits = char_traits<charT>>
class basic_string_view structural {
Modify the class template definition of span in
[views.span.overview]:
template<class ElementType, size_t Extent = dynamic_extent>
class span structural {
Add to [views.span.overview] and [string.view.template.general], respectively:
span<ElementType, Extent>is a structural type ([temp.param]).
basic_string_view<charT, traits>is a structural type ([temp.param]).
[Drafting note: The exposition-only data members of
span and basic_string_view are of pointer and
size_t type, which are structural, so the class-head marking
alone makes the specification consistent. The added sentences are there to
make it clear to implementations that any internal helper types must also
be structural. — end drafting note]
Modify the declaration of define_static_string in the
header <meta> synopsis [meta.syn] as follows:
template<ranges::input_range R> constevalconst ranges::range_value_t<R>*basic_string_view<ranges::range_value_t<R>> define_static_string(R&& r);
Modify [meta.define.static]/15 and the declaration preceding it as follows:
template<ranges::input_range R> constevalconst ranges::range_value_t<R>*basic_string_view<ranges::range_value_t<R>> define_static_string(R&& r);Effects: Equivalent to:
return basic_string_view<ranges::range_value_t<R>>(meta::extract<const ranges::range_value_t<R>*>(meta::reflect_constant_string(r)));
Modify the entry for __cpp_nontype_template_args in
[cpp.predefined] as follows, where 20XXXXL is replaced by the year and month
in which this proposal is adopted:
__cpp_nontype_template_args201911L20XXXXL
Modify the following entries in [version.syn], with 20XXXXL as above:
#define __cpp_lib_define_static202506L20XXXXL // freestanding, also in <meta> #define __cpp_lib_span202311L20XXXXL // freestanding, also in <span> #define __cpp_lib_string_view202403L20XXXXL // also in <string>, <string_view>
[Drafting note: __cpp_nontype_template_args
covers the language change, since a class marked structural
can be the type of a constant template parameter.
__cpp_lib_define_static covers the new return type of
define_static_string, and __cpp_lib_span and
__cpp_lib_string_view cover those types being structural.
__cpp_lib_is_structural is unchanged: the trait's
specification doesn't change, only the set of types it is true for.
— end drafting note]