Document #: | P3385R4 |
Date: | 2025-03-11 |
Project: | Programming Language C++ |
Audience: |
sg7, EWG, LEWG |
Reply-to: |
Aurelien Cassagnes <aurelien.cassagnes@gmail.com> |
Since [P3385R3]
Since [P3385R2]
Since [P3385R1]
Since [P3385R0]
info
equal operatorAttributes are used to a great extent, and there is new attributes
being added to the language somewhat regularly.
As reflection makes its way into our standard, we are missing 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::meta::info filterOutDeprecated() {
::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}
};
// Migrated user will no longer support deprecated fields
struct MigratedUser;
(^^MigratedUser, selectNonDeprecated());
define_aggregate
template for (constexpr auto member : members_of(^^User)) {
(member);
keepLive}
return ^^MigratedUser;
}
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 [P2996R8], and so we feel this proposal is in a good place to fill that gap.
Attributes are colloquially split into standard and non-standard. The
current wording in the standard states that attributes which are not
recognized are ignored, without being clear on what ignoring
means here. When we speak about reflection and attributes, what matter
is whether they are kept alive until semantic analysis. While this is
true for standard attributes, regardless of whether recommendations from
the standard are applied (eg. Warning on ignoring a
nodiscard
marked entity), it is less
clear whether that holds as well for non-standard ones. The proposal
does not ask for implementation to change their current scheme, how “non
standard attributes”, “reflection” and “ignorability” interact together
will be discussed further in following paragraph.
To summarize, this proposal (via wording) wishes to be prescriptive when it comes to standard attributes (9.12 [dcl.attr]) and permissive when it comes to non-standard.
Revisions of this proposal up to [P3385R2] were willfully ignoring
attribute-argument-clause, prescriptively so when it comes to
comparison (ie. ^^[[nodiscard("foo")]] == ^^[[nodiscard("bar")]]
).
Feedback post Wroclaw was unanimous on the need to support argument
clause. Feedback from implementers on this feature has been that while
it brings no concern for attributes like
nodiscard
, it was unrealistic in the
case of assume
that accepts an
arbitrary expression as argument.
Note that this is at this point a somewhat artificial concern as
there is no meaningful way to reflect on a null statement, which is what
assume
appertains to. The only way
to get a reflection of assume would be to explicitly construct one via
constexpr auto r = ^^[[assume(expr)]];
1. For any other entity, there is no
call to attributes_of
that could
return a reflection of assume
attribute.
Let us recap here what are the attributes 9.12 [dcl.attr] found in the standard and their argument clause
Attribute
|
Argument-clause
|
---|---|
assume | conditional-expression |
carries_dependency | N/A |
deprecated | unevaluated-string |
fallthrough | N/A |
indeterminate | N/A |
likely | N/A |
maybe_unused | N/A |
nodiscard | unevaluated-string |
noreturn | N/A |
no_unique_address | N/A |
unlikely | N/A |
Besides the assume
case, the
arguments are unproblematic.
On the other hand if we look at implementation specific attributes,
we’ll see that arguments can take arbitrary shape; without listing (all)
clang recognized attributes, we’ll share one example here
[[clang::availability(macos,introduced=10.4,deprecated=10.6,obsoleted=10.7)]] void f();
There is no standard way to express the shape of the arguments (list of tag = value where tags are from a predetermined set ?) to be supported here, short of treating those arguments clause as unevaluated soup of arbitrary tokens…
In the end, the current proposal aims to cover realistic use cases by rendering ill formed the reflection of attributes with conditional-expression argument clause
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. 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.
We claim that whether an implementation decides to semantically ignore a standard attribute (by not applying the recommended practices) does not matter in the context of this proposal. What does matter is that attributes and reflection interact in a self-consistent fashion. We propose the following rules/guidelines
[[ ]]
.About the first rule
For standard attribute, the first rule does not come into the
picture as they are not syntactically ignorable.
For implementation specific attributes 2, if
a particular implementation wishes to ignore this attribute then the
first rule says the implementation should treat this as an empty
attribute list.
About the second rule
For standard attribute, this rule simply enforce appertainance
rule as they are usually prescribed in the standard.
For implementation specific attributes, this rule means that if
an implementation choses to ignore a particular attribute, splicing a
reflection of that attribute will trigger the same ignorability
trap.
We put ourselves in the context of [P2996R8] 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 attribute argument clause if any. If the attribute is not a standard attribute, as per our earlier discussion, an implementation is free to either compute that reflection or to treat this as an empty list of attribute.
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
end
enumerator while preserving the
original attributes:
enum class [[nodiscard]] ErrorCode {
warn,
fatal,};
enum class [[ [: ^^ErrorCode :] ]] ClosedErrorCode {
begin,template for (constexpr auto e : enumerators_of(^^ErrorCode)) {
return [:e:],
}
end,};
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]];
enum class [[ using CC: debug, [: attribute :] ]] 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]];
enum class [[ [: attribute :] ]][[ using CC: debug ]] Code {};
We propose to add two metafunctions to what has already been discussed in [P2996R8]. 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_string_of(r)
(resp. u8display_string_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 nodiscard
as it might be
better fitted for log extraction.
As it stands now,
define_aggregate
allows piecewise
building of a class via
data_member_spec
. However, to
support arbitrary 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 [P2996R8] includes
no_unique_address
and
alignment
as part of
data_member_options
API. We think
that this approach scale awkwardly in that every new attributes
introduced into the standard will lead to discussions on whether or not
they should be included in
data_member_options
. Attaching
attributes through the above proposed 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 in this proposal but in a follow-up paper, as we wish to keep the scope of change rather small.
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[attributes] (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
- a data member description (11.4.1 [class.mem.general]).
Update 17.2 Recommended practices
to remove attributes from the list
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 for attribute reflection
reflect-expression:
^^
::
^^
unqualified-id
^^
qualified-id
^^
type-id
^^
pack-index-expression^^ [[
attribute]]
Add the following paragraph at the bottom of 7.6.2.10 to describe: 1)
the reflection of attributes, 2) what implementation shall yield when
reflecting unsupported attributes, 3) forbidding the reflection of [[assume(conditional-expression)]]
(11.1) Areflect-expression
of the form^^[[ attribute ]]
computes a reflection of the attribute 9.12 [dcl.attr]. For an attribute-token not described in this document, an implementation ignoring such_attribute-token_
shall return a computed value equal to^^[[ ]]
.
(11.2) For an attributer
described in this document whoseattribute-argument-clause
is present and is aconditional-expression
, computing the reflection ofr
is ill-formed.
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:- (5+.5) Otherwise, if one operand represents a direct base class relationship, then they compare equal if and only if the other operand represents the same direct base class relationship.- (5+.*) Otherwise if one operand represents an attribute, then they compare equal if and only if the other operand represents an attribute, both those attributes have the sameattribute-token
, and if present, bothattribute-argument-clause
are equal.[ Example:— end example ]static_assert(^^[[nodiscard]] == ^^[[nodiscard]]); static_assert(^^[[nodiscard("keep me")]] == ^^[[nodiscard("keep me")]]); static_assert(^^[[nodiscard("keep me")]] != ^^[[nodiscard("keep me too")]]); static_assert(^^[[nodiscard("keep me")]] != ^^[[nodiscard]]); static_assert(^^[[nodiscard]] != ^^[[deprecated]]);
- (5+.6) Otherwise, both operandsO1
andO2
represent data member descriptions. The operands compare equal if and only if the data member descriptions represented byO1
andO2
compare equal (11.4.1 [class.mem.general]).
Change the grammar to allow asplice-specifier
inside [[ ]]
…
attribute-specifier:
[ [ attribute-using-prefixopt attribute-list ] ][ [ splice-specifier ] ]
Modify the paragraph 5 to relax appertainance rules 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 when computing the reflection of an attribute the operandreflect-expression
(7.6.2.10 [expr.reflect]).
Add the following paragraph to describe the process of resolving the
entity identified by the splice specifier found inside [[ ]]
,
and extracting attributes from the reflected entity
8 For anattribute-specifier
containing asplice-specifier
- The constructS
designated by thesplice-specifier
and the referred to expression is matched as described per [expr.prim.splice] 7.5.8-2
- If the expressionS
found is not ill-formed, every attributes appertaining toS
is now considered appertaining to the entity the originalattribute-specifier
appertains to.[ Example:— end example ]struct [[nodiscard("keep me"), maybe_unused]] Foo; struct [[ [: ^^Foo :] ]] Bar; // same as "struct [[nodiscard("keep me"), maybe_unused]] Bar;"
<
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
,
u8identifier_of
to describe return
value of reflected attribute
The attribute reflection feature is guarded behind macro, augment 15.11 [cpp.predefined]
__cpp_impl_reflection 2025XXL
__cpp_impl_reflection_attributes 2025XXL
Most of the features presented here are available on a branch 3 that is being PR to the Bloomberg P2996 Clang implementation. Most of the challenges met so far were about recovering arguments for attributes at splice time. After an attribute becomes semantic, there is not an unified way to go back to their syntactic forms and from there, extract arguments. Our strategy (hack) was to add a backlink from semantic to syntactic form of attributes, and extend their lifetime so the links remain available.
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.
Oddly enough this is what reflection of a null statement decorated by this attribute would look like, if that was possible.↩︎
Note that we are here going straight to the subset of non standard attributes that matter the most, implementation specific attributes. User defined arbitrary attributes are best addressed via the annotation proposal.↩︎
https://github.com/zebullax/clang-p2996/tree/p3385↩︎