r/wg21
P3856R4 — New reflection metafunction - is_structural_type (US NB comment 49) Standards
Posted by u/constexpr_archaeologist · 5 hr. ago

Document: P3856R4
Author: Jagrut Dave, Alisdair Meredith
Date: 2026-01-18
Audience: LEWG / LWG

You know that template<auto V> pattern where you want to constrain V to types that can legally appear as non-type template parameters? There is a specific property called a structural type that governs NTTP eligibility — scalars, lvalue references, and literal class types with all-public non-mutable bases and members, recursively. The standard already mandates structural-ness in several library clauses (meaning every implementation checks it internally), but there has never been a standard way for user code to ask.

P3856R4 proposes to fix that with two additions: std::meta::is_structural_type(info) in <meta> (a reflection metafunction, building on P2996), and std::is_structural<T> / std::is_structural_v<T> in <type_traits> (a classic trait for code that does not want to pull in the full reflection machinery). The paper addresses US NB comment 49 on the C++26 CD.

Also included is a ~200-line proof-of-concept implementation in pure P2996 reflection — instructive partly for what it cannot do. Non-aggregate classes with constexpr constructors require an is_constexpr intrinsic that P2996 does not yet expose, and captureless lambdas (structural per [expr.prim.lambda.closure]) need is_lambda_closure_type, also missing. The authors flag both as needed P2996 additions. Section 4 has a full page on whether type traits and reflection metafunctions should evolve independently — it will either fascinate you or make you wish the paper had stayed narrowly focused.

https://wg21.link/p3856r4

▲ 312 points (91% upvoted) · 31 comments
sorted by: best
u/r_cpp_janitor 1 point 5 hr. agoModerator

Reminder: paper authors sometimes read these threads. Keep it constructive.

u/UB_enjoyer_69 487 points 5 hr. ago

Took a National Body comment to ship a missing type query. Classic. We have only been using NTTPs since C++11.

u/lord_segfault 178 points 4 hr. ago

To be fair, the current structural type rules for class types only crystallized in C++20 when extended NTTPs landed. Before that it was basically "is it an int or an enum?"

u/UB_enjoyer_69 234 points 4 hr. ago

Ok fine, C++20 complaint then. We have been waiting four years.

u/daily_template_wizard_2019 89 points 4 hr. ago

committee gonna committee. at least it is shipping in the NB resolution pile instead of festering in R-proposals.

u/former_boost_contributor 34 points 5 hr. ago

Wait, how is a structural type different from an aggregate? I thought you needed an aggregate for NTTP class types.

u/senior_template_wizard_2020 156 points 4 hr. ago

Common confusion. They overlap but are not the same.

Structural ([temp.param]):
- Scalar (int, pointer, enum, float...)
- lvalue reference
- Literal class type with all-public, non-mutable bases and non-static data members, where all those types are also structural

Aggregate has different rules — no user-provided constructors, no private/protected non-static data members, etc.

You can have a structural type that is not an aggregate (uncommon, possible with certain inheritance), and an aggregate that is not structural (e.g., has a mutable member). The operative requirement for NTTPs is structural, not aggregate.

u/former_boost_contributor 23 points 3 hr. ago

So struct S { mutable int x; }; would be an aggregate but not structural?

u/senior_template_wizard_2020 67 points 3 hr. ago

Exactly. The mutable disqualifier exists because the compiler needs to embed the NTTP value as a compile-time constant — a mutable field would let you change what is supposed to be immutable.

u/nttp_veteran_42 134 points 4 hr. ago

Read the proof-of-concept section carefully — specifically what it cannot do. The paper notes in passing:

The implementation could be improved if metafunctions such as "is this a literal type", "is this a lambda closure type", or "is this function constexpr" were available

But the framing undersells the gap. Captureless lambdas are structural per [expr.prim.lambda.closure] — that is normative text. The PoC has no way to detect them because P2996R13 does not expose is_lambda_closure_type(info). So any implementation built purely on current P2996 primitives would return false for a captureless lambda type, even though the correct answer is true.

This is not a polish note. It means the PoC's postcondition is not "returns true iff T is structural" — it is "returns true iff I can prove T is structural with current tooling." Those are different guarantees. The wording proposal itself is fine (the compiler will use an intrinsic), but LEWG should treat the missing P2996 primitives as a real dependency ticket, not aspirational future work.

u/clang_implementer_throwaway 78 points 3 hr. ago

From the compiler side: correct on all counts. The intrinsic path for is_structural_type is straightforward — we track this internally for template argument deduction already. The P2996 primitive gap is real but it does not block the proposal; it becomes a follow-on P2996 change request. Good paper to read alongside https://wg21.link/p2996 if you want the full dependency picture.

u/trait_design_watcher 89 points 2 hr. ago

Both of you are circling a more structural problem (sorry). The PoC incompleteness is downstream of a bigger unresolved question: the paper proposes two APIs — is_structural_type(info) and is_structural<T> — and Section 4 lands here:

More research and discussion needs to be carried out

If both APIs are defined against [temp.param] and cannot ever diverge, why do you have two? If they can diverge — say, due to future access-context semantics or evolution of the structural type rules — you have a latent defect. The paper presents the dual-API choice as a principled design decision but actually defers the hard question entirely.

u/nttp_veteran_42 56 points 2 hr. ago

They cannot diverge semantically — both are defined against [temp.param] and the paper explicitly uses unchecked access context (LEWG approved this at the January telecon). The difference is purely pragmatic: is_structural_v<T> works without #include <meta>. In constrained environments where the reflection machinery is unavailable or adds compile overhead you care about, you want the standalone trait. That is a legitimate split even if the paper buries the motivation.

u/trait_design_watcher 71 points 1 hr. ago

Then put that argument in the paper. Look at the motivating examples in Section 3.1.1: every single one uses is_structural_type_v<> — the trait — not the metafunction. Zero metafunction usage examples. If the "no <meta> header" story is the actual motivation for shipping a separate trait, that should be the first paragraph of Section 4 rather than a page of philosophical debate about "different metaprogramming paradigms." As written, the paper implies both APIs are co-equal and recommends neither.

u/nttp_veteran_42 43 points 58 min. ago

Fair point on the examples. The paper would land better if Section 4 led with the header-dependency use case instead of the paradigm essay. Concede the presentation critique. The dual proposal is still correct; the motivation just needs tighter framing before LEWG sees the final revision.

u/yet_another_lurker 201 points 4 hr. ago

Alisdair Meredith is a co-author. The paper is fine. It will pass. Moving on.

u/not_a_real_cpp_dev 134 points 4 hr. ago

This is the most r/cpp comment I have ever read. "Respected veteran co-authored it, ship it."

u/yet_another_lurker 267 points 3 hr. ago

I mean, am I wrong?

u/actually_a_firmware_dev 98 points 3 hr. ago

Slightly tangential but: when does P2996 reflection actually compile in a non-Bloomberg Clang? I want to use this stuff but I am not maintaining a compiler fork.

u/clang_implementer_throwaway 87 points 2 hr. ago

GCC 15 has experimental P2996 support behind -freflection. Clang upstream has a work-in-progress branch. EDG has had it for a while — it is what Bloomberg's Clang is built on. Godbolt has the Bloomberg Clang fork available if you want to experiment now without building anything.

u/UB_enjoyer_69 312 points 3 hr. ago

laughs in 2031 deployment

u/api_ergonomics_skeptic 56 points 3 hr. ago

The type-traits-vs-metafunctions section is doing a lot of work for a paper this narrow. Specifically:

Type traits should not use reflection under the covers as that may surprise programmers who expect to see and handle the side effects of template metaprogramming.

"Surprise" is carrying a lot here. If the observable behavior of is_structural_v<T> is correct and the diagnostics are good, users generally do not care whether the internals are SFINAE or a consteval function. This reads like an argument for two permanent maintenance paths dressed up as user protection from implementation details they will never encounter.

u/senior_template_wizard_2020 44 points 2 hr. ago

There is a more defensible version of that argument: compile diagnostics. If is_structural_v<T> is implemented on top of a consteval reflection function, a failed check might produce a diagnostic rooted in <meta> internals rather than the clean <type_traits> error path. That is a real UX difference. But the paper does not make that case — it talks about "side effects" and "error logs" without showing a concrete example of what the divergence would look like.

u/just_use_rust_bro 12 points 2 hr. ago

In Rust, const generics do not need a separate trait to query this because —

u/[deleted] 0 points 2 hr. ago

[removed by moderator]

u/daily_template_wizard_2019 34 points 1 hr. ago

what did they say

u/UB_enjoyer_69 89 points 1 hr. ago

four paragraphs about trait bounds and const generics. you know how it goes.

u/compiles_first_try_lol 167 points 2 hr. ago

The motivating example is clean:

template<auto V>
requires is_structural_type_v<decltype(V)>
void register_token() { /* V is a valid NTTP, known at compile time */ }

template<auto V>
requires (!is_structural_type_v<decltype(V)>)
void register_token() = delete;

You can approximate this today with concepts and recursive member inspection, but the diagnostics are genuinely terrible. A named trait makes the pattern teachable to people who are not template metaprogramming specialists.

u/former_boost_contributor 23 points 1 hr. ago

Edit: wait — does not template<auto V> already enforce structural-ness at instantiation? You cannot pass a non-structural value there at all.

u/compiles_first_try_lol 56 points 47 min. ago

It does enforce it, but the error fires as "cannot deduce template argument" with no explanation of why. The constraint lets you fire a static_assert with actual diagnostic text. Also useful in constexpr if dispatch, library mandates, and anywhere you need to branch on structural-ness before substitution happens.

u/not_a_real_cpp_dev 445 points 4 hr. ago

Bloomberg quietly carrying a solid 30% of C++26.