diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml
index bcad3fd..e66776d 100644
--- a/.github/workflows/cmake.yml
+++ b/.github/workflows/cmake.yml
@@ -211,6 +211,18 @@ jobs:
std: 20,
},
}
+ - {
+ name: "Ubuntu GCC-16 (C++26 Reflection)",
+ os: ubuntu-24.04,
+ compiler:
+ {
+ type: GCC16,
+ version: 16,
+ cc: "gcc-16",
+ cxx: "g++-16",
+ std: 26,
+ },
+ }
steps:
- uses: actions/checkout@v4
- uses: seanmiddleditch/gha-setup-ninja@master
@@ -231,9 +243,15 @@ jobs:
with:
version: ${{ matrix.settings.compiler.version }}
platform: x64
+ - name: Install GCC 16 from ubuntu-toolchain-r PPA
+ if: matrix.settings.compiler.type == 'GCC16'
+ run: |
+ sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
+ sudo apt-get update
+ sudo apt-get install -y --no-install-recommends g++-16 gcc-16
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Set up Python
run: uv sync
- name: Run CMake
- run: ./scripts/cmake.sh --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }}
+ run: ./scripts/cmake.sh --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }} ${{ matrix.settings.compiler.type == 'GCC16' && '--reflection' || '' }}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0626252..772266e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,6 +29,34 @@ option(ENABLE_UBSAN "Enable Undefined Behaviour Sanitizer" OFF)
option(ENABLE_TSAN "Enable Thread Sanitizer" OFF)
option(ENABLE_MSAN "Enable Memory Sanitizer" OFF)
+option(
+ XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL
+ "Also build and test tutorials/3_reflection.cc, which requires a compiler \
+with C++26 P2996 reflection support (GCC 16+ with -freflection)."
+ OFF)
+
+if(XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL)
+ include(CheckCXXSourceCompiles)
+ set(CMAKE_REQUIRED_FLAGS "-std=c++26 -freflection")
+ check_cxx_source_compiles(
+ "
+ #include
+ constexpr std::meta::info reflection_of_int = ^^int;
+ int main() {}
+ "
+ XYZ_PROTOCOL_REFLECTION_SUPPORTED)
+ unset(CMAKE_REQUIRED_FLAGS)
+ if(NOT XYZ_PROTOCOL_REFLECTION_SUPPORTED)
+ message(
+ FATAL_ERROR
+ "XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL is ON but the compiler "
+ "(${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}) does not "
+ "accept '-std=c++26 -freflection'. C++26 reflection currently "
+ "requires GCC 16 or newer; configure with e.g. "
+ "CXX=g++-16 CC=gcc-16 and a separate build directory (-B).")
+ endif()
+endif()
+
if(ENABLE_ASAN OR ENABLE_UBSAN OR ENABLE_TSAN OR ENABLE_MSAN)
set(ENABLE_SANITIZERS ON)
endif()
@@ -148,6 +176,31 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT)
target_include_directories(protocol_test
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+ # Standalone teaching tutorials (tutorials/) -- see tutorials/README.md.
+ # Each is self-contained, with no dependency on protocol.h.
+ xyz_add_test(
+ NAME
+ type_erasure_tutorial
+ FILES
+ tutorials/1_type_erasure.cc)
+
+ xyz_add_test(
+ NAME
+ vanishing_this_pointer_tutorial
+ FILES
+ tutorials/2_vanishing_this_pointer.cc)
+
+ if(XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL)
+ xyz_add_test(
+ NAME
+ reflection_tutorial
+ VERSION
+ 26
+ FILES
+ tutorials/3_reflection.cc)
+ target_compile_options(reflection_tutorial PRIVATE -freflection)
+ endif()
+
add_executable(protocol_benchmark protocol_benchmark.cc)
target_link_libraries(protocol_benchmark PRIVATE protocol benchmark::benchmark)
add_dependencies(protocol_benchmark generate_protocols)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4e77735..0a936be 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -144,6 +144,20 @@ This library is an active proof of concept and is subject to change.
- Development: Use this library for understanding its concepts and
contributing to its development. Avoid using it in production code.
+## Interactive Docker Shell
+
+To launch an interactive bash shell in the pre-configured Docker container (which includes GCC 16, Python, CMake, and `uv`) without running an AI agent, use:
+
+```bash
+./scripts/docker-shell.sh [--rebuild-docker]
+```
+
+Inside the shell, GCC 16 is what lets you build and run the `tutorials/3_reflection.cc` tutorial, which needs C++26 reflection support:
+
+```bash
+./scripts/cmake.sh --release --reflection -B build-reflection
+```
+
## AI Coding Sandboxes
The repository includes a Docker-based sandbox script for AI coding
diff --git a/GEMINI.md b/GEMINI.md
index 5e03711..6b4f6e2 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -23,7 +23,8 @@ general defaults for this repository.
- **Tooling:** Always use `uv` for Python dependency management (`uv run ...`).
- **Build & Test:** Use `scripts/cmake.sh` for all build and test operations.
The `scripts/cmake.sh` entrypoint supports `--debug`, `--release`,
- `--asan`, `--ubsan`, `--tsan`, and `--msan`.
+ `--asan`, `--ubsan`, `--tsan`, `--msan`, and `--reflection` (builds and
+ tests `tutorials/3_reflection.cc`, requires a P2996 reflection compiler).
- **Compiler Preferences:** Prefer Clang 19+ for sanitizer-based verification
and CI, as it provides superior support for MSAN and TSAN compared to
older GCC versions.
diff --git a/cmake/xyz_add_test.cmake b/cmake/xyz_add_test.cmake
index 00cf265..ff9f4a6 100644
--- a/cmake/xyz_add_test.cmake
+++ b/cmake/xyz_add_test.cmake
@@ -50,7 +50,7 @@ function(xyz_add_test)
if(NOT XYZ_VERSION)
set(XYZ_VERSION 20)
else()
- set(VALID_TARGET_VERSIONS 11 14 17 20 23)
+ set(VALID_TARGET_VERSIONS 11 14 17 20 23 26)
list(FIND VALID_TARGET_VERSIONS ${XYZ_VERSION} index)
if(index EQUAL -1)
message(FATAL_ERROR "TYPE must be one of <${VALID_TARGET_VERSIONS}>")
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 4966bbb..7b89f3a 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -26,6 +26,13 @@ RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/nul
&& apt-get update && apt-get install -y --no-install-recommends cmake \
&& rm -rf /var/lib/apt/lists/*
+# Install GCC 16 (experimental C++26 reflection support via -std=c++26
+# -freflection, see https://gcc.gnu.org/gcc-16/changes.html) from the Ubuntu
+# Toolchain Test PPA, since Ubuntu 24.04's own repos only go up to GCC 14.
+RUN add-apt-repository -y ppa:ubuntu-toolchain-r/test \
+&& apt-get update && apt-get install -y --no-install-recommends g++-16 gcc-16 \
+&& rm -rf /var/lib/apt/lists/*
+
# Install bazelisk.
RUN ARCH=$(dpkg --print-architecture) && \
wget https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-${ARCH} -O /usr/local/bin/bazelisk \
diff --git a/scripts/cmake.py b/scripts/cmake.py
index ecfca13..d17f7c5 100644
--- a/scripts/cmake.py
+++ b/scripts/cmake.py
@@ -2,6 +2,7 @@
"""CMake helper script for building and testing the project."""
import argparse
+import os
import subprocess
from typing import Any
@@ -37,6 +38,12 @@ def main() -> None:
)
parser.add_argument("--tsan", action="store_true", help="Enable Thread Sanitizer")
parser.add_argument("--msan", action="store_true", help="Enable Memory Sanitizer")
+ parser.add_argument(
+ "--reflection",
+ action="store_true",
+ help="Build and test tutorials/3_reflection.cc (requires a P2996 "
+ "reflection compiler, e.g. CXX=g++-16 CC=gcc-16)",
+ )
parser.add_argument("-B", "--build-dir", help="Build directory")
parser.add_argument(
"--clean", action="store_true", help="Fresh configuration and clean-first build"
@@ -74,6 +81,8 @@ def log(msg: Any) -> None:
f"-DENABLE_UBSAN={'ON' if args.ubsan else 'OFF'}",
f"-DENABLE_TSAN={'ON' if args.tsan else 'OFF'}",
f"-DENABLE_MSAN={'ON' if args.msan else 'OFF'}",
+ "-DXYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL="
+ + ("ON" if args.reflection else "OFF"),
]
if args.build_dir:
configure_args.extend(["-B", args.build_dir])
@@ -82,8 +91,17 @@ def log(msg: Any) -> None:
configure_args.extend(extra)
+ # A P2996 reflection compiler is required to configure with
+ # XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL=ON. CMake only reads CXX/CC
+ # from the environment, not from -D cache variables, so set them here
+ # rather than as configure_args.
+ configure_env = os.environ.copy()
+ if args.reflection:
+ configure_env["CXX"] = "g++-16"
+ configure_env["CC"] = "gcc-16"
+
log(f"Running: {' '.join(configure_args)}")
- subprocess.check_call(configure_args)
+ subprocess.check_call(configure_args, env=configure_env)
# Build step (required for build, test, benchmark)
build_args = ["cmake", "--build"]
diff --git a/scripts/docker-shell.sh b/scripts/docker-shell.sh
new file mode 100755
index 0000000..cbe1b88
--- /dev/null
+++ b/scripts/docker-shell.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+set -eu -o pipefail
+
+WORKSPACE_ROOT=$(git rev-parse --show-toplevel)
+IMAGE_NAME="cc-protocol-sandbox"
+
+REBUILD=0
+for argument in "$@"; do
+ if [ "$argument" == "--rebuild-docker" ]; then
+ REBUILD=1
+ fi
+done
+
+if [ "$REBUILD" -eq 1 ] || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
+ echo "--- Building Docker Image: ${IMAGE_NAME} ---"
+ docker build -t "$IMAGE_NAME" -f "${WORKSPACE_ROOT}/docker/Dockerfile" "$WORKSPACE_ROOT"
+fi
+
+echo "--- Starting Interactive Docker Shell ---"
+echo "Project root ${WORKSPACE_ROOT} is mounted at /workspace"
+echo ""
+echo "To build and test with the C++26 reflection backend (GCC 16):"
+echo " ./scripts/cmake.sh --release --reflection -B build-reflection"
+echo ""
+
+exec docker run -it --rm \
+ -v "${WORKSPACE_ROOT}:/workspace" \
+ -w /workspace \
+ "$IMAGE_NAME" bash
diff --git a/tutorials/1_type_erasure.cc b/tutorials/1_type_erasure.cc
new file mode 100644
index 0000000..5b8019f
--- /dev/null
+++ b/tutorials/1_type_erasure.cc
@@ -0,0 +1,210 @@
+/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+==============================================================================*/
+
+// Type erasure, from first principles:
+// 1. The problem type erasure solves.
+// 2. A vtable of function pointers.
+// 3. The eraser object.
+
+#include
+
+#include
+#include
+#include
+
+// ---------------------------------------------------------------------------
+// 1. The problem.
+//
+// A function template can call draw() polymorphically: it works for any T
+// with a draw() method. Each instantiation, though, is a distinct function.
+// render and render are two different functions with two
+// different addresses; there is no single type you could use to store
+// "either a Circle or a Square, decided at runtime" and call draw() on it.
+// The rest of this file builds one: type erasure.
+// ---------------------------------------------------------------------------
+namespace section_1 {
+
+class Circle {
+ public:
+ std::string draw() const { return "○"; }
+};
+
+class Square {
+ public:
+ std::string draw() const { return "□"; }
+};
+
+template
+std::string render(const Shape& shape) {
+ return shape.draw();
+}
+
+TEST(TypeErasureProblem, TemplatesArentUniform) {
+ Circle circle;
+ Square square;
+ EXPECT_EQ(render(circle), "○");
+ EXPECT_EQ(render(square), "□");
+
+ // render and render are different types once
+ // instantiated, so no single function pointer type could point at both.
+ using RenderCircle = std::string (*)(const Circle&);
+ using RenderSquare = std::string (*)(const Square&);
+ static_assert(!std::is_same_v);
+}
+
+} // namespace section_1
+
+// ---------------------------------------------------------------------------
+// 2. A vtable of function pointers.
+//
+// Circle and Square each get called through a small static object, a
+// "vtable", built from exactly the same struct type. The two casts below
+// (erasing to const void* at the call site, un-erasing back inside the
+// function) are the cast-and-call pattern type erasure relies on.
+// ---------------------------------------------------------------------------
+namespace section_2 {
+
+class Circle {
+ public:
+ std::string draw() const { return "○"; }
+};
+
+class Square {
+ public:
+ std::string draw() const { return "□"; }
+};
+
+// One function pointer, one method. A real interface with several methods
+// would just repeat this shape once per method it needs to expose.
+struct draw_vtable {
+ std::string (*draw)(const void* erased);
+};
+
+// One static instance per concrete type. Each is initialized with a
+// function that does the two casts: the incoming pointer arrives already
+// erased to const void* (the caller did that), and this function un-erases
+// it back to the one concrete type it actually knows how to handle, then
+// calls draw() on it normally.
+template
+inline constexpr draw_vtable vtable_for = {
+ .draw = [](const void* erased) -> std::string {
+ return static_cast(erased)->draw();
+ }};
+
+TEST(TypeErasureVtable, SameVtableDifferentTypes) {
+ Circle circle;
+ Square square;
+
+ // Two different concrete objects are called through the same vtable
+ // type (draw_vtable) via the same cast-and-call pattern; only the
+ // static instance and the erased pointer differ.
+ EXPECT_EQ(vtable_for.draw(&circle), "○");
+ EXPECT_EQ(vtable_for.draw(&square), "□");
+
+ // Both static vtable instances share one C++ type, unlike the two
+ // distinct render instantiations from section 1.
+ static_assert(std::is_same_v),
+ decltype(vtable_for)>);
+}
+
+} // namespace section_2
+
+// ---------------------------------------------------------------------------
+// 3. The eraser object.
+//
+// Package section 2's erased pointer and vtable pointer into one small
+// class, with a constructor template that captures whatever concrete type
+// it's given. Circle and Square can both be stored behind the result, and both
+// go in one std::vector. This is the minimal, one-method analogue of
+// xyz::protocol_view: a non-owning, type-erased handle with forwarding
+// methods that dispatch through the vtable.
+// ---------------------------------------------------------------------------
+namespace section_3 {
+
+class Circle {
+ public:
+ std::string draw() const { return "○"; }
+};
+
+class Square {
+ public:
+ std::string draw() const { return "□"; }
+};
+
+struct draw_vtable {
+ std::string (*draw)(const void* erased);
+};
+
+template
+inline constexpr draw_vtable vtable_for = {
+ .draw = [](const void* erased) -> std::string {
+ return static_cast(erased)->draw();
+ }};
+
+// erased_shape only stores a pointer to a Shape it doesn't own. The
+// concrete object (the Circle or Square passed to the constructor) must
+// outlive every erased_shape that refers to it.
+class erased_shape {
+ public:
+ template
+ explicit erased_shape(const Shape& shape)
+ : erased_(&shape), vtable_(&vtable_for) {}
+
+ std::string draw() const { return vtable_->draw(erased_); }
+
+ private:
+ const void* erased_;
+ const draw_vtable* vtable_;
+};
+
+TEST(TypeErasureEraser, UnifiesUnrelatedTypes) {
+ Circle circle;
+ Square square;
+
+ erased_shape erased_circle(circle);
+ erased_shape erased_square(square);
+
+ EXPECT_EQ(erased_circle.draw(), "○");
+ EXPECT_EQ(erased_square.draw(), "□");
+
+ // Both are the same C++ type, unlike Circle and Square themselves.
+ static_assert(
+ std::is_same_v);
+}
+
+TEST(TypeErasureEraser, BothTypesInOneVector) {
+ Circle circle;
+ Square square;
+
+ // One vector holds both a Circle and a Square, dispatched uniformly;
+ // neither class was touched or related to the other.
+ std::vector shapes;
+ shapes.emplace_back(circle);
+ shapes.emplace_back(square);
+
+ std::vector drawn;
+ for (const erased_shape& shape : shapes) {
+ drawn.push_back(shape.draw());
+ }
+
+ EXPECT_EQ(drawn, (std::vector{"○", "□"}));
+}
+
+} // namespace section_3
diff --git a/tutorials/2_vanishing_this_pointer.cc b/tutorials/2_vanishing_this_pointer.cc
new file mode 100644
index 0000000..020f47a
--- /dev/null
+++ b/tutorials/2_vanishing_this_pointer.cc
@@ -0,0 +1,300 @@
+/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+==============================================================================*/
+
+// The vanishing `this` pointer.
+//
+// This technique is adopted from https://ryanjk5.github.io/posts/rjk-duck/.
+//
+// A technique for giving a data member ordinary call syntax:
+// 1. The offset-zero layout guarantee.
+// 2. Recovering the owner pointer.
+// 3. Ordinary call syntax from a data member.
+// 4. What breaks when offset zero is violated, and the guard against it.
+// 5. Composing several such members through inheritance.
+
+#include
+
+#include
+#include
+#include
+#include
+
+// ---------------------------------------------------------------------------
+// 1. The offset-zero layout guarantee.
+//
+// A class's first non-static data member starts at the same address as the
+// object that contains it. This is called offset zero.
+// ---------------------------------------------------------------------------
+namespace section_1 {
+
+struct Wrapper {
+ int tag = 0;
+};
+
+struct Owner {
+ Wrapper wrapper; // first data member
+ int other_data = 0;
+};
+
+TEST(ThisPointerOffsetZero, FirstMemberSharesAddress) {
+ Owner owner;
+
+ EXPECT_EQ(static_cast(&owner), static_cast(&owner.wrapper));
+ EXPECT_EQ(offsetof(Owner, wrapper), 0u);
+
+ // other_data is not first, so it does not share the owner's address.
+ EXPECT_NE(static_cast(&owner), static_cast(&owner.other_data));
+ EXPECT_NE(offsetof(Owner, other_data), 0u);
+}
+
+// An empty Wrapper, stored as a data member rather than inherited from,
+// still costs no extra storage: [[no_unique_address]] is the data-member
+// counterpart of the empty base optimization, and lets an empty member
+// overlap entirely with the owner's own storage.
+struct EmptyWrapper {};
+
+struct OwnerWithEmptyWrapper {
+ [[no_unique_address]] EmptyWrapper wrapper;
+ int payload = 0;
+};
+
+TEST(ThisPointerOffsetZero, EmptyWrapperAddsNoStorage) {
+ EXPECT_EQ(sizeof(OwnerWithEmptyWrapper), sizeof(int));
+ EXPECT_EQ(offsetof(OwnerWithEmptyWrapper, wrapper), 0u);
+}
+
+} // namespace section_1
+
+// ---------------------------------------------------------------------------
+// 2. Recovering the owner pointer, "the vanishing this pointer" itself.
+//
+// Because a Wrapper sitting at offset zero of an Owner shares the Owner's
+// address (section 1), a method defined on Wrapper can compute a pointer to
+// its owner just by casting its own `this`, without Wrapper storing a
+// back-pointer anywhere. This is undefined behavior unless Wrapper is the first
+// data member of Owner, and Owner is a standard layout type.
+// ---------------------------------------------------------------------------
+namespace section_2 {
+
+struct Wrapper {
+ int recover_owner_value() const;
+};
+
+struct Owner {
+ Wrapper wrapper;
+ int value = 0;
+};
+
+int Wrapper::recover_owner_value() const {
+ const auto* owner = static_cast(static_cast(this));
+ return owner->value;
+}
+
+TEST(ThisPointerRecoverOwner, ReachesOwnersData) {
+ Owner owner;
+ owner.value = 42;
+
+ EXPECT_EQ(owner.wrapper.recover_owner_value(), 42);
+}
+
+TEST(ThisPointerRecoverOwner, NoExtraStorage) {
+ // sizeof(Wrapper) == 1 (the minimum for any object, empty or not), not 8
+ // (a pointer's worth): confirms no back-pointer is stored.
+ EXPECT_EQ(sizeof(Wrapper), 1u);
+}
+
+} // namespace section_2
+
+// ---------------------------------------------------------------------------
+// 3. Ordinary call syntax from a data member.
+//
+// Give Wrapper an operator() instead of an oddly-named method, make it a
+// data member of Owner instead of a method, and owner.member_name(...)
+// becomes an ordinary function call, even though member_name is a value of
+// class type rather than a function. Combined with section 2's owner
+// recovery, that call can reach and mutate the real owner.
+// ---------------------------------------------------------------------------
+namespace section_3 {
+
+struct GreetWrapper {
+ std::string operator()(std::string_view visitor) const; // defined below
+};
+
+struct Owner {
+ [[no_unique_address]] GreetWrapper greet;
+ std::string name = "World";
+};
+
+std::string GreetWrapper::operator()(std::string_view visitor) const {
+ const auto* owner = static_cast(static_cast(this));
+ return "Hello, " + std::string(visitor) + ", from " + owner->name + "!";
+}
+
+TEST(ThisPointerCallSyntax, BehavesAsOrdinaryCall) {
+ Owner owner;
+ owner.name = "Owner";
+
+ EXPECT_EQ(owner.greet("Reader"), "Hello, Reader, from Owner!");
+}
+
+TEST(ThisPointerCallSyntax, IsADataMemberNotMethod) {
+ Owner owner;
+
+ // owner.greet() calls that member's operator().
+ static_assert(std::is_class_v);
+ static_assert(!std::is_function_v);
+}
+
+} // namespace section_3
+
+// ---------------------------------------------------------------------------
+// 4. What breaks when offset zero is violated, and the guard against it.
+//
+// Everything in sections 2-3 depends on Wrapper being Owner's first data
+// member. This section shows what happens if it isn't: the address a
+// Wrapper method would recover as "the owner" no longer matches the real
+// owner's address, off by exactly sizeof(preceding_member): enough to land
+// on preceding_member's storage instead of Owner's real first member.
+// Every check below computes and compares addresses only; none
+// dereferences the miscomputed pointer, since actually reading through it
+// would be undefined behavior.
+// ---------------------------------------------------------------------------
+namespace section_4 {
+
+struct Wrapper {
+ int tag = 0;
+};
+
+// BadOwner puts another member before wrapper, so wrapper is no longer at
+// offset zero.
+struct BadOwner {
+ int preceding_member = 0;
+ [[no_unique_address]] Wrapper wrapper;
+};
+
+TEST(ThisPointerOffsetViolated, AddressNoLongerMatches) {
+ BadOwner owner;
+
+ EXPECT_NE(static_cast(&owner), static_cast(&owner.wrapper));
+ EXPECT_NE(offsetof(BadOwner, wrapper), 0u);
+}
+
+TEST(ThisPointerOffsetViolated, MisrecoveredAddressLandsOnPrecedingMember) {
+ BadOwner owner;
+
+ // wrong_owner is the address a Wrapper method would recover as "the
+ // owner." Only the distance to real_owner is computed; that address is
+ // never dereferenced.
+ const auto* wrong_owner =
+ static_cast(static_cast(&owner.wrapper));
+ const auto* real_owner =
+ static_cast(static_cast(&owner));
+ EXPECT_EQ(wrong_owner - real_owner,
+ static_cast(sizeof(owner.preceding_member)));
+}
+
+// GoodOwner asserts its layout precondition instead of assuming it: a
+// static_assert here fails at compile time if a future change moves wrapper
+// away from being GoodOwner's first member.
+struct GoodOwner {
+ Wrapper wrapper;
+ int payload = 0;
+};
+
+static_assert(offsetof(GoodOwner, wrapper) == 0,
+ "wrapper must be GoodOwner's first data member for the "
+ "vanishing-this-pointer cast to be valid");
+
+TEST(ThisPointerOffsetViolated, GuardConfirmsGoodCase) {
+ EXPECT_EQ(offsetof(GoodOwner, wrapper), 0u);
+}
+
+} // namespace section_4
+
+// ---------------------------------------------------------------------------
+// 5. Composing several such members through inheritance.
+//
+// Owner in section 3 has one wrapper data member, greet. Giving it a
+// second, shout, the same way needs each in its own small base struct,
+// GreetBase and ShoutBase below, combined by having Owner inherit from
+// both.
+//
+// Each wrapper's own this only has to recover its own base's address:
+// GreetWrapper is GreetBase's sole member, so it sits at offset zero of
+// GreetBase unconditionally, exactly as section 2's Wrapper sits at
+// offset zero of Owner. Getting from GreetBase's address to Owner's is
+// then a base-to-derived static_cast, which the compiler performs
+// correctly regardless of GreetBase's actual offset within Owner.
+// [[no_unique_address]] on greet and shout still matters for size, same
+// as sections 1 and 4, but no longer for correctness: with flat data
+// members, an unhonored [[no_unique_address]] misplaces every member
+// after the first (section 4's failure mode); here it only costs Owner
+// some otherwise-unnecessary bytes. This is the technique Duck (cited
+// above) uses to build its vtable_wrapper, one base per interface member.
+// ---------------------------------------------------------------------------
+namespace section_5 {
+
+struct GreetWrapper {
+ std::string operator()(std::string_view visitor) const;
+};
+
+struct GreetBase {
+ [[no_unique_address]] GreetWrapper greet;
+};
+
+struct ShoutWrapper {
+ std::string operator()(std::string_view visitor) const;
+};
+
+struct ShoutBase {
+ [[no_unique_address]] ShoutWrapper shout;
+};
+
+struct Owner : GreetBase, ShoutBase {
+ std::string name = "World";
+};
+
+std::string GreetWrapper::operator()(std::string_view visitor) const {
+ const auto* base =
+ static_cast(static_cast(this));
+ const auto* owner = static_cast(base);
+ return "Hello, " + std::string(visitor) + ", from " + owner->name + "!";
+}
+
+std::string ShoutWrapper::operator()(std::string_view visitor) const {
+ const auto* base =
+ static_cast(static_cast(this));
+ const auto* owner = static_cast(base);
+ return std::string(visitor) + ", " + owner->name + " SHOUTS HELLO!";
+}
+
+TEST(ThisPointerInheritance, EachMemberReachesTheSameOwner) {
+ Owner owner;
+ owner.name = "Owner";
+
+ EXPECT_EQ(owner.greet("Reader"), "Hello, Reader, from Owner!");
+ EXPECT_EQ(owner.shout("Reader"), "Reader, Owner SHOUTS HELLO!");
+}
+
+TEST(ThisPointerInheritance, NoExtraStorage) {
+ EXPECT_EQ(sizeof(Owner), sizeof(std::string));
+}
+
+} // namespace section_5
diff --git a/tutorials/3_reflection.cc b/tutorials/3_reflection.cc
new file mode 100644
index 0000000..963d3ad
--- /dev/null
+++ b/tutorials/3_reflection.cc
@@ -0,0 +1,661 @@
+/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+==============================================================================*/
+
+// C++26 reflection, from std::meta::info to synthesizing a type at compile
+// time:
+// 1. What std::meta::info is.
+// 2. Splicing an info back into a type.
+// 3. Splicing an info as a call target.
+// 4. Enumerating a type's members.
+// 5. Writing your own consteval helpers about a member.
+// 6. Synthesizing a type's data members with define_aggregate.
+// 7. Converting an enum value to its name with template for.
+// 8. Instantiating a template from infos with substitute.
+// 9. Reading a constant out of an info with extract.
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// ---------------------------------------------------------------------------
+// 1. What std::meta::info is.
+//
+// C++26 reflection (P2996) lets code ask questions about the program at
+// compile time, such as what a type's members are or whether a function is
+// const, and so on. The reflection operator ^^ takes something in the program
+// (a type, a variable, a function, a member...) and lifts it into a
+// std::meta::info value: an opaque, compile-time value describing that
+// thing, distinct from the thing itself (^^int is a value you can pass
+// around, compare, and query; it is not int). This section only
+// establishes that fact; turning an info back into usable code
+// ("splicing") is sections 2 and 3.
+// ---------------------------------------------------------------------------
+namespace section_1 {
+
+struct Point {
+ int x = 0;
+ int y = 0;
+};
+
+TEST(ReflectionMetaInfo, SameEntityEqualValues) {
+ constexpr std::meta::info reflection_of_int = ^^int;
+
+ // ^^int, evaluated twice, describes the same entity both times.
+ static_assert(reflection_of_int == ^^int);
+
+ // Different entities reflect to different, unequal info values.
+ static_assert(^^int != ^^double);
+ static_assert(^^Point != ^^int);
+
+ // std::meta::info values are ordinary compile-time values: they can be
+ // stored in constexpr variables and compared with ==, exactly like any
+ // other type in this respect.
+}
+
+using AliasForPoint = Point;
+
+TEST(ReflectionMetaInfo, AliasesCompareDistinctUntilDealiased) {
+ // ^^ reflects the name as written, not what it eventually names: an
+ // alias and its underlying type are different entities to ^^, even
+ // though AliasForPoint is nothing more than another name for Point.
+ static_assert(^^Point != ^^AliasForPoint);
+
+ // std::meta::dealias strips away any aliasing, giving back the info
+ // you'd get by reflecting the underlying type directly.
+ static_assert(dealias(^^AliasForPoint) == ^^Point);
+}
+
+// ^^ isn't limited to types. It can also reflect a namespace, a function, a
+// specific member, and more. This tutorial mostly needs "reflect a type" and
+// "reflect a member," both shown here just to establish that both work with
+// the same ^^ operator; later sections do more with each.
+int free_function() { return 1; }
+
+TEST(ReflectionMetaInfo, OtherEntityKinds) {
+ constexpr std::meta::info reflected_namespace = ^^std;
+ constexpr std::meta::info reflected_function = ^^free_function;
+ constexpr std::meta::info reflected_member = ^^Point::x;
+
+ // Each ^^ result carries its own entity kind, queryable with a predicate
+ // like std::meta::is_namespace. What you can do with each (splice it as a
+ // type, call it, read its name) depends on this kind, covered starting in
+ // section 2.
+ static_assert(is_namespace(reflected_namespace));
+ static_assert(is_function(reflected_function));
+ static_assert(is_nonstatic_data_member(reflected_member));
+}
+
+} // namespace section_1
+
+// ---------------------------------------------------------------------------
+// 2. Splicing an info back into a type.
+//
+// Section 1 lifted a type into an opaque std::meta::info with ^^. This
+// section lowers it back: typename [:some_info:] names the type that info
+// reflects, wherever a type is expected: a variable declaration, a
+// template argument, anywhere. typename is required because [:some_info:]
+// alone is an expression (the form section 3 uses to splice a value);
+// nothing about info's own type says whether it reflects a type or a
+// value, so the parser needs typename to pick the type-naming parse. This
+// round trip (^^ then [: :]) is called "splicing," and it's how reflection
+// turns a compile-time value describing a type back into an ordinary,
+// usable type.
+// ---------------------------------------------------------------------------
+namespace section_2 {
+
+struct Point {
+ int x = 0;
+ int y = 0;
+};
+
+TEST(ReflectionSpliceType, RoundTripsToOriginal) {
+ constexpr std::meta::info reflection_of_int = ^^int;
+
+ // typename [:reflection_of_int:] names int again, so this line declares an
+ // ordinary int variable, just spelled through a splice.
+ typename[:reflection_of_int:] x = 5;
+ EXPECT_EQ(x, 5);
+
+ // Splicing a struct's reflection produces the exact same type, not merely
+ // a look-alike: std::is_same_v confirms it.
+ static_assert(std::is_same_v);
+}
+
+// A spliced type is usable everywhere an ordinary type name would be,
+// including as a template argument.
+template
+consteval bool is_class_type() {
+ return std::is_class_v;
+}
+
+TEST(ReflectionSpliceType, WorksAsTemplateArgument) {
+ // Storing each info before splicing it, rather than splicing ^^Point
+ // itself, stands in for the ordinary case: the info comes from somewhere
+ // else (a parameter, a search result, ...) and only gets spliced at its
+ // point of use.
+ constexpr std::meta::info point_info = ^^Point;
+ constexpr std::meta::info int_info = ^^int;
+
+ static_assert(is_class_type());
+ static_assert(!is_class_type());
+}
+
+} // namespace section_2
+
+// ---------------------------------------------------------------------------
+// 3. Splicing an info as a call target.
+//
+// find_member, below, searches a type's members at compile time and
+// returns the one it finds, as a std::meta::info identifying that member.
+//
+// A splice can stand in for a member's name in ordinary member access:
+// greeter.[:member_info:] accesses whichever member member_info reflects,
+// exactly as writing that member's name after the dot would. Appending
+// (args) calls it, if that member is a member function; used on its own,
+// with no call, it reads or writes it, if that member is a data member.
+// ---------------------------------------------------------------------------
+namespace section_3 {
+
+struct Greeter {
+ std::string greet(std::string_view name) const {
+ return "Hello, " + std::string(name);
+ }
+
+ int value = 0;
+};
+
+// members_of takes an access_context controlling which members it is
+// allowed to see. access_context::current() applies ordinary C++ access
+// rules as they hold at this point in the source (a member or friend
+// function sees private members here; free-standing code like find_member
+// does not, so private members stay invisible to it, just as they would to
+// ordinary code written at this call site). access_context::unprivileged()
+// always restricts the query to public members, however privileged the
+// calling code actually is, useful for reflecting on a type the way an
+// arbitrary external caller would see it. access_context::unchecked()
+// removes access checking entirely, useful for tooling that must reach
+// every member regardless of access, such as a serializer.
+consteval std::meta::info find_member(std::meta::info type,
+ std::string_view name) {
+ auto const members = members_of(type, std::meta::access_context::current());
+ auto const is_member = [&](std::meta::info member) {
+ return has_identifier(member) && identifier_of(member) == name;
+ };
+
+ if (auto const member = std::ranges::find_if(members, is_member);
+ member != members.end()) {
+ return *member;
+ }
+ // A default-constructed std::meta::info is a valid, comparable sentinel value
+ // that specifically means "nothing here," rather than describing any real
+ // type, function, variable, or namespace.
+ return {};
+}
+
+TEST(ReflectionSpliceCall, MemberFunctionAsTarget) {
+ Greeter greeter;
+ constexpr std::meta::info greet_member = find_member(^^Greeter, "greet");
+
+ // greeter.[:greet_member:](...) calls greet(), found by reflection,
+ // not by writing its name at the call site.
+ EXPECT_EQ(greeter.[:greet_member:]("Reader"), "Hello, Reader");
+ EXPECT_EQ(greeter.[:greet_member:]("Reader"), greeter.greet("Reader"));
+}
+
+TEST(ReflectionSpliceCall, DataMemberAccess) {
+ Greeter greeter;
+ constexpr std::meta::info value_member = find_member(^^Greeter, "value");
+
+ greeter.[:value_member:] = 42;
+ EXPECT_EQ(greeter.value, 42);
+ EXPECT_EQ(greeter.[:value_member:], 42);
+}
+
+} // namespace section_3
+
+// ---------------------------------------------------------------------------
+// 4. Enumerating a type's members.
+//
+// std::meta::members_of and std::meta::nonstatic_data_members_of enumerate a
+// type's members as a std::vector, but that vector exists
+// only during constant evaluation, so it can only be built inside a
+// consteval function. std::define_static_array copies the vector into
+// static storage as a constexpr array, extending its lifetime past the
+// return of that consteval function.
+// ---------------------------------------------------------------------------
+namespace section_4 {
+
+struct Point {
+ int x = 0;
+ int y = 0;
+ int z = 0;
+};
+
+consteval std::vector data_members_of(std::meta::info type) {
+ return nonstatic_data_members_of(type, std::meta::access_context::current());
+}
+
+// define_static_array is what makes the enumerated members usable as a
+// constexpr array outside the consteval function that found them.
+constexpr auto point_data_members =
+ std::define_static_array(data_members_of(^^Point));
+
+TEST(ReflectionEnumerate, MembersInDeclarationOrder) {
+ static_assert(point_data_members.size() == 3);
+ static_assert(identifier_of(point_data_members[0]) == "x");
+ static_assert(identifier_of(point_data_members[1]) == "y");
+ static_assert(identifier_of(point_data_members[2]) == "z");
+}
+
+struct Widget {
+ int value() const { return 1; }
+
+ void set_value(int) {}
+
+ int payload = 0;
+};
+
+consteval std::vector member_functions_of(
+ std::meta::info type) {
+ // is_function alone would also match Widget's implicit special members
+ // (default constructor, destructor, ...); is_special_member_function
+ // excludes those, leaving just the two ordinary methods below.
+ const auto is_member_function = [](std::meta::info member) {
+ return is_function(member) && !is_special_member_function(member);
+ };
+ return members_of(type, std::meta::access_context::current()) |
+ std::ranges::views::filter(is_member_function) |
+ std::ranges::to>();
+}
+
+constexpr auto widget_member_functions =
+ std::define_static_array(member_functions_of(^^Widget));
+
+TEST(ReflectionEnumerate, FilteredToFunctionsOnly) {
+ // payload (a data member) is excluded; value() and set_value(int) (member
+ // functions) are the only two results.
+ static_assert(widget_member_functions.size() == 2);
+ static_assert(identifier_of(widget_member_functions[0]) == "value");
+ static_assert(identifier_of(widget_member_functions[1]) == "set_value");
+}
+
+} // namespace section_4
+
+// ---------------------------------------------------------------------------
+// 5. Writing your own consteval helpers about a member.
+//
+// The standard library's reflection queries (std::meta::is_const,
+// std::meta::parameters_of, std::meta::return_type_of, ...) are small,
+// single-fact primitives. Answering richer questions means building
+// consteval helpers on top of them. This section writes
+// two such helpers: one classifying constness, and one building a
+// signature string that distinguishes a member from other overloads of the
+// same name.
+// ---------------------------------------------------------------------------
+namespace section_5 {
+
+struct Widget {
+ int read() const { return 1; }
+
+ void write(int) {}
+
+ void write(double) {}
+};
+
+// Unlike section 4's member_functions_of, this doesn't also filter out
+// special member functions. The name check below already excludes them,
+// since none of Widget's constructors, destructor, or assignment operators
+// is named "read" or "write".
+consteval std::vector members_named(std::meta::info type,
+ std::string_view name) {
+ std::vector result;
+ for (std::meta::info member :
+ members_of(type, std::meta::access_context::current())) {
+ if (is_function(member) && has_identifier(member) &&
+ identifier_of(member) == name) {
+ result.push_back(member);
+ }
+ }
+ return result;
+}
+
+TEST(ReflectionHelpers, ClassifiesConstCorrectly) {
+ constexpr auto read_candidates =
+ std::define_static_array(members_named(^^Widget, "read"));
+ constexpr auto write_candidates =
+ std::define_static_array(members_named(^^Widget, "write"));
+
+ static_assert(read_candidates.size() == 1);
+ static_assert(is_const(read_candidates[0]));
+
+ static_assert(write_candidates.size() == 2);
+ static_assert(!is_const(write_candidates[0]));
+ static_assert(!is_const(write_candidates[1]));
+}
+
+// A simplified signature-string builder: name plus each parameter type's
+// display string, joined together, turning a member's signature into a
+// distinguishable string. A real version would go on to escape that string
+// into a valid C++ identifier, useful anywhere generated code needs a
+// unique name per overload.
+//
+// A parameter's type isn't a named declaration, so std::meta::identifier_of
+// (which reads a declaration's own name) doesn't apply to it.
+// std::meta::display_string_of is the primitive for turning a type into a
+// readable string. The output of std::meta::display_string_of is
+// implementation-defined and should not be used outside of debug output.
+consteval std::string simple_signature_string(std::meta::info member) {
+ std::string result(identifier_of(member));
+ result += "(";
+ bool first = true;
+ for (std::meta::info parameter : parameters_of(member)) {
+ if (!first) result += ",";
+ first = false;
+ result += display_string_of(dealias(type_of(parameter)));
+ }
+ result += ")";
+ return result;
+}
+
+TEST(ReflectionHelpers, SignatureDistinguishesOverloads) {
+ constexpr auto write_candidates =
+ std::define_static_array(members_named(^^Widget, "write"));
+
+ // define_static_array returns a span sized to exactly the string's own
+ // length, with no guarantee of a trailing null byte, so wrap it in a
+ // string_view (which carries its own length) rather than treating .data()
+ // as a C-string, so comparisons never read past the span's actual bounds.
+ constexpr auto int_overload_signature =
+ std::define_static_array(simple_signature_string(write_candidates[0]));
+ constexpr auto double_overload_signature =
+ std::define_static_array(simple_signature_string(write_candidates[1]));
+ std::string_view int_signature(int_overload_signature.data(),
+ int_overload_signature.size());
+ std::string_view double_signature(double_overload_signature.data(),
+ double_overload_signature.size());
+
+ EXPECT_EQ(int_signature, "write(int)");
+ EXPECT_EQ(double_signature, "write(double)");
+ EXPECT_NE(int_signature, double_signature);
+}
+
+} // namespace section_5
+
+// ---------------------------------------------------------------------------
+// 6. Synthesizing a type's data members with define_aggregate.
+//
+// Every section so far has queried an existing type; this one builds one:
+// std::meta::define_aggregate takes a type that's only been forward-declared
+// so far (no members, incomplete) plus a list of std::meta::data_member_spec
+// values, and gives that type exactly those data members, decided at
+// compile time, not written out as a struct definition in source. This is
+// useful anywhere generated code needs a struct whose members (their count,
+// types, and names) aren't known until compile time, such as a vtable of
+// function pointers with one entry per member some other type happens to
+// declare.
+//
+// define_aggregate is called from inside a `consteval { ... }` block: a
+// statement, not a function, that always runs during translation regardless
+// of where it appears, even directly inside an ordinary, non-constexpr
+// function body, as the first test below does.
+//
+// In C++26, define_aggregate is the only avenue available for code generation.
+// ---------------------------------------------------------------------------
+namespace section_6 {
+
+// Adapted from
+// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html
+//
+// struct data_member_options {
+// optional name; // name_type is a string
+// optional alignment;
+// optional bit_width;
+// bool no_unique_address = false;
+// };
+
+consteval std::vector count_ratio_specs() {
+ std::vector specs;
+ // clang-format off
+ specs.push_back(data_member_spec(^^int, {.name = "count"}));
+ specs.push_back(data_member_spec(^^double, {.name = "ratio"}));
+ // clang-format on
+ return specs;
+}
+
+TEST(ReflectionSynthesize, SynthesizedMembersEnumerable) {
+ struct Incomplete;
+ consteval { define_aggregate(^^Incomplete, count_ratio_specs()); }
+
+ // The consteval block above already ran during translation, so by the
+ // time this ordinary, runtime TEST body executes, Incomplete already has
+ // its two members. Enumerating it works exactly like enumerating an
+ // ordinary, hand-written type (section 4). define_aggregate produces an
+ // ordinary type, usable the same way as a hand-written one.
+ constexpr auto members = std::define_static_array(nonstatic_data_members_of(
+ ^^Incomplete, std::meta::access_context::current()));
+ static_assert(members.size() == 2);
+ static_assert(identifier_of(members[0]) == "count");
+ static_assert(identifier_of(members[1]) == "ratio");
+}
+
+// This test goes further than the one above: it constructs an instance and
+// reads its members back, not just enumerates them. Declaring that instance
+// constexpr forces its construction to happen in a constant expression,
+// showing the synthesized type supports compile-time use like an ordinary
+// type.
+TEST(ReflectionSynthesize, MembersFromSpecList) {
+ struct Incomplete;
+ consteval { define_aggregate(^^Incomplete, count_ratio_specs()); }
+ constexpr Incomplete instance{.count = 3, .ratio = 1.5};
+ static_assert(instance.count == 3 && instance.ratio == 1.5);
+}
+
+} // namespace section_6
+
+// ---------------------------------------------------------------------------
+// 7. Converting an enum value to its name with template for.
+//
+// std::meta::enumerators_of(type) returns every enumerator of an enum
+// type, in declaration order. This is the same shape of query section 4
+// used for a struct's data members, applied to an enum instead. Splicing an
+// enumerator's info ([:e:]) gives back the enum value itself, so a
+// to_string function can compare a runtime value against it directly.
+//
+// `template for` is a compile-time loop: for (constexpr auto e : range)
+// unrolls into ordinary code, once per element, entirely during
+// compilation. This is unlike every for loop earlier in this file, which either
+// ran inside a consteval function (computing a vector for
+// define_static_array to later hand out) or walked an already-computed
+// span at runtime. Here the loop body itself becomes runtime code once per
+// enumerator.
+//
+// enum_to_string below is P2996's own example (Reflection for C++26,
+// "Enum to String"), with one adaptation: enumerators_of returns a
+// std::vector, and only a copy in static storage is
+// usable as template for's range. This is the same reason section 4 needed
+// define_static_array.
+// ---------------------------------------------------------------------------
+namespace section_7 {
+
+enum class Color { Red, Green, Blue };
+
+template
+ requires std::is_enum_v
+std::string enum_to_string(E value) {
+ template for (constexpr std::meta::info e :
+ std::define_static_array(enumerators_of(^^E))) {
+ if (value == [:e:]) return std::string(identifier_of(e));
+ }
+ return "";
+}
+
+TEST(ReflectionEnumToString, NamesEachEnumerator) {
+ EXPECT_EQ(enum_to_string(Color::Red), "Red");
+ EXPECT_EQ(enum_to_string(Color::Green), "Green");
+ EXPECT_EQ(enum_to_string(Color::Blue), "Blue");
+}
+
+TEST(ReflectionEnumToString, UnmatchedValueFallsThrough) {
+ // A value with no matching enumerator falls through every generated
+ // comparison to the fallback return, reached here via an explicit
+ // cast, since Color itself declares no such value.
+ EXPECT_EQ(enum_to_string(static_cast(99)), "");
+}
+
+} // namespace section_7
+
+// ---------------------------------------------------------------------------
+// 8. Instantiating a template from infos with substitute.
+//
+// A splice like typename[:int_info:] needs its operand to be a constant
+// expression, e.g. a constexpr variable, not just any info that happens to
+// exist while a consteval function runs. A range-based for loop over a
+// std::vector doesn't give you that: the loop variable is
+// rebound on each iteration, so it isn't a constant expression, even though
+// the whole loop runs at compile time. A template specialization whose
+// argument list is only known as such a range can't be built one splice at
+// a time. std::meta::substitute(template_info, argument_infos) takes
+// the whole range as an ordinary function argument instead, and returns an
+// info for the resulting specialization directly. Splicing that result, as
+// in section 2, turns it into a usable type.
+// ---------------------------------------------------------------------------
+namespace section_8 {
+
+template
+struct Boxed {
+ T value = T();
+};
+
+TEST(ReflectionSubstitute, InstantiatesTemplateFromInfos) {
+ // clang-format off
+ constexpr std::meta::info boxed_int_info = substitute(^^Boxed, {^^int});
+ // clang-format on
+
+ // The substitution names the same type as writing Boxed directly.
+ static_assert(std::is_same_v>);
+
+ typename[:boxed_int_info:] boxed{.value = 5};
+ EXPECT_EQ(boxed.value, 5);
+}
+
+template
+struct Sized {};
+
+TEST(ReflectionSubstitute, SubstitutesANonTypeTemplateArgument) {
+ // substitute's argument list isn't limited to type infos: reflect_constant
+ // turns an ordinary value into an info for a non-type template argument.
+ // clang-format off
+ constexpr std::meta::info sized_five_info =
+ substitute(^^Sized, {std::meta::reflect_constant(5)});
+ // clang-format on
+ static_assert(std::is_same_v>);
+}
+
+struct Widget {
+ int compute(double) const { return 1; }
+};
+
+template
+using fn_ptr_t = R (*)(Args...);
+
+consteval std::meta::info find_member(std::meta::info type,
+ std::string_view name) {
+ for (std::meta::info member :
+ members_of(type, std::meta::access_context::current())) {
+ if (has_identifier(member) && identifier_of(member) == name) {
+ return member;
+ }
+ }
+ return std::meta::info{};
+}
+
+// A member's return type plus its parameter types, gathered by reflection
+// rather than named in source, are exactly the pieces substitute needs to
+// build the matching function pointer type below.
+consteval std::vector fn_ptr_arguments(
+ std::meta::info member) {
+ std::vector types{dealias(return_type_of(member))};
+ for (std::meta::info parameter : parameters_of(member)) {
+ types.push_back(dealias(type_of(parameter)));
+ }
+ return types;
+}
+
+TEST(ReflectionSubstitute, BuildsFunctionPointerTypeFromAMember) {
+ constexpr std::meta::info compute_member = find_member(^^Widget, "compute");
+ constexpr auto arguments =
+ std::define_static_array(fn_ptr_arguments(compute_member));
+ constexpr std::meta::info fn_ptr_info = substitute(^^fn_ptr_t, arguments);
+
+ static_assert(std::is_same_v);
+}
+
+} // namespace section_8
+
+// ---------------------------------------------------------------------------
+// 9. Reading a constant out of an info with extract.
+//
+// std::meta::extract(info) converts a std::meta::info that reflects a
+// value back into an ordinary T (a real value you can return, store, or
+// pass around), rather than something you can only splice into source
+// code at a fixed spot.
+//
+// member_array_extent, below, takes a struct's info and a member name,
+// finds that member, and reads N off its array type: std::array.
+// std::meta::template_arguments_of gives back N only as an info, held in
+// an ordinary local variable rather than spelled at a splice site, so it
+// can't be spliced. extract is what turns it into an actual size_t.
+// ---------------------------------------------------------------------------
+namespace section_9 {
+
+struct Row {
+ std::array values{};
+};
+
+consteval std::meta::info find_member(std::meta::info type,
+ std::string_view name) {
+ for (std::meta::info member :
+ members_of(type, std::meta::access_context::current())) {
+ if (has_identifier(member) && identifier_of(member) == name) {
+ return member;
+ }
+ }
+ return std::meta::info{};
+}
+
+consteval size_t member_array_extent(std::meta::info type,
+ std::string_view name) {
+ std::meta::info member_type = type_of(find_member(type, name));
+ std::meta::info extent = template_arguments_of(member_type)[1];
+ return extract(extent);
+}
+
+TEST(ReflectionExtract, ReadsANonTypeTemplateArgument) {
+ static_assert(member_array_extent(^^Row, "values") == 4);
+}
+
+} // namespace section_9
diff --git a/tutorials/README.md b/tutorials/README.md
new file mode 100644
index 0000000..a2f45b0
--- /dev/null
+++ b/tutorials/README.md
@@ -0,0 +1,7 @@
+# Tutorials
+
+Three standalone tutorials. Each is a single GoogleTest file whose sections build up one technique step by step.
+
+- [`1_type_erasure.cc`](1_type_erasure.cc) — manual type erasure from first principles: a vtable of function pointers, and an eraser object built from an erased pointer plus a vtable pointer.
+- [`2_vanishing_this_pointer.cc`](2_vanishing_this_pointer.cc) — a layout/casting technique for giving a data member ordinary call syntax, by recovering the address of its owning object from `this`.
+- [`3_reflection.cc`](3_reflection.cc) — C++26 reflection, from `std::meta::info` up to synthesizing a type's members at compile time. Requires a P2996 compiler (`scripts/cmake.sh --reflection`).