P3385R7 - Attributes reflection WG21
Posted by u/reflection_watcher_26 · 10 hr. ago

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.

▲ 214 points (89% upvoted) · 47 comments
sorted by: best
u/AutoModerator 1 point 10 hr. ago pinned comment

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.

u/paper_trail_2019 1 point 10 hr. ago pinned comment

Reminder: paper authors sometimes read these threads. Critique the design, not the person.

u/compiles_first_try 178 points 9 hr. ago 🏆

Seven revisions. Seven. And along the way they removed [[assume]] support entirely. The committee giveth and the committee taketh away.

u/daily_cmake_victim 42 points 8 hr. ago

In fairness, reflecting on assume expressions 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.

u/axioms_all_the_way 97 points 8 hr. ago 🏆

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:

Creating reflection of unsupported attribute is ill-formed (diagnostic required)

attributes_of does not return unsupported attributes

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_of will 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.

u/sfinae_survivor 34 points 7 hr. ago

This is the right framing. And it gets worse when you add the attribute_comparison bitmask to the picture. You have ignore_namespace and ignore_argument as flags, which means you can ask "does this entity have something that looks like deprecated regardless 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."

u/axioms_all_the_way 21 points 6 hr. ago

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. The ignore_namespace | ignore_argument combo is designed for exactly this case, but you have to know it exists and remember to use it. I'd bet most users will write has_attribute(member, ^^[[deprecated]]) and then file a bug when it misses the GNU-prefixed variant.

u/reflection_watcher_26 12 points 7 hr. ago

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.

u/turbo_llama_9000 142 points 8 hr. ago 🏆

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.

u/compiles_first_try 89 points 7 hr. ago

laughs in template template parameter syntax

u/senior_dangling_ref 52 points 6 hr. ago

^^[[nodiscard("please")]] reads like a cat walked across the keyboard, hit enter, and it compiled.

u/enterprise_cxx_2021 62 points 7 hr. ago

The change to data_member_options is going to bite anyone already using P2996's define_aggregate. The paper deprecates no_unique_address as a boolean field and replaces it with a general-purpose vector<info> attributes member. That's a cleaner design, sure, but every existing call site that has .no_unique_address = true now gets a deprecation warning.

What's the migration story here? The paper shows the replacement:

.attributes = {^^[[no_unique_address]]}

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.

u/consteval_enjoyer 28 points 6 hr. ago

The migration is straightforward though:

// Before
data_member_spec(^^Empty, {.name = "e",
                           .no_unique_address = true})

// After
data_member_spec(^^Empty, {.name = "e",
                           .attributes = {^^[[no_unique_address]]}})

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.

u/enterprise_cxx_2021 15 points 5 hr. ago

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.

u/clang_adjacent 54 points 7 hr. ago

The [[assume]] exclusion is the right call and the paper is honest about why. The core problem is expression identity:

Should ^^[[assume(i + 1)]] compare equal to ^^[[assume(1 + i)]]?

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, so attributes_of would 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.

u/template_metaprogrammer_4k 19 points 6 hr. ago

Token comparison? So ^^[[assume(i + 1)]] != ^^[[assume(1 + i)]]? I can already hear the stackoverflow questions.

u/clang_adjacent 22 points 5 hr. ago

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.

u/just_want_annotations 89 points 6 hr. ago

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.

u/cpp_mass_migration_when 45 points 5 hr. ago

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.

u/yet_another_cpp_dev 67 points 5 hr. ago

Every single thread. Without fail.

u/daily_cmake_victim 38 points 4 hr. ago

You're telling me r/wg21 has a Rust tangent? I'm shocked. Shocked.

[deleted] 6 hr. ago

[removed by moderator]

u/UB_enjoyer_69 3 points 5 hr. ago

What did they say?

u/linker_errors_daily 7 points 5 hr. ago

Something about how attributes are just "comments with extra steps" and we should stop pretending the committee cares about usability. You know, the usual.

u/consteval_enjoyer 44 points 5 hr. ago

The motivating example is actually compelling. Filtering out deprecated members at compile time:

for (auto member : nonstatic_data_members_of(^^T, ctx)) {
  if (!has_attribute(member, ^^[[deprecated]])) {
    migratedMembers.push_back(
      data_member_spec(type_of(member),
        {.name = identifier_of(member)}));
  }
}

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_aggregate extensions let you carry attributes forward, but there's no add_attribute or equivalent.

For the "migrate away from deprecated" use case that's fine. For the "generate Python bindings with @deprecated decorators" 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.

u/reflection_watcher_26 12 points 4 hr. ago

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.

u/just_want_annotations 16 points 4 hr. ago

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.

u/coroutine_hater_2024 72 points 5 hr. ago

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.

u/not_a_compiler_dev 48 points 4 hr. ago

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?

u/coroutine_hater_2024 31 points 4 hr. ago

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::execution somewhere usable. Maybe "ship the base and iterate" isn't the win people think it is when the base keeps moving under you.

u/committee_archaeologist 47 points 4 hr. ago

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 appertain and focus on querying

But 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 appertain and [[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.

u/standards_gossip_2025 19 points 3 hr. ago

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.

u/so_many_proposals 61 points 4 hr. ago

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 @Deprecated and a single line of reflection code. We're getting there though. Only two more standards cycles and a dozen more papers.

u/embedded_for_20_years 18 points 3 hr. ago

At least Java's @Deprecated doesn'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.

u/generic_lib_author 41 points 3 hr. ago

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_of API 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.

u/ordering_matters_42 31 points 3 hr. ago

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_of to drive code generation output.

u/generic_lib_author 28 points 2 hr. ago

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. Making attributes_of return 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.

u/ordering_matters_42 22 points 2 hr. ago

I'm not asking for source-ordered or semantically-ordered. I'm asking for deterministic. If I call attributes_of on 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_of returns a deterministic order. attributes_of should do the same.

u/generic_lib_author 18 points 1 hr. ago

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.

u/async_embedded_dev 35 points 3 hr. ago

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.

u/clang_adjacent 20 points 2 hr. ago

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::availability with positional arguments will take longer.

u/template_metaprogrammer_4k 8 points 2 hr. ago

"Conditionally supported" is the new SFINAE. Technically it works. Practically you need #ifdef ladders to find out.

u/definitely_not_bjarne 42 points 2 hr. ago

I for one welcome our new consteval overlords.

u/needs_more_constexpr 24 points 1 hr. ago

^^[[consteval_overlord]] - ill-formed, unsupported attribute. Even our overlords have limits.

u/throwaway_84729 22 points 2 hr. ago

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.

u/enterprise_cxx_2021 31 points 1 hr. ago

We use it extensively. [[nodiscard("check error code before proceeding")]] on every function returning error_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.

u/sfinae_survivor 18 points 1 hr. ago

Every single error_code return 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.