Document: P3856R5
Author: Jagrut Dave, Alisdair Meredith
Date: 2026-02-11
Audience: LEWG / LWG
You know how C++20 expanded NTTPs to allow class types as template parameters? The standard defines exactly what a "structural type" means — scalar, lvalue reference, or literal class type with all-public, non-mutable direct bases and data members, recursively — and then mandates that certain library types must be structural, and gives users exactly zero portable way to verify their own type qualifies.
P3856R5 closes that gap. It adds std::meta::is_structural_type(info) to the <meta> header (C++26 reflection) and std::is_structural<T> / std::is_structural_v<T> to <type_traits>. The trigger was US NB comment 49 from the C++26 CD ballot — someone finally noticed the oversight and wrote it up as a proper paper.
The interesting detour is Section 3: the authors built a proof-of-concept on P2996 reflection primitives. It mostly works, but reveals gaps in the reflection API that are arguably more notable than the proposal itself. There is no is_constexpr metafunction for querying whether a destructor is constexpr, no is_literal_type (removed from <type_traits> in C++20), no is_lambda_closure_type. The PoC works around all of these with conservative approximations. Captureless lambdas are structural, but the PoC cannot verify it cleanly.
Section 4 contains a design-philosophy argument about type traits vs. reflection metafunctions: they should be implemented independently, not built on each other. That one will age interestingly.
Reminder: be civil. Paper authors occasionally stop by these threads.
Bloomberg paper detector: online. Alisdair Meredith papers authored: approaching countably infinite. Carry on.
In fairness, Alisdair co-authoring is a fairly reliable quality signal. He has forgotten more about the library than most of us have learned.
The paper buries the lede:
This undersells the situation. When C++20 shipped class-type NTTPs, every stdlib implementation immediately needed to check structural-ness to implement their own mandates. The way they all did it: undocumented compiler intrinsics. Check libstdc++, libc++, MSVC STL sources — they all have equivalent private machinery. It has been sitting there for five years, unexposed, because there was no standard way to describe the predicate to users.
This paper is standardizing what compiler vendors already had to implement. The question is not "should we add this" — it is "why did this take until the NB comment phase of C++26 to write up."
Structural type check in C++20: squint at the error message and figure out which member broke it. Structural type check in C++26:
is_structural_v<T>. Progress.Section 3.1.1, Example 1:
This does not compile.
Vis a type parameter.decltype(V)is ill-formed — you cannot calldecltypeon a type. This should betemplate<auto V>. The intent is clearly a non-type template parameter constrained to structural types, but the example usestypenamethroughout the motivating section. Did these get built before submission?Same section also uses
is_structural_type_vin the example code, but the proposed variable template in the actual wording section isis_structural_v. Two different names for the same thing within the same paper. One of them gets fixed before LWG sees it — hopefully.Ah yes, the classic pipeline: motivation section describes API A, wording section proposes API B, editorial note says "these are the same."
Section 4 states:
I understand the argument — different observable side effects (template instantiations vs. reflection queries). But this creates a correctness surface. Two independent implementations of the same predicate can drift. There is no enforcement mechanism. If
is_structural_v<T>andis_structural_type(^^T)give different answers on an edge case, which one is authoritative? The answer "both are normatively specified against [temp.param] so they agree" is satisfying in theory and fragile in practice.The independence is about implementation strategy, not observable semantics.
is_structural_v<T>andis_structural_type(^^T)are both specified normatively against [temp.param]. If they disagree on any type, that is a non-conforming implementation — no different fromis_trivially_copyable_v<T>diverging from__is_trivially_copyable(T). We already have this pattern throughout<type_traits>. Section 4 is telling vendors what they should do internally, not claiming divergence is acceptable.Sure, but the PoC in the paper is demonstrably conservative by the paper's own admission. It cannot correctly identify structural-ness for non-aggregate literal class types because
is_constexpr(info)does not exist yet. A correctis_structural_v<T>(via compiler intrinsic) would returntruefor some such types today, while the PoCis_structural_type(info)returnsfalse. The divergence is present right now, in the paper. The "normative spec ensures agreement" argument only holds once P2996 has the missing primitives.That is the most honest critique of the paper. The PoC being conservative is not the proposal being conservative — the actual wording says
is_structural_type(info)returns true iffinforeflects a structural type per [temp.param], full stop. Real implementations will use intrinsics, not the PoC. But you are right that the paper does not make this distinction loudly enough. The PoC reads like the intended implementation. It is not. Someone in LWG will ask this exact question, and the authors will need a cleaner answer.Rust solved const generic eligibility in 1.51. Types usable as const generic parameters implement
PartialEq. Much simpler than C++, which needs this whole recursive "are all members public and non-mutable and also structural" walk.oh we're doing this again
Rust const generics and C++ NTTPs are solving different problems. Rust's
PartialEqrequirement is about equality semantics so the compiler can deduplicate template instantiations. C++ structural types are about layout and public visibility so the compiler can encode the value in the mangled name. Neither is strictly better — they encode different invariants for different purposes. Also: Rust still does not allow floats as const generic parameters. C++ does. Different design goals, different trade-offs.Genuine question from someone who does not follow reflection closely: why can't you approximate this with a SFINAE probe? Something like trying to use
Tas an NTTP and seeing if substitution succeeds? Is the issue that structural-type violations are hard errors rather than substitution failures?You nailed it — structural-type violations in NTTP context are ill-formed, not substitution failures. SFINAE does not catch them. Even if you managed to make a specific compiler swallow it, you would be relying on implementation-defined behavior. The standard
is_structural_v<T>can be a compiler intrinsic: O(1), no template instantiation overhead, guaranteed portable. The SFINAE trick works on some compilers today and breaks quietly when the next version tightens conformance.The most interesting thing in this paper is buried in Section 3.3:
std::is_literal_type<T>was deprecated in C++17 and removed in C++20 on the grounds that it was too broad to be useful. Now, six years later, we need the reflection equivalent to implementis_structural_typecorrectly. The committee removed a building block, then built something that depends on the building block. This is not the first time and will not be the last.The subtitle "US NB comment 49" is doing a lot of work in that title. This is a proposal responding to a National Body comment on a Committee Draft that is itself the output of a process that began with earlier papers. Somewhere there is a comment that generated a paper that cites a paper that addresses a comment that will generate another paper. The process is a fixed point.
[deleted]
what did they say
something about Rust, you know the usual
[removed by moderator]
Early bird registration now open. The conference where standards papers become talks. Submit your proposal by June 1.
Because sometimes you need to see the assembly. Free, fast, supports 30+ compilers including experimental reflection builds.