Document #: | P3385R2 |
Date: | 2024-12-12 |
Project: | Programming Language C++ |
Audience: |
sg7 |
Reply-to: |
Aurelien Cassagnes <[email protected]> Roman Khoroshikh <[email protected]> Anders Johansson <[email protected]> |
Since [P3385R1]
Since [P3385R0]
info
equal operatorAttributes are used to a great extent, and there likely will be new
attributes added as the language evolves.
As reflection makes its way into our standard, what is missing is a way
for generic code to look into the attributes appertaining to an entity.
That is what this proposal aims to tackle by introducing the building
blocks.
We expect a number of applications for attribute introspection to happen
in the context of code injection [P2237R0], where, for example, one may
want to skip over [[deprecated]]
members. The following example demonstrates skipping over deprecated
members:
struct User {
[[deprecated]] std::string name;
[[deprecated]] std::string country;
::string uuidv5;
std::string countryIsoCode;
std};
consteval std::vector<info> selectNonDeprecated() {
::vector<info> liveMembers;
stdconstexpr auto attributes = attributes_of(^^[[deprecated]]);
auto keepLive = [&] (info r) {
if (!std::ranges::any_of(
(^^r),
attributes_of[&] (auto meta) { meta == attributes[0]; }
)) {
.push_back(r);
liveMembers}
};
template for (constexpr auto member : members_of(^^User)) {
(member);
keepLive}
return liveMembers;
}
// Migrated user will no longer support deprecated fields
struct MigratedUser;
(^^MigratedUser, selectNonDeprecated());
define_aggregate
While collecting feedback on this draft, we were redirected to [P1887R1], a pre existing proposal. In this paper, the author discusses two topics: ‘user defined attributes’ (also in [P2565R0]) and reflection over said attributes. We believe these two topics need not be conflated; both have intrinsic values on their own. We aim to focus this discussion entirely on standard attributes reflection. Furthermore the earlier paper has not seen work following the progression of [P2996R7], and so we feel this proposal is in a good place to fill that gap.
Attributes are colloquially split into standard and non-standard.
This proposal wishes to limit itself to standard attributes (9.12
[dcl.attr]). We feel
that since it is up to the implementation to define how to handle
non-standard attributes, it would lead to obscure situations that we
don’t claim to tackle here.
A fairly simple (admittedly artificial) example can be built as such:
Given an implementation supporting a non-standard [[privacy::no_reflection]]
attributes that suppresses all reflection information appertaining to an
entity, we would have a hard time coming up with a self-consistent
system of rules to start with.
Henceforth, in this proposal, ‘attributes’, ‘standard attributes’ are all meant to be equivalent terms for attributes described by the standard.
There is a long standing and confusing discussion around the
ignorability of attributes. We’ll refer the reader to [P2552R3] for an at-length discussion of
this problem, and especially with regard to what ‘ignorability’ really
means. This proposal agrees with the discussion carried on there and in
[CWG2538]. We also feel that whether an
implementation decides to semantically ignore a standard attribute
should not matter.
Another interesting conversation takes place in [P3254R0], around the [[no_unique_address]]
case, which serves again to illustrate that the tension around so called
ignorability should not be considered a novel feature of this
proposal.
What matters more is the following set of rules that aim to discuss self-consistency
We put ourselves in the context of [P2996R7] for this proposal to be more illustrative in terms of what is being proposed.
We propose that attributes be a supported reflectable
property of the expression that is reflected upon. That means value of
type std::meta::info
should be able to represent an attribute in addition to the currently
supported set.
The current proposition for reflection operator grammar does not
cover attributes, i.e., the expression ^^[[deprecated]]
is ill-formed. Our proposal advocates to support expression as
following:
constexpr auto keepAttribute = ^^[[nodiscard]];
The resulting value is a reflection value embedding salient property of the attribute which are the token and the argument clause if any. If the attribute is not a standard attribute, the expression is ill-formed.
We propose that the syntax
[[ [: r :] ]]
be supported in any context where attributes are allowed.
[[ [: r :] ]]
produces a potentially empty
attribute-list
corresponding to the attributes found via reflection
r
. In effect every attributes that
are found via attributes_of(r)
are expanded in place inside [[ ]]
.Note that as it stands now
attribute-list
(9.12.1
[dcl.attr.grammar])
does not cover
alignas
. We
understand that this limits potential use of the current proposal but
also comes with difficulty, so this shall be discussed in a separate
paper.
A simple example of splicer using expansion statements is as follows.
We create an augmented
enum
introducing a begin
and
last
enumerator while preserving the
original attributes:
[[nodiscard]]
enum class ErrorCode {
warn,
fatal,};
[[ [: ^^ErrorCode :] ]]
enum class ClosedErrorCode {
begin,template for (constexpr auto e : enumerators_of(^^ErrorCode)) {
return [:e:],
}
last,};
If the attributes produced through introspection violate the rules of what attributes can appertain to what entity, the program is ill-formed, as usual.
It is worth pointing out the interaction between
attribute-using-prefix
and
splice expression that could lead to unexpected results, such as in the
following example
auto attribute = ^^[[nodiscard]];
[[ using CC: debug, [: attribute :] ]] enum class Code {};
While it is unlikely the user intends for the standard attributes to
be targeted by
using CC
,
current grammar says that the prefix applies to attributes as they are
found in the subsequent list. To remediate this we can either
enforce that
splice-name-qualifier
precedes
attribute-using-prefix
or
have those constructs be mutually exclusive as they occur in [[ ]]
.
To reduce the need to memorize unintuitive rules, we favor the later of
those options, as following:
auto attribute = ^^[[nodiscard]];
[[ [: attribute :] ]][[ using CC: debug ]] enum class Code {};
We propose to add two metafunctions to what has already been discussed in [P2996R7]. In addition, we will add support for attributes in the other metafunctions, when it makes sense.
namespace std::meta {
consteval auto attributes_of(info entity) -> vector<info>;
}
This would return a vector of reflections representing individual attributes that were appertaining to reflected upon entity.
namespace std::meta {
consteval auto is_attribute(info entity) -> bool;
}
This would return true if the
entity
reflection represents a [ [ attribute ] ]
such as described in [dcl.attr], otherwise, it would return false.
Given a reflection r
designating
a standard attribute, identifier_of(r)
(resp. u8identifier_of(r)
)
should return a string_view
(resp.
u8string_view
) corresponding to the
attribute-token
.
We do not think the leading
[[
and
closing ]]
are meaningful, besides, they contribute visual noise.
A sample follows
[[nodiscard]] int func();
constexpr auto nodiscard = attributes_of(^^func);
static_assert(identifier_of(nodiscard[0]) == identifier_of(^^[[nodiscard]]));
static_assert(identifier_of(nodiscard[0]) == "nodiscard"); // != "[[nodiscard]]"
Given a reflection r
that
designates an individual attribute, display_identifier_of(r)
(resp. u8display_identifier_of(r)
)
returns an unspecified non-empty
string_view
(resp.
u8string_view
). Implementations are
encouraged to produce text that is helpful in identifying the reflected
attribute for display purpose. In the preceding example we could imagine
printing [[nodiscard]]
instead of discard
as it might be
more striking for display purpose to the user.
As it stands now,
define_aggregate
allows piecewise
building of a class via
data_member_spec
. However, to
support attributes pertaining to those data members, we’ll need to
augment data_member_options
to
encode attributes we may want to attach to a data member.
The structure will change thusly:
namespace std::meta {
struct data_member_options {
struct name_type {
template <typename T> requires constructible_from<u8string, T>
consteval name_type(T &&);
template <typename T> requires constructible_from<string, T>
consteval name_type(T &&);
};
optional<name_type> name;
optional<int> alignment;
optional<int> bit_width;- bool no_unique_address = false;
+ vector<info> attributes;
};
}
From there building an aggregate piecewise proceeds as usual
struct Empty {};
struct [[nodiscard]] S;
(^^S, {
define_aggregate(^^int, {.name = "i"}),
data_member_spec(^^Empty, {.name = "e",
data_member_spec.attributes = {^^[[no_unique_address]]}})
});
// Equivalent to
// struct [[nodiscard]] S {
// int i;
// [[no_unique_address]] struct Empty { } e;
// };
Note here that [P2996R7] includes
no_unique_address
and
alignment
as part of
data_member_options
API. We think
that this approach scale poorly in that every new attributes introduced
into the standard will lead to discussions on whether or not they
deserve to be included in
data_member_options
. There is also
little explanations as to why those were picked among all. Attaching
attributes through above approach is more in line with the philosophy of
leveraging info
as the opaque
vehicle to carry every and all reflections.
We will not pursue this change here but in a follow-up paper, as we wish to keep the scope of change rather small in our proposal.
For any reflection where
is_attribute
returns
true
, other
metafunctions not listed above are not considered constant
expressions
We do not think it is necessary to introduce any additional query or
queries at this point. We would especially not recommend introducing a
dedicated query per attribute (e.g.,
is_deprecated
,
is_nouniqueaddress
, etc.). Having
said that, we feel those should be achievable via concepts, something
akin to:
auto attributes = attributes_of(^^[[deprecated]]);
template<class T>
concept IsDeprecated = std::ranges::any_of(
(^^T),
attributes_of[deprecatedAttribute] (auto meta) { meta == attributes[0]; }
);
Augment the description of std::meta::info
found in new paragraph 17.1 to add standard attribute as a valid
representation to the current enumerated list
17-1 A value of type std::meta::info is called areflection
. There exists a uniquenull reflection
; every other reflection is a representation of
…
- an attribute described in this document
- an implementation-defined construct not otherwise specified by this document.
Update 17.2
Recommended practices
17-2Recommended practice
: Implementations are discouraged from representing any constructs described by this document that are not explicitly enumerated in the list above (e.g., partial template specializations,attributes,placeholder types, statements).
Edit ^^
operator grammar to allow reflecting over an attribute
reflect-expression:
^^
::
^^
unqualified-id
^^
qualified-id
^^
type-id
^^
pack-index-expression^^ [ [
attribute] ]
Add the following paragraph at the bottom of 7.6.2.10
8 Areflect-expression
having the form^^ [[ attribute ]]
computes a reflection of the attribute 9.12 [dcl.attr]. If attribute is not described in 9.12 [dcl.attr], the behavior is implementation defined.
Update new paragraph between 7.6.10 [expr.eq]/5 and /6 to add a clause for comparing reflection of attributes and renumber accordingly
If both operands are of typestd::meta::info
, comparison is defined as follows:- (*.7) Otherwise, if one operand represents abase-specifier
, then they compare equal if and only if the other operand represents the samebase-specifier
.
- (*.) Otherwise if one operand represents an attribute described in this document, then they compare equal if and only if the other operand represents an attribute as well and both those attributes share the sameidentifier
.- (*.8) Otherwise, both operandsO1
andO2
represent descriptions of declarations of non-static data members: LetC1
andC2
be invented class types such that eachCk
has a single non-static data member having the properties described byOk
. The operands compare equal if and only if the data members ofC1
andC2
would be declared with the same type ortypedef-name
, name (if any),alignment-specifiers
(if any), width, and attributes
Change the grammar to allow splice-specifier
inside [ [ ] ]
…
attribute-specifier:
[ [ attribute-using-prefixopt attribute-list ] ][ [ splice-specifier ] ]
Modify the paragraph 5 to relax appertainance when an attribute is the operand of a reflection expression
5 Outside areflect-expression
, eachEachattribute-specifier-seq
is said to appertain to some entity or statement, identified by the syntactic context where it appears (Clause 8, Clause 9, 9.3). If anattribute-specifier-seq
that appertains to some entity or statement contains anattribute
oralignment-specifier
that is not allowed to apply to that entity or statement, the program is ill-formed. If anattribute-specifier-seq
appertains to a friend declaration (11.8.4), that declaration shall be a definition.
Modify the paragraph 7 to allow consecutive left square brackets following the reflection operator
7 Two consecutive left square bracket tokens shall appear only when introducing anattribute-specifier
or, within thebalanced-token-seq
of anattribute-argument-clause
or as an operand of areflect-expression
.
Add the following paragraph
8 If anattribute-specifier
contains asplice-specifier
, every attribute contained in that reflection appertain to the entity to which thatattribute-specifier
appertain.
[ Example 1:
struct [[nodiscard, maybe_unused]] Foo;
struct [[ [: ^^Foo :] ]] Bar; // same as struct [[nodiscard, maybe_unused]] Bar;
— end example ]
<
meta>
synopsisAdd to the [meta.reflection.queries] section from the synopsis, the
two metafunctions is_attribute
and
attributes_of
…
// [meta.reflection.queries], reflection queries
…consteval bool is_attribute(info r);
consteval vector<info> attributes_of(info r);
Update description of
has_identifier
return value to be
true
for
reflected attribute and renumber accordingly
Add a paragraph to identifier_of
to describe return value of reflected attribute
The attribute reflection features is guarded behind macro, augment 15.11 [cpp.predefined]
__cpp_impl_reflection 2024XXL
__cpp_impl_reflection_attributes 2024XXL
Poll: P3385R1: SG7 encourages more work on reflection of attributes as described in the paper.
Unanimous consent
Features proposed here were implemented on a public fork (off the Bloomberg P2996 branch) of Clang. Although it isn’t production ready code it demonstrates the feasability of the proposal.
Originally the idea of introducing a declattr(Expression)
keyword seemed the most straightforward approach to tackling this
problem. However based on feedback, the concern of introspecting on
expression attributes was a topic that belongs with the Reflection SG.
The current proposal shifted away from the original
declattr
idea to align better with
the reflection toolbox. Note also that, as we advocate here for [[ [: r :] ]]
to be supported, we recover the ease of use that we first envisioned
declattr
to have.
Discussion in https://eel.is/c++draft/dcl.attr#depend-2, imply that translation units need to carry their attributes unchanged to observe that rule.↩︎