Author: Aurelien Cassagnes
Document: P3385R7
Date: 2026-01-13
Target: SG7, EWG, LEWG
Link: wg21.link/p3385r7
Now that P2996 reflection has landed in C++26, the next question is obvious: what about attributes? P3385 proposes the building blocks for reflecting on C++ attributes at compile time - query what attributes appertain to an entity, compare them, and feed them into code generation via define_aggregate.
The core API is clean: ^^[[nodiscard]] gives you a reflection value, attributes_of(^^MyClass) returns all attributes on an entity, and has_attribute() lets you check for specific attributes with optional fuzzy matching via a bitmask (ignore namespace, ignore arguments). The motivating example filters out [[deprecated]] members when migrating a struct at compile time.
Scope is deliberately narrow. Standard attributes and well-behaved vendor attributes (like [[gnu::constructor(100)]]) are in. Unknown attributes and expression-argument attributes like [[assume]] are explicitly out - R7 drops [[assume]] support entirely and rebases the wording. Seven revisions in, this has been through the SG7 gauntlet with mixed signals from EWG at Sofia. The question isn't whether attribute reflection is useful - it's whether this particular slice is the right one to standardize.
P3385R7 - Attributes reflection
Author: Aurelien Cassagnes | Date: 2026-01-13 | Audience: SG7, EWG, LEWG
Paper link: wg21.link/p3385r7
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.
Reminder: paper authors sometimes read these threads. Critique the design, not the person.
Seven revisions. Seven. And along the way they removed
[[assume]]support entirely. The committee giveth and the committee taketh away.In fairness, reflecting on
assumeexpressions sounds like a headache even for compiler devs. The paper basically says "we can't agree on what equality means for expressions, so we're punting." Refreshingly honest for a standards paper.I've read the whole thing and I think the interesting tension in this paper isn't the API surface - it's the categorization system it creates. The paper establishes a three-tier hierarchy of attributes:
1. Standard attributes (fully supported, reflection guaranteed)
2. Vendor attributes (conditionally supported, up to the implementation)
3. Unknown attributes (unsupported, ill-formed to reflect)
And then it codifies this with two rules:
This means the reflection system now has a visible seam between "attributes the implementation knows about" and "attributes it doesn't." Before this paper, that seam was invisible to user code - the compiler silently ignored unknown attributes. After this paper, your metaprogram can observe the difference, because
attributes_ofwill return different results on different compilers depending on which vendor attributes they've opted into supporting.That's a defensible design choice - the alternative (reflecting unknown attributes as opaque tokens) would be much harder to specify. But it means that generic code using attribute reflection is inherently non-portable across implementations. The paper acknowledges this in section 3.2, but I think it deserves more attention than a paragraph. This is the kind of thing that bites library authors three years after the feature ships.
This is the right framing. And it gets worse when you add the
attribute_comparisonbitmask to the picture. You haveignore_namespaceandignore_argumentas flags, which means you can ask "does this entity have something that looks likedeprecatedregardless of vendor prefix?" But the answer depends on whether the implementation considers[[gnu::deprecated]]a supported attribute in the first place.So you have three axes of portability concern: which attributes exist, which the implementation supports for reflection, and which comparison flags you use. That's a lot of surface area for "I just want to know if a member is deprecated."
Exactly. And in practice every real codebase has a mix of
[[deprecated]]and[[gnu::deprecated("...")]]depending on which compiler warnings they were silencing that week. Theignore_namespace | ignore_argumentcombo is designed for exactly this case, but you have to know it exists and remember to use it. I'd bet most users will writehas_attribute(member, ^^[[deprecated]])and then file a bug when it misses the GNU-prefixed variant.Good analysis. The paper does address this in section 3.2 with the "self consistency" framing, but I agree the portability implications could use a dedicated subsection. Especially since the motivating example in section 2.1 uses only standard attributes, which sidesteps the problem entirely.
The godbolt link in the paper is the first time I've seen
^^[[in the wild and honestly it looks like line noise that gained sentience.laughs in template template parameter syntax
^^[[nodiscard("please")]]reads like a cat walked across the keyboard, hit enter, and it compiled.The change to
data_member_optionsis going to bite anyone already using P2996'sdefine_aggregate. The paper deprecatesno_unique_addressas a boolean field and replaces it with a general-purposevector<info> attributesmember. That's a cleaner design, sure, but every existing call site that has.no_unique_address = truenow gets a deprecation warning.What's the migration story here? The paper shows the replacement:
But that's a more verbose spelling for the same thing, and it means you need the attribute reflection machinery just to set a layout property. I get why it's better architecturally - you can now attach any attribute, not just the one they hard-coded - but the deprecation timing feels aggressive for a feature that just shipped.
The migration is straightforward though:
And the upside is real - you can now do
.attributes = {^^[[msvc::no_unique_address]]}for the MSVC variant, which was impossible before. The old boolean only gave you the standard attribute.Fair, the MSVC variant argument is compelling. I just wish they'd kept the boolean as a non-deprecated convenience alongside the new vector. Deprecation-then-removal for something that shipped in C++26 and gets replaced in C++29 is a tight timeline.
The
[[assume]]exclusion is the right call and the paper is honest about why. The core problem is expression identity:Canonicalizing arbitrary expressions is an unsolved problem in the general case. You either do token comparison (which gives surprising results -
i + 1!=1 + i) or you try to normalize (which is undecidable for complex expressions). The paper's experimental implementation uses token comparison, which works but would be a terrible thing to standardize.The practical impact is near zero anyway.
[[assume]]can only appertain to the null statement, soattributes_ofwould never return one. The only way to get an assume reflection is to construct one explicitly, and there's no use case for that.Edit: I should clarify - the Bloomberg Clang fork does have experimental support for expression-argument attributes using token comparison. It works fine in practice. The issue is purely about what semantics to bless in the standard.
Token comparison? So
^^[[assume(i + 1)]]!=^^[[assume(1 + i)]]? I can already hear the stackoverflow questions.Yes, and that's exactly why they're punting. Better to ship without it than to standardize surprising semantics that we're stuck with forever. The door stays open for a future paper once we have reflection of expressions worked out.
I'm more interested in when C++ will get user-defined attributes. The paper explicitly says unknown attributes are "unsupported" and reflecting on them is ill-formed. So we get to inspect the attributes the committee blesses, but anything from my own codebase is invisible to reflection. Every other language with a reflection system figured out that user annotations are the killer feature.
Rust has had proc macros and custom derive for years. You can
#[derive(Serialize)]and the macro has full access to your struct's attributes, fields, types. This paper is reflecting on[[nodiscard]]which is nice I guess but it's not even in the same ballpark.Every single thread. Without fail.
You're telling me r/wg21 has a Rust tangent? I'm shocked. Shocked.
[removed by moderator]
What did they say?
Something about how attributes are just "comments with extra steps" and we should stop pretending the committee cares about usability. You know, the usual.
The motivating example is actually compelling. Filtering out deprecated members at compile time:
Clean, readable, does what it says. But notice what's missing: this is read-only. You can inspect attributes and filter on them, but you can't create new attributes or programmatically attach attributes to entities that don't have them. The
define_aggregateextensions let you carry attributes forward, but there's noadd_attributeor equivalent.For the "migrate away from deprecated" use case that's fine. For the "generate Python bindings with
@deprecateddecorators" use case mentioned in the intro, you still need an external code generator to produce the Python side. The reflection helps you identify what to tag, but you're on your own for the tagging.Right, attribute creation is explicitly out of scope. The paper is read-only for attributes - introspection, not manipulation. Which is probably the right scope for a first paper, but it does limit the use cases.
Which loops back to my point. Read-only reflection on a fixed set of standard attributes is useful but limited. The moment you want to annotate your own types with domain-specific metadata - serialization hints, RPC tags, validation rules - you're back to the macro hacks we've been using for 20 years.
P2996 barely got into C++26 and we're already stacking extensions on top. Can we at least wait for implementations to stabilize before bolting on more features? Or is that too much to ask.
That's... literally how the standard process works. You ship the base feature, then you build on it in the next cycle. This paper is targeting C++29, not trying to squeeze into C++26. Would you rather they wait three years doing nothing and then ship a bigger paper that takes six years to review?
I mean yes? Modules shipped half-baked and we're still paying for it five years later. Coroutines shipped without library support and it took three more standards cycles to get
std::executionsomewhere usable. Maybe "ship the base and iterate" isn't the win people think it is when the base keeps moving under you.The committee poll history on this paper is worth reading carefully. SG7 gave unanimous encouragement on R1, but then the consensus fractured:
- Dec 2024: no consensus on reflecting full expression arguments (expected)
- Dec 2024: no consensus on forwarding target - C++26 or C++29 (interesting)
- Feb 2025: consensus to forward to EWG/LEWG as-is
- Sofia: EWG told them to drop
appertainand focus on queryingBut here's the one that matters: "EWG encourages more work on reflecting attributes in the direction of the paper" got a 1/14/12/3/2 split (SF/F/N/A/SA). That's fourteen in favor, twelve neutral, and five against. Not consensus. Fourteen neutral-to-negative votes on a directional question is the committee saying "we're not sure this is the right shape."
R7 responds by dropping
appertainand[[assume]]- which is what EWG asked for. The question is whether the slimmed-down version satisfies the skeptics or whether they wanted a fundamentally different approach.The author has indicated they'd rather not discuss P3385 at Croydon, so this is probably targeting a future telecon or Budapest. More time to iterate isn't necessarily bad given the EWG temperature.
Ah yes, my favorite C++ activity: reading a 20-page paper to find out that I still can't do what Java developers do with
@Deprecatedand a single line of reflection code. We're getting there though. Only two more standards cycles and a dozen more papers.At least Java's
@Deprecateddoesn't require you to understand bitmask comparison flags, conditional implementation support, and expression identity semantics. The C++ version of "is this deprecated" is a research paper.One use case the paper undersells: automatic Python/Lua binding generation. If you're already using P2996 to enumerate members and generate bindings at compile time, attribute reflection lets you skip deprecated members, respect
[[nodiscard]]in the binding wrapper, and carry[[maybe_unused]]through to suppress warnings in generated code. That's a real quality-of-life improvement over the current approach where binding generators either ignore attributes entirely or require a separate annotation mechanism.The
attributes_ofAPI is the right primitive for this. My concern is with the return type:vector<info>with unspecified ordering. For code generation you need deterministic output.This is a real problem. "Unspecified ordering" means the implementation is free to return attributes in any order, and it can change between compilations. For reproducible builds - which matter in embedded, safety-critical, and regulated environments - that's a non-starter if you're using
attributes_ofto drive code generation output.I'd push back slightly. The ordering of attributes in source code doesn't carry semantic meaning in C++.
[[nodiscard, deprecated]]and[[deprecated, nodiscard]]are identical. Makingattributes_ofreturn a source-ordered vector would create the false expectation that attribute order is meaningful, which it explicitly isn't.For reproducible codegen, you can sort the result yourself by
identifier_of. That's one line of code and it gives you a canonical ordering that doesn't depend on the source.I'm not asking for source-ordered or semantically-ordered. I'm asking for deterministic. If I call
attributes_ofon the same entity twice in the same translation unit, I need the same vector. "Unspecified" technically allows the implementation to return a different order each time. That's the gap.The sort-by-identifier workaround is fine, but it shouldn't be necessary. P2996's
members_ofreturns a deterministic order.attributes_ofshould do the same.Fair point on the within-TU guarantee. I'd concede that at minimum it should be deterministic within a translation unit. Every implementation will do that in practice anyway - the question is whether to spell it in the wording. Cross-TU consistency is a harder ask and probably not worth the constraint on implementers. But "deterministic within a TU" costs nothing to specify and removes a real footgun.
For embedded, vendor attributes are the ones that matter:
[[gnu::section("...")]],[[gnu::packed]],[[clang::no_sanitize("...")]]. The standard attributes are nice for library ergonomics, but the vendor ones control memory layout, linking, and codegen in ways that affect whether your firmware fits in flash.The paper makes vendor attributes "conditionally supported" which is the right design. But the practical question is whether GCC and Clang will actually opt in. If the major compilers don't support reflecting on their own vendor attributes, this feature is limited to checking
[[nodiscard]]and[[deprecated]]on types you already control. Useful, but not transformative.In the Bloomberg fork, we support a subset of Clang vendor attributes for reflection. The paper deliberately leaves this as implementation-defined so compilers can opt in at their own pace. My expectation is that the "easy" vendor attributes - the ones with no arguments or simple string/integer arguments - will get supported quickly. The complex ones like
clang::availabilitywith positional arguments will take longer."Conditionally supported" is the new SFINAE. Technically it works. Practically you need
#ifdefladders to find out.I for one welcome our new consteval overlords.
^^[[consteval_overlord]]- ill-formed, unsupported attribute. Even our overlords have limits.Genuine question: does anyone actually use
[[nodiscard]]with a message string? I've been writing C++ for 8 years and I've never seen[[nodiscard("reason")]]in production code. The entire argument-clause comparison machinery feels over-engineered for an argument type that nobody uses.We use it extensively.
[[nodiscard("check error code before proceeding")]]on every function returningerror_code. The message shows up in the compiler warning output, which means developers actually read it instead of just seeing "return value discarded" and adding(void). It's one of those features you don't miss until you have it.Every single
error_codereturn type in our codebase has it. The message is for the next person who reads the warning at 2 AM wondering whether they can safely ignore it. Spoiler: they can't.Slightly off topic but does anyone know if modules work with cmake 3.30 yet? I've been trying to get import std working with GCC 15 and it keeps failing with "unable to find module interface." I know this isn't the right thread but the cmake subreddit is dead.