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
Reminder: paper authors sometimes read these threads. Keep it constructive.
Took a National Body comment to ship a missing type query. Classic. We have only been using NTTPs since C++11.
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?"
Ok fine, C++20 complaint then. We have been waiting four years.
committee gonna committee. at least it is shipping in the NB resolution pile instead of festering in R-proposals.
Wait, how is a structural type different from an aggregate? I thought you needed an aggregate for NTTP class types.
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
mutablemember). The operative requirement for NTTPs is structural, not aggregate.So
struct S { mutable int x; };would be an aggregate but not structural?Exactly. The
mutabledisqualifier 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.Read the proof-of-concept section carefully — specifically what it cannot do. The paper notes in passing:
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 returnfalsefor a captureless lambda type, even though the correct answer istrue.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.
From the compiler side: correct on all counts. The intrinsic path for
is_structural_typeis 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.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)andis_structural<T>— and Section 4 lands here: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.
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.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.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.
Alisdair Meredith is a co-author. The paper is fine. It will pass. Moving on.
This is the most r/cpp comment I have ever read. "Respected veteran co-authored it, ship it."
I mean, am I wrong?
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.
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.laughs in 2031 deployment
The type-traits-vs-metafunctions section is doing a lot of work for a paper this narrow. Specifically:
"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 aconstevalfunction. This reads like an argument for two permanent maintenance paths dressed up as user protection from implementation details they will never encounter.There is a more defensible version of that argument: compile diagnostics. If
is_structural_v<T>is implemented on top of aconstevalreflection 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.In Rust, const generics do not need a separate trait to query this because —
[removed by moderator]
what did they say
four paragraphs about trait bounds and const generics. you know how it goes.
The motivating example is clean:
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.
Edit: wait — does not
template<auto V>already enforce structural-ness at instantiation? You cannot pass a non-structural value there at all.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_assertwith actual diagnostic text. Also useful inconstexpr ifdispatch, library mandates, and anywhere you need to branch on structural-ness before substitution happens.Bloomberg quietly carrying a solid 30% of C++26.
godbolt.org — because you need to see the assembly. Bloomberg Clang (P2996 experimental) available in the compiler list.
The conference for the C++ community. Where papers become talks.