Document #: | P3385R6 [Latest] [Status] |
Date: | 2025-09-26 |
Project: | Programming Language C++ |
Audience: |
EWG, LEWG |
Reply-to: |
Aurelien Cassagnes <acassagnes@bloomberg.net> |
R6
appertain
has_attribute
metafunctionR5
appertain
metafunctionAttributes 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 generation [P2237R0], where for example, one may
want to skip over [[deprecated]]
members, explicitly tag python bindings with
@deprecated
decorators, etc.
The following example demonstrates cloning an aggregate while leaving
out all deprecated members:
constexpr auto ctx = std::meta::access_context::current();
struct User {
[[deprecated]] std::string name;
[[deprecated]] std::string country;
::string uuidv5;
std::string countryIsoCode;
std};
template<class T>
struct MigratedT {
struct impl;
consteval {
::vector<std::meta::info> migratedMembers = {};
stdfor (auto member : nonstatic_data_members_of(^^T, ctx)) {
if (!std::meta::has_attribute(member, ^^[[deprecated]])) {
.push_back(data_member_spec(
migratedMembers::meta::type_of(member),
std{.name = std::meta::identifier_of(member)}
));
}
}
(^^impl, migratedMembers);
define_aggregate}
};
using MigratedUser = MigratedT<User>::impl;
static_assert(std::meta::nonstatic_data_members_of(^^User, ctx).size() == 4);
static_assert(std::meta::nonstatic_data_members_of(^^MigratedUser, ctx).size() == 2);
int main() {
MigratedUser newUser;// Uncomment the following line to show the deprecated fields are gone
// newUser.name = "bob";
//
// error: no member named 'name' in 'MigratedT<User>::impl'
// 142 | newUser.name = "bob";
//
.uuidv5 = "bob";
newUser}
[link].
A more fundamental motivation shows up when looking at
define_aggregate
design
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 &&);
};
<name_type> name;
optional<int> alignment;
optional<int> bit_width;
optionalbool no_unique_address = false;
};
}
Here we have 2 attributes showing up in
alignas
and
[[no_unique_adress]]
.
We have no way to appertain [[deprecated]]
,
[[maybe_unused]]
,
or any other attributes.
If one wants to explicitly tell their compiler to enforce this
attribute, they may want to tag [[msvc::no_unique_address]]
out of caution.
Having a vehicle to solve this design concern is a major motivation for
this paper.
Before longer discussions, we can give a quick rundown of what the initial support is expected to be
Category
|
Sample
|
Rationale
|
---|---|---|
🟢 Standard | [[nodiscard]] |
|
🟢 Vendor variant | [[clang::warn_unused_result]] |
|
🟢 Vendor specific | [[gnu::constructor]] |
|
🟠 Vendor specific | [[clang::availability]] |
Complexity |
🔴 Unknown | [[my::attribute(freeform arg)]] |
Undefined parsing |
Complexity here refers to our implementation experience dealing with positional arguments. Experienced implementers may feel differently, what remains true is that we want to leave the choice to implementers to opt out for problematic attributes.
Let us recap here what are the attributes 9.13 [dcl.attr] found in the standard and their argument clause
Attribute
|
Argument-clause
|
---|---|
assume | conditional-expression |
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 |
Revisions of this proposal up to [P3385R2] were willfully ignoring
attribute-argument-clause when it comes to comparison. Feedback
post Wroclaw was unanimous on treating the argument clause as a salient
property (and so starting from the second revision ^^[[nodiscard("foo")]] != ^^[[nodiscard("bar")]]
).
Feedback from implementers has been that while it brings no concern for
attributes like nodiscard
, it is
more an open question 1 when it comes to an attribute like
assume
accepting an expression as
argument… Should ^^[[assume(i + 1)]]
compare equal to ^^[[assume(1 + i)]])
?
Ultimately we leave to implementations the freedom to define the
comparisons of the argument clause for
assume
. To keep things simple our
experimental implementation does not transform the expression into a
canonical representation before evaluating equality via profiling and so
^^[[assume(i + 1)]]
is not the same as ^^[[assume(1 + i)]])
.
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, and around [[assume]]
in CWG
3038, all of which serves again to illustrate that the tension
around so called ignorability should not be considered a novel feature
of this proposal.
Here we introduce an alternative terminology to guide the conversation in the context of reflection and attributes
[[my::attribute]]
).[[clang::availability(macos,introduced=10.4,deprecated=10.6,obsoleted=10.7)]]
)[[nodiscard]]
,
[[gnu::constructor(100)]]
)With this category in place, we make the following design choice
attributes_of
does not return
unsupported attributesThis is done to allow implementers plenty room to grow the set of
supported attributes w/o worrying about breaking code. The current
practice around creating reflection of problematic constructs (such as
using-declarator
)
is to be ill-formed and we’ll follow that here. Diagnostic in those
cicrumstances are not hard to emit, and so we should do so.
The other alternative would be to yield a null reflection and emit a
warning, which we think is a worse option.
Our proposal advocates to support reflect expression like
constexpr auto r = ^^[[nodiscard("keepMe")]];
The result is a reflection value embedding salient property of the attribute which are the attribute namespace, token and the argument clause if any.
We propose to add a couple of metafunctions to what is available in
<meta>
.
In addition, we will extend support to attributes in the other
metafunctions when it makes sense.
namespace std::meta {
consteval auto attributes_of(info construct) -> vector<info>;
}
attributes_of()
returns a vector of reflections representing all individual attributes
that appertain to construct
.
Simple example follows
enum class [[nodiscard("Error discarded")]] ErrorCode {
Disconnected,
ConfigurationIncorrect,
OutdatedCredentials,};
static_assert(attributes_of(^^ErrorCode)[0] == ^^[[nodiscard("Error discarded")]]);
In the case where an entity is legally redeclared with different
attribute arguments, attribute_of
return one of those.
enum class ErrorCode;
enum class [[nodiscard("Error discarded")]] ErrorCode;
enum class [[nodiscard]] ErrorCode {
Disconnected,
ConfigurationIncorrect,
OutdatedCredentials,};
// Either of [[nodiscard("Error discarded")]] or [[nodiscard]]
static_assert(attributes_of(^^ErrorCode).size() == 1);
namespace std::meta {
enum class attribute_comparison {
// Namespace is ignored during the comparison
ignore_namespace, // Arguments are ignored during the comparison
ignore_argument, };
consteval auto has_attribute(info construct,
) -> bool;
info attribute
consteval auto has_attribute(info construct,
info attribute,) -> bool;
attribute_comparison policy}
has_attribute()
returns true if the specified
attribute
is found appertaining to
construct
, false otherwise.
Simple example follows
struct [[clang::consumable(unconsumed)]] F {
[[clang::callable_when(unconsumed)]] void f() {}
};
static_assert(std::meta::has_attribute(^^F::f, ^^[[clang::callable_when(unconsumed)]]));
[link]
The overload with policy
parameter allows a combo of flags to dictate what part of an attribute
are meaningful to the comparison. This comes in handy when we want to
find out an attribute, ignoring the vendor prefix and or the particular
message that is being attached here and there.
[[gnu::deprecated("Standard deprecated")]] void f() { }
// Ignore both the namespace and the argument
static_assert(std::meta::has_attribute(
^^f,
^^[[deprecated]],
::meta::attribute_comparison::ignore_namespace | std::meta::attribute_comparison::ignore_argument
std));
[link]
namespace std::meta {
consteval auto is_attribute(info r) -> bool;
}
is_attribute()
returns true if r
represents an
attribute, it returns false otherwise. Its use is trivial
static_assert(is_attribute(^^[[nodiscard]]));
Given a reflection r
designating
an attribute, identifier_of(r)
(resp. u8identifier_of(r)
)
should return a string_view
(resp.
u8string_view
) corresponding to the
attribute-token
.
A sample follows
static_assert(identifier_of(^^[[clang::warn_unused_result("message")]] == "clang::warn_unused_result")); // true
static_assert(identifier_of(^^[[nodiscard("message")]] == "nodiscard")); // true
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 [[clang::warn_unused_result("message")]]
as
it might be better fitted for diagnostics.
To support arbitrary attributes appertaining to data members, we’ll
need to augment data_member_options
to encode attributes we want to attach here.
The structure changes 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;
+ [[deprecated]] bool no_unique_address = false;
+ vector<info> attributes;
};
}
From there building an aggregate piecewise proceeds as usual
struct Empty{};
struct [[nodiscard]] S;
consteval {
(^^S, {
define_aggregate(^^int, {.name = "i"}),
data_member_spec(^^Empty, {.name = "e",
data_member_spec.attributes = {^^[[msvc::no_unique_address]]}})
});
}
// Equivalent to
// struct [[nodiscard]] S {
// int i;
// [[msvc::no_unique_address]] struct Empty { } e;
// };
Passing attributes through the above proposed approach is well in
line with the philosophy of leveraging
info
as the opaque vehicle to carry
every and all reflections.
Augment the description of std::meta::info
found in new paragraph 17 to add attribute as a valid representation to
the current enumerated list
17 A value of typestd::meta::info
is called a reflection. There exists a uniquenull reflection
; every other reflection is a representation of
…
17-20 - a direct base class relationship ([class.derived.general]), or,
17-21 - a data member description ([class.mem.general])., or
17-22 - an attribute([dcl.attr])
Update 17.18
Recommended practices
to remove
attributes from the list
17-2Recommended practice
: Implementations should not represent other constructs specified in this document, such asusing-declarators
, partial template specializations,attributes,placeholder types, statements, or expressions, as values of type std::meta::info.
Edit reflect-expression
production rule to support reflecting over attributes
reflect-expression:
^^
::
^^
reflection-name
^^
type-id
^^
id-expression^^ [[
attribute]]
Add the following paragraph after the last paragraph of
7.6.2.10
[expr.reflect] to
describe the new rule ^^ [[
attribute
]]
(11.1) Areflect-expression
of the form^^[[ attribute ]]
for attribute described in this document [dcl.attr], represents said attribute.
(11.2) For an attributer
non described in this document, computing the reflection ofr
is ill-formed absent implementation-defined guarantees with respect to said attribute .
Update 7.6.10 [expr.eq]/6 to add a clause for comparing reflection of attributes
- (6.8) represent identical attribute ([dcl.attr])[ Example:— end example ]static_assert(^^[[nodiscard]] == ^^[[nodiscard]]); static_assert(^^[[nodiscard("keep")]] == ^^[[nodiscard("keep")]]); static_assert(^^[[nodiscard]] != ^^[[deprecated]]); // different attribute token static_assert(^^[[nodiscard("keep")]] != ^^[[nodiscard("keep too")]]); // different argument clause static_assert(^^[[nodiscard("keep")]] != ^^[[nodiscard]]); // different argument clause
and they compare unequal otherwise.
Add a new paragraph to describe when are two standard attribute
considered identical. Two [[assume]]
attributes are always identical, for others we compare the attribute
tokens which must match, and their clause for simple clause.
9+ For any two attributesr1
andr2
described in this document,r1
andr2
are identical if theirattribute-token
are identical and
-attribute-token
isassume
, or
-r1
andr2
admit noattribute-argument-clause
, or
-r1
andr2
admit optionalattribute-argument-clause
and they are both empty or
-r1
andr2
admitattribute-argument-clause
of the form( unevaluated-string )
andr1
andr2
balanced-token-seqs
are identical.
Otherwiser1
andr2
are not identical.
(Note: Identity between attributes not described in this document is implementation defined)[ Example:— end example ][[nodiscard("A")]][[deprecated]] void f() {} [[nodiscard("A")]][[deprecated("B")]] void g() {} // the 'deprecated' attributes are not identical // the 'nodiscard' attributes in f() and g() are identical static_assert(^^[[gnu::constructor(2)]] == ^^[[gnu::constructor(1 + 1)]]); // implementation defined
Add a paragraph at the bottom to indicate that a dependent expression in an attribute does participate in SFINAE when that dependent expression is ill-formed for a given instantiation.
8+ Substitution into anattribute
is in the immediate context.[ Example:— end example ]template <class T> auto foo(T) -> decltype(^^[[assume(T::value == 0)]]); void foo(...); foo(1); // call the second overload
<
meta>
synopsisAdd to the [meta.reflection.queries] section from the synopsis, the
metafunctions is_attribute
,
attributes_of
and
has_attribute
along with the
attribute_comparison
enumeration.
... // [meta.reflection.queries], reflection queries ...
consteval bool is_attribute(info r); consteval vector<info> attributes_of(info r); enum class attribute_comparison { ignore_namespace, ignore_argument, }; consteval bool has_attribute(info r, info a); consteval bool has_attribute(info r, info a, attribute_comparison flags);
Introduce a subclause to
has_identifier
describing the return
value to be
true
for
attribute reflection
Introduce a subclause to
identifier_of
,
u8identifier_of
, describing the
return value of attribute reflection to be the attribute-token
.
Renumber the last clause appropriately.
Add the new clauses to support new metafunctions, and the new enumeration.
*consteval bool is_attribute(info r);
Returns:true
ifr
represents an attribute. Otherwise,false
.
*consteval vector<info> attributes_of(info r);
Returns: A vectorv
containing reflections of all attributes appertaining to the entity represented byr
, such thatis_attribute(vi)
is true for every attribute vi inv
. The ordering ofv
is unspecified.
[ Example:— end example ]enum class [[nodiscard, deprecated]] Result { Success, Failure }; static_assert(attributes_of(^^Result).size() == 2);
Add a new table to describe the comparison policy
attribute_comparison
between
attributes. Add a new clause to describe
has_attribute
enum class attribute_comparison { ignore_namespace = unspecified, ignore_argument = unspecified, };
*The type attribute_comparison is an implementation-defined bitmask type ([bitmask.types]). Setting its elements has the effect listed in Table (*) [tab:meta.reflection.queries]
attribute_comparison
effects [tab:meta.reflection.queries] Element Effect(s) if setignore_namespace
Specifies that the attribute-namespace
is ignored when comparing attributesignore_argument
Specifies that the attribute-argument-clause
is ignored when comparing attributes
*consteval bool has_attribute(info r, info a);
Returns: True ifa
was found appertaining to the constructr
.
Throws:meta::exception
unlessis_attribute(a)
istrue
.
*consteval bool has_attribute(info r, info a, attribute_comparison flags);
Returns: True ifa
was found appertaining to the constructr
. The bitmasks specified inflags
determine which components of an attribute are considered significant for matching purpose.
Throws:meta::exception
unlessis_attribute(a)
istrue
.
data_member_options
definition to mark the current
no_unique_address
member deprecated
and add the attributes
data
member.Returns:
description to accomodate for the new
attributes
data member.Effects:
to
describe the new attributes
data
member effects.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; + [[deprecated]] bool no_unique_address = false; + vector<info> attributes;
}; } ... Returns: A reflection of a data member description (T, N, A, W, NUA, AT) (11.4.1 [class.mem.general]) where (5.1) - T is the type represented by delias(type), (5.2) - N is either the identifier encoded by options.name or ⊥ if options.name does not contain a value, (5.3) - A is either the alignment value held by options.alignment or ⊥ if options.alignment does not contain a value, (5.4) - W is either the value held by options.bit_width or ⊥ if options.bit_width does not contain a value, and (5.5) - NUA is the value held by options.no_unique_address.
+ (5.6) - AT is the value held by options.attributes.
... Effects: Produces an injected declaration D ([expr.const]) that provides a definition for C with properties as follows: ... If oK.name does not contain a value, it is an unnamed bit-field. Otherwise, it is a non-static data member with an identifier determined by the character sequence encoded by u8identifier_of(rK) in UTF-8.
+ Every reflection r in ok.attributes is appertained to D if is_attribute(r) is true ...
The attribute reflection feature is guarded behind macro, augment 15.12 [cpp.predefined]
__cpp_impl_reflection 2025XXL
__cpp_impl_reflection_attributes 2025XXL
The features presented here are available on compiler explorer2.
It is mostly an academic question
since [[assume]]
can only appertain to the null statement, no calls to
attributes_of
could return such a
reflection. The only way to get one is to construct one explicitly via
constexpr auto r = ^^[[assume(expr)]];
and the utility of doing so is null.↩︎