Video summary

Practical Reflection With C++26 - Barry Revzin - CppCon 2025

Main summary

Key takeaways

Technology

Summary of technological concepts and features (C++26 Reflection + “SOA vector”)

Barry Revzin (CppCon 2025) frames C++26 reflection as a goal of making libraries as easy to use as “one-line changes”, rather than as dull examples like enum -> string. His primary teaching vehicle is implementing a Structure-of-Arrays (SoA) vector in standard C++26 using reflection-powered code generation.


Origin / motivation

  • A colleague recommended Andrew Kelly’s talk on data-oriented design (Zigg), highlighting that a simple code change can switch from an array-of-structs to struct-of-arrays efficiently.
  • Revzin wants to demonstrate that with C++26 reflection, libraries can become similarly ergonomic:
    • users don’t manually write the boilerplate
    • the library generates it

What the talk does (step-by-step build)

1. Storage layout

  • Starts with a “simple” struct type (with internal padding), e.g.: cpp struct square { char x; long y; ... };

  • Compares AoS vs SoA:

    • AoS: a contiguous array of square
    • SoA:
      • separate contiguous buffers per member (e.g., one for x, one for y)
      • maintains shared size and capacity once, rather than per member

2. Reflection-driven “define aggregate” technique

Uses C++26 reflection to complete an incomplete type by populating it with a specific set of non-static data members.

Key pattern:

  • declare an incomplete type
  • in a const declaration context:
    • loop over reflected members
    • call std::define_aggregate (via reflection) to complete the type

3. Access control via “access contexts”

Reflection can cooperate with privacy using an access context:

  • unchecked: access all members
  • unprivileged: public-only
  • “current”: only what’s accessible in the current scope (e.g., friends)

For the tutorial, Revzin assumes a global unchecked-like context to keep focus on the core mechanics.


4. Reserved member-name conflict problem

If the library-generated SoA wrapper includes member names like size_ / capacity_, it breaks for user types that already define those names.

Solution:

  • keep size/capacity outside the reflected/generated member set
  • generate only per-original-member pointer fields inside

5. Constraints on the element type T

SoA vector support is not universal. Requirements include:

  • T must be an aggregate (needed for reconstruction / decomposition)
  • T must have no base classes (simplifies reconstruction via list-initialization)
  • members must be independently destructible
    • supports destruction of member-storage without cross-dependencies

6. Custom trait predicates implemented as functions (not templates)

Reflection changes how to express type properties.

  • Instead of variable templates like is_aggregate_v, use functions such as is_aggregate_type(...)
  • When a trait depends on a template instantiation, it uses reflection primitives:
    • substitute + extract
      • substitute: perform template argument substitution on reflected templates
      • extract: retrieve a value (e.g., a boolean) from a reflected entity

Container operations implemented with reflection

7. push_back

  • Checks capacity; grows storage if needed.
  • Uses placement construction per member storage:
    • construct x into the x array slot
    • construct y into the y array slot
    • etc.
  • Exception safety is postponed (explicitly not delved into).

8. grow / reallocation strategy

  • Grows each member buffer (each pointer) independently.
  • Relocation/move behavior is described using C++26 concepts (mentions relocate / move-destroy).

9. Different compile-time iteration strategies

Revzin presents several ways to iterate over reflected members and corresponding storages:

  • Option 1: structured bindings with packs from pointers and values, then fold with construct_at
  • Option 2: C++26 expansion statements using views::iota or views::indices
  • Option 3: build a tuple-based “zip” of member references and construct via tuples
  • A more range-based zip exists but hits a lifetime/allocation limitation.
    • Workaround: define_static_array avoids allocation by promoting a constexpr-computed range into static storage.

10. Detour: implementing define_static_array

Implements the idea:

  • take a range
  • lift elements into constant template parameters via reflection
  • instantiate a reflected array

It uses the fact that reflection can represent both types and values uniformly as info.


Observability: turning the structure into printable/debuggable output

11. Creating “spans” view into member buffers

Provides a span-of-members layout:

  • spans.x is a span<char>
  • spans.y is a span<long>

This allows printing SoA members independently.


12. Automatic formatting via annotations

Instead of writing custom formatters for each generated wrapper type:

  • introduces an annotation mechanism (e.g., “derive debug”)
  • the formatter detects annotated types
  • formatting then proceeds via reflection of members

Reflection utilities and ideas used:

  • annotations_of_with_type to filter annotations by their tag type
  • reflection-based equality to compare annotation payloads without requiring operator== for arbitrary payload types

13. Formatting details

Printed output includes:

  • type name
  • base classes (if any)
  • non-static data members:
    • their names
    • their values

Type names and identifiers come from reflection helpers like:

  • display_string_of
  • identifier_of

Mutable element access: proxy + “write-back”

14. v[i] returns a proxy (not a real T)

Because members live in separate arrays, mutation needs indirection.

Implements:

  • a proxy type holding lvalue references to each member’s element (char&, long&, etc.)
  • an assignment operator that:
    • assigns from a T
    • destructures T into the member references
  • a conversion operator that:
    • builds a T from the member references

15. Formatting proxy correctly

If the proxy is formatted as its own reflected type, output is ugly (it reveals internal proxy_base structure).

Fix:

  • introduce an annotation/trait like format_as to declare:
    • “format this proxy as if it were T”
  • the formatter uses conditional logic/inheritance to format either:
    • the proxy
    • or underlying T

Bonus: an API akin to “named member selection”

16. A with-like / w^? idea using reflection

Implements a function selected:

  • takes:
    • a function reflection
    • a type reflection
  • produces an array of the corresponding reflected members for parameter names
  • emits a compile-time error if a parameter name doesn’t exist on the type

This supports selecting member fields by name order, then invoking a callback for each element index.


Tooling / implementation notes discussed

  • Current implementations
    • Most up-to-date reflection support is Dan Katz’s Clang fork, available on Compile Explorer under “reflection C++26”
    • GCC work is underway by another developer
    • compile-time benchmarking isn’t yet meaningful early on
  • Debugging
    • given the “define aggregate” workflow, debugging leans on printing/reflection introspection of generated members (e.g., offsets / produced members)

Open questions raised in Q&A (and answers)

  • Can reflection observe attributes?
    • Not in the current model (“you cannot reflect on attributes”).
  • Can code generation like getters/setters be produced now?
    • Not yet; current reflection/codegen mechanisms are limited (no true “generate members/getters” yet).
  • Does “compile-time exception” become compile-time error?
    • Yes: meta-exceptions thrown during constant evaluation produce compile-time diagnostics
    • meta exception can include a message plus location info
  • Unchecked access vs breaking changes when private members change
    • Access contexts are explicit (unchecked, unprivileged, current)
    • library authors can choose whether to depend on public-only or all members
  • Why not allow consteval functions with non-constant parameters?
    • Discussed as fundamental: functions must behave consistently across runtime argument variations
    • substitute / extract helps lift constants into compile-time context safely
  • Standardizing formatting/member-name annotations
    • Likely: “default member-wise formatting” is a good candidate for future standardization
    • annotations may become standardized utilities, similar to how language features can emerge from libraries

Main speakers / sources

  • Speaker: Barry Revzin
    • software engineer at Jump Trading
    • involved in C++ standardization since 2016
    • author/blogger; also a writer (noted as “swims writer” in the source)
  • Referenced sources:
    • Andrew Kelly — “practical guide to applying data oriented design” (Zigg language creator)
    • Victoria — keynote mentioned for data-oriented design
    • Dan Katz — Clang fork implementing reflection C++26
    • Matt — referenced in audience thanks (possibly related to Calabrese / Compile Explorer context)
    • Range-v3 — and related ideas like zip/transform/actions as conceptual parallels

Original video