diff --git a/.clang-format b/.clang-format
index 1a7aa7a..b59b976 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,5 +1,7 @@
----
-Language: Cpp
+# No `Language: Cpp` key: clang-format's language guesser classifies
+# protocol_reflection.h (C++26 `^^` reflection syntax) as Objective-C, and a
+# Cpp-only configuration makes it error out on that file. A single
+# language-agnostic document applies to every language clang-format detects.
# BasedOnStyle: Google
AccessModifierOffset: -1
AlignAfterOpenBracket: Align
@@ -153,4 +155,3 @@ StatementMacros:
- QT_REQUIRE_VERSION
TabWidth: 8
UseTab: Never
-...
diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml
index bcad3fd..fc3b83f 100644
--- a/.github/workflows/cmake.yml
+++ b/.github/workflows/cmake.yml
@@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- configuration: ["Release", "Debug"]
+ configuration: ["release", "debug"]
settings:
- {
name: "Ubuntu GCC-11",
@@ -211,6 +211,20 @@ 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,
+ },
+ lib: "libstdc++16",
+ implementation: "reflection",
+ }
steps:
- uses: actions/checkout@v4
- uses: seanmiddleditch/gha-setup-ninja@master
@@ -231,9 +245,17 @@ 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
+ echo "CC=gcc-16" >> "$GITHUB_ENV"
+ echo "CXX=g++-16" >> "$GITHUB_ENV"
- 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 }} --implementation=${{ matrix.settings.implementation || 'Python' }}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0626252..f1931ef 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,6 +29,36 @@ 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_ENABLE_REFLECTION_BACKEND
+ "Also build and test the C++26-reflection code-generation \
+backend (protocol_reflection.h). Requires a compiler with P2996 reflection \
+support (GCC 16+ with -freflection). The default Python code-generation \
+path is unaffected."
+ OFF)
+
+if(XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND)
+ 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_ENABLE_REFLECTION_BACKEND 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 +178,47 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT)
target_include_directories(protocol_test
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+ if(XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND)
+ # protocol_test.cc includes the per-interface headers directly (no
+ # Python-generated "generated/protocol_.h") when
+ # XYZ_PROTOCOL_ENABLE_REFLECTION is defined: protocol_reflection.h
+ # synthesizes the same machinery from the plain interface struct at
+ # compile time, so no code generation step or shim headers are needed.
+ xyz_add_test(
+ NAME
+ protocol_test_reflection
+ VERSION
+ 26
+ LINK_LIBRARIES
+ protocol
+ FILES
+ protocol_test.cc
+ protocol_reflection_detail/naming_test.cc
+ protocol_reflection_detail/members_test.cc
+ protocol_reflection_detail/types_test.cc
+ protocol_reflection_detail/thunk_test.cc
+ protocol_reflection_detail/conformance_test.cc
+ protocol_reflection_detail/vtable_layout_test.cc
+ protocol_reflection.h
+ protocol_reflection_detail/naming.h
+ protocol_reflection_detail/members.h
+ protocol_reflection_detail/types.h
+ protocol_reflection_detail/thunk.h
+ protocol_reflection_detail/conformance.h
+ protocol_reflection_detail/vtable_layout.h
+ protocol_reflection_detail/operator_forwarders.h
+ interface_A.h
+ interface_A_Subset.h
+ interface_B.h
+ interface_C.h
+ interface_D.h)
+ target_include_directories(protocol_test_reflection
+ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+ target_compile_options(protocol_test_reflection PRIVATE -freflection)
+ target_compile_definitions(protocol_test_reflection
+ PRIVATE XYZ_PROTOCOL_ENABLE_REFLECTION)
+ endif()
+
add_executable(protocol_benchmark protocol_benchmark.cc)
target_link_libraries(protocol_benchmark PRIVATE protocol benchmark::benchmark)
add_dependencies(protocol_benchmark generate_protocols)
@@ -170,6 +241,23 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT)
--flags=-I${CMAKE_CURRENT_BINARY_DIR}
--flags=-I${CMAKE_CURRENT_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+ if(XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND)
+ # No generated-header include path is needed here (unlike
+ # concept_errors_test above): the reflection backend synthesizes its
+ # machinery from a plain struct at compile time.
+ add_test(
+ NAME concept_errors_test_reflection
+ COMMAND
+ ${Python3_EXECUTABLE} -m pytest
+ ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_concept_errors_reflection.py
+ --compiler=${CMAKE_CXX_COMPILER}
+ --flags=-std=c++26
+ --flags=-freflection
+ --flags=-DXYZ_PROTOCOL_ENABLE_REFLECTION
+ --flags=-I${CMAKE_CURRENT_SOURCE_DIR}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+ endif()
endif()
endif(${BUILD_TESTING})
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4e77735..6abd0f9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,6 +114,21 @@ The `xyz_generate_protocol` CMake macro automates code generation and supports e
This macro ensures that the Python script runs during the CMake configuration
or build phase to generate the necessary C++ source files for the protocol.
+### C++26 Reflection Backend
+
+An opt-in second backend, `protocol_reflection.h`, generates the same
+machinery inside the compiler using C++26 reflection instead of a build step.
+It requires GCC 16+ (`-freflection`) and is enabled with a CMake option:
+
+```bash
+CXX=g++-16 CC=gcc-16 ./scripts/cmake.sh --release \
+ -DXYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND=ON -B build-reflection
+```
+
+This builds an additional test binary, `protocol_test_reflection`, running
+the same tests against the reflection backend. See `implementation-notes.md`
+(section 5) for the backend's design and known limitations.
+
## Performance and Benchmarks
You can measure the performance of member function dispatch, copying, and moving by running the `protocol_benchmark` target via the wrapper script:
@@ -144,6 +159,21 @@ 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, you can experiment with the C++26 reflection backend directly:
+
+```bash
+CXX=g++-16 CC=gcc-16 ./scripts/cmake.sh --release \
+ -DXYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND=ON -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..4e54bbf 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -1,51 +1,44 @@
-# Gemini Project Context: protocol
-
-This file provides project-specific mandates and conventions that override
-general defaults for this repository.
-
-## Engineering Standards
-
-- **C++ Specification:** Target C++20/26. Prioritize value semantics, type
- erasure, and allocator-aware designs consistent with P3019
- (`std::polymorphic`).
-- **Naming Conventions:** NEVER use abbreviations in public or internal variable
- names. Use descriptive names like `XYZ_GENERATE_MANUAL_VTABLE` instead of
- `XYZ_GEN_MAN_VT`.
-- **Stability:** Ensure that generated symbols (e.g., vtable entry names) remain
- deterministic and stable between runs by using MD5 hashing of function
- signatures.
-- **WG21 Style:** `DRAFT.md` must adhere to ISO C++ standardization proposal
- norms
-- **Paper Format:** We use pure Markdown, no YAML frontmatter, and no HTML blocks.
-
-## Workflow Mandates
-
-- **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`.
-- **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.
-- **Verification:** All changes must be verified using the `scripts/cmake.sh`
- script to build and test the implementation.
-- **Sanitizer Verification:** When modifying memory-sensitive or concurrent
- code, verify changes locally using at least one sanitizer (e.g.,
- `./scripts/cmake.sh --asan` or `--tsan`). Note that ASAN, TSAN, and MSAN
- are mutually exclusive.
-- **Post-Change Checks:** Tests and pre-commit checks MUST be run after any
- modifications to the codebase.
-
-## Git Usage
-
-- **Source Control:** This repository uses git.
-- **History Integrity:** NEVER use git commands that affect the git history.
-- **Commit & Branching:** Never commit changes, create, or delete branches.
-- **Human Intervention:** If git commands must be run, you MUST ask for human
- intervention.
-
-## Critical Paths
-
-- Generation Script: `scripts/generate_protocol.py`
-- Proposal Draft: `DRAFT.md`
-- Build Entrypoint: `scripts/cmake.sh`
+# protocol: project conventions
+
+Project-specific mandates that override defaults for this repo.
+
+## Engineering standards
+
+- C++20/26. Prioritize value semantics, type erasure, allocator-aware design
+ (P3019 `std::polymorphic`).
+- No abbreviations in variable names (`XYZ_GENERATE_MANUAL_VTABLE`, not
+ `XYZ_GEN_MAN_VT`).
+- Generated symbols (e.g. vtable entry names) must be deterministic,
+ stable, and overload-disambiguating. The Python backend mangles by
+ hashing the function signature (MD5); the reflection backend instead
+ escapes the signature string byte-for-byte into a valid identifier
+ (`identifier_safe_string`), which is injective by construction rather than
+ probabilistically collision-resistant.
+- `DRAFT.md` follows WG21 proposal norms
+ (). Pure Markdown
+ only — no YAML frontmatter, no HTML.
+
+## Workflow
+
+- Python deps: always `uv run ...`.
+- Build/test only via `scripts/cmake.sh`: `--debug`/`--release`,
+ `--asan`/`--ubsan`/`--tsan`/`--msan` (mutually exclusive),
+ `--implementation=Python|reflection`. `reflection` needs a P2996 compiler
+ (e.g. `CXX=g++-16 CC=gcc-16`) and hard-fails rather than falling back.
+- Prefer Clang 19+ for sanitizers/CI (better MSAN/TSAN than GCC).
+- While iterating, a targeted compile/test is enough. Before calling a
+ change done, run the full suite via `scripts/cmake.sh` plus pre-commit
+ checks. If the change touches allocators or manual object lifetime
+ (clone/move/destroy, pointer casts), also run `--asan --ubsan`. If it
+ touches the vtable registry's shared cache/mutex (`get_mapped_vtable`),
+ also run `--tsan`.
+
+## Git
+
+- Never commit, branch, or run history-altering git commands. Ask the human
+ if one is needed.
+
+## Critical paths
+
+- Generator: `scripts/generate_protocol.py` · Proposal: `DRAFT.md` · Build:
+ `scripts/cmake.sh`
diff --git a/README.md b/README.md
index 4c65884..5f02097 100644
--- a/README.md
+++ b/README.md
@@ -6,14 +6,36 @@
We propose the addition of two class templates, `protocol` and
`protocol_view`, to the C++ Standard Library. Both classes support
-structural-subtyping, `protocol` is owning, `protocol_view` is non-owning.
+structural subtyping: `protocol` is owning (allocator-aware), and
+`protocol_view` is non-owning.
-See [DRAFT.md](DRAFT.md) for more details on design.
+See [DRAFT.md](DRAFT.md) for complete details on the proposal design and library specifications.
-This repository contains both the ISO C++ proposal to add these new library
-types and a reference implementation. The reference implementation is currently
-reliant on a Python code-generation step as C++26 reflection is missing some of
-the features needed to generate code needed by these types at compile time.
+## Implementation Architecture & Backends
+
+This repository contains both the ISO C++ proposal paper and a reference implementation supporting two code-generation backends:
+
+1. Python Build-Step Backend (Default): Uses `libclang` (`py_cppmodel`) and Jinja2 templates ([scripts/protocol.j2](scripts/protocol.j2)) to parse C++ interface headers and generate static vtable specializations at build time. Supported on all target C++20 compilers (GCC 11-14, Clang 17-21, MSVC, Apple Clang).
+2. C++26 Reflection Backend (Opt-In): Uses C++26 reflection ([P2996R13]) via [protocol_reflection.h](protocol_reflection.h) to synthesize vtables, forwarding wrappers, and concepts entirely inside the compiler at compile time without any external Python generation step. Requires GCC 16+ with `-freflection`.
+
+Both backends share the same public API in [protocol.h](protocol.h), support narrowing conversions, and pass the identical test suite. Full removal of the Python code-generation step remains future work as compiler reflection implementations mature.
+
+For deep architectural details, see [implementation-notes.md](implementation-notes.md).
+
+## Quick Start & Building
+
+Ensure you have [CMake](https://cmake.org/), a modern C++ compiler, and [uv](https://docs.astral.sh/uv/) installed.
+
+```bash
+# Build and run tests with default Python backend
+./scripts/cmake.sh
+
+# Build and run tests with the C++26 Reflection backend (requires GCC 16+)
+CXX=g++-16 CC=gcc-16 ./scripts/cmake.sh --release \
+ -DXYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND=ON -B build-reflection
+```
+
+For complete build options, benchmarking, and development practices, see [CONTRIBUTING.md](CONTRIBUTING.md).
## Standardization
@@ -22,14 +44,6 @@ The paper [P4148R2](https://wg21.link/P4148R2.pdf) (derived from
working group in Brno on June 11th 2026. The authors have been encouraged to
continue work.
-## Contributing and Development
-
-For build instructions, testing, contributing guidelines, and a deeper look into
-the code generation architecture, see [CONTRIBUTING.md](CONTRIBUTING.md).
-
-## GitHub codespaces
+## GitHub Codespaces
-Press `.` or visit [https://github.dev/jbcoe/cc-protocol] to open the project in
-an instant, cloud-based, development environment. We have defined a
-[devcontainer](.devcontainer/devcontainer.json) that will automatically install
-the dependencies required to build and test the project.
+Press `.` or visit [https://github.dev/jbcoe/cc-protocol](https://github.dev/jbcoe/cc-protocol) to open the project in an instant, cloud-based development environment. We provide a [devcontainer](.devcontainer/devcontainer.json) pre-configured with all required dependencies, including GCC 16 for C++26 reflection experiments.
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 1f92f6b..6578bf7 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -26,6 +26,16 @@ 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.
+# This is additive: the default g++ (13.3.0) installed above remains the
+# toolchain used for the default build; g++-16/gcc-16 are opt-in, invoked
+# explicitly (e.g. via CMAKE_CXX_COMPILER=g++-16) for the reflection backend.
+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/implementation-notes.md b/implementation-notes.md
index 640e653..3fe6fb4 100644
--- a/implementation-notes.md
+++ b/implementation-notes.md
@@ -1,6 +1,6 @@
# C++ Protocol Reference Implementation Notes
-This document details the design and implementation of the `protocol` and `protocol_view` types, focusing on code generation, virtual dispatch, narrowing conversions, and concurrent safety.
+This document details the design and implementation of the `protocol` and `protocol_view` types, focusing on code generation, virtual dispatch, narrowing conversions, and concurrent safety. Sections 1-4 describe the default, Python/libclang build-step backend; section 5 describes the opt-in C++26-reflection backend, which reuses the narrowing and concurrency machinery in sections 3-4 unchanged.
---
@@ -66,7 +66,7 @@ constexpr protocol_view(const protocol_view& other)
: ptr_(other.ptr_),
vptr_(get_mutable_vtable(other.vptr_)) {}
```
-Conversions are fully transitive (for example, `protocol_view` to `protocol_view` to `protocol_view`). In each step, the registry maps the current vtable pointer to the target interface vtable. Since the mapping registry resolves type transitions directly, intermediate conversions do not create chain-linked redirects.
+Conversions are fully transitive (for example, `protocol_view` to `protocol_view` to `protocol_view`). Each step is an independent call into the registry that maps straight from the vtable pointer it's given to the target interface's vtable, so a multi-step conversion produces a sequence of directly-mapped, independently-cached vtables rather than a chain of vtables that each redirect through the previous one.
### Converting Owning Protocols
Allocator-extended and standard converting constructors construct the target `protocol` from the source `protocol`. If the allocators are equal, the storage pointer `p_` is moved directly (`std::exchange`) and the target vtable is mapped. If the allocators are not equal, the source's `xyz_protocol_move` or `xyz_protocol_clone` function is called to construct the value in the target allocator's storage.
@@ -86,7 +86,7 @@ const void* get_mapped_vtable(
```
### The Cache and Lifetime Control (Intentional Leak)
-Mapped vtables are cached in a static `std::unordered_map` keyed by `CacheKey{source_vtable_pointer, conversion_anchor}`. The `conversion_anchor` is the address of a static template local `conversion_anchor`, ensuring target vtable/allocator uniqueness. Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable.
+Mapped vtables are cached in a static `std::unordered_map` keyed by `CacheKey{source_vtable_pointer, conversion_anchor}`, where `conversion_anchor` is the address of a `static const char conversion_anchor = 0;` local declared inside each of `get_vtable`/`get_mutable_vtable`/`get_owning_vtable`. Those functions are templates, so each `` (or `<..., Allocator>`) instantiation gets its own copy of that local — its address is therefore a stable, unique stand-in for "which target type (and allocator) this conversion is for," without needing RTTI. Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable.
To ensure safety during program shutdown, the cache map and its protecting mutex are initialized as dynamic objects allocated via `new` on the heap and referenced statically (`static auto& cache = *new ...`). This deliberately prevents their destruction during program termination, avoiding Undefined Behavior (such as segfaults) if other global or static objects trigger protocol conversions during cleanup/destructor execution.
@@ -94,7 +94,7 @@ Because active references to these static structures reside in the global data s
Since the vtables are dynamically allocated and retained on the heap until program termination, memory growth is bounded by the total number of distinct conversion type pairs in the binary. This compile-time bound ensures that the cache does not require an eviction policy (such as LRU) or memory cap, as memory consumption remains flat after startup.
-Pointer equality is used to compare the `CacheKey` components. This is safe because static vtable instances and anchor variables are guaranteed to have unique heap or data segment addresses. Compiler optimization techniques (such as COMDAT folding or duplicate variable consolidation) do not affect correctness because identical layouts that are folded share identical function pointer semantics.
+Pointer equality is used to compare the `CacheKey` components. This is safe because static vtable instances and anchor variables are guaranteed to have unique heap or data segment addresses. A linker is free to fold two of these read-only statics into a single address when it can prove they're byte-for-byte identical (identical code folding); that doesn't break the cache, since two definitions only get folded when they were already identical, so folding can't make the cache conflate two conversions that are actually different.
### Split-Lock Pattern
To prevent recursive deadlocks when nested conversions occur (e.g. mapping an owning vtable requires mapping its nested mutable vtable on the same thread), the mutex is not held during mapping.
@@ -111,3 +111,41 @@ The lookup and population sequence is:
7. If the insertion fails (meaning another thread inserted the key concurrently), the local buffer is destroyed, and the already-cached pointer is returned.
This guarantees that all threads always resolve to the identical vtable pointer for a given conversion key, eliminating data races and leaks under high contention.
+
+---
+
+## 5. C++26 Reflection Code-Generation Backend
+
+[protocol_reflection.h](protocol_reflection.h) is an opt-in second backend that generates the same machinery as sections 1-2 above, but inside the compiler at compile time via C++26 reflection ([P2996R13]), instead of ahead of compilation via `libclang` and a Jinja2 template. It requires GCC 16+ (`-std=c++26 -freflection`) and the CMake option `XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND`. [protocol_reflection_guide.md](protocol_reflection_guide.md) is a full section-by-section walkthrough of the header for anyone about to read or modify it; this section gives the shorter design summary in the style of sections 1-4.
+
+### Reflection Primitives
+
+Three operations cover almost all of what the backend needs. `^^Type` lifts a type, function, or data member into a `std::meta::info` value. A library of `consteval std::meta::*` functions (`is_function`, `is_const`, `identifier_of`, `return_type_of`, and similar) then answers ordinary questions about that `info`. A splice, `[:member:]`, lowers an `info` back into code: `object.[:member:]()` calls the member it reflects, and `typename [:type:]` names the type it reflects.
+
+### Interface Enumeration and Vtable Generation
+
+`is_interface_member_function` and `interface_member_functions` enumerate an interface's public, non-static, non-special member functions in declaration order, playing the same role as the AST traversal in section 1 (the same restriction on template member functions applies, for the same reason: no fixed signature to give a vtable slot). Entry names are the member's own signature string (name/operator, parameter types, constness, noexcept), escaped byte-for-byte into a valid identifier: `identifier_safe_string` passes `[a-zA-Z0-9]` through unchanged and replaces everything else, including a literal `_`, with `_` followed by its two-digit hex value. Unlike section 1's MD5 suffix, this isn't a hash: it's injective by construction (no unescaped `_` ever survives from the input, so a `_` in the output always starts an unambiguous two-hex-digit escape), so two different signatures can never collide — a real, if academic, possibility a hash-based scheme (MD5, or this backend's original FNV-1a) can't fully rule out. Names are longer than a hash digest, but that's invisible outside compiler diagnostics.
+
+Rather than rendering a vtable struct from a template, `define_vtable_entries` builds a list of `data_member_spec`s (one function pointer per interface member). `std::meta::define_aggregate` takes a class that has only been forward-declared so far, with no members, plus that list, and gives the class exactly those data members — the reflection equivalent of generating and compiling a struct definition, except the "source" is the list of specs rather than text. `view_vtable` and `owning_vtable` wrap that generated `entries` struct in a handwritten shell that adds the fixed lifetime operations (`xyz_protocol_clone`/`move`/`destroy`), mirroring the two vtable layouts in section 2. Populating a vtable for a given `(Interface, Implementation)` pair resolves each interface member against the stored type (see below) and stores the address of an `erased_call_thunk::call` specialization. That function does the same job as one of section 2's per-type lambdas (`static_cast` the erased pointer back to the concrete type, then call the member on it) but as a plain static member function rather than a lambda, generated once per `(Interface, Implementation, member)` combination, and calling through a splice (`self->[:member:](args...)`) rather than a member name written literally in source.
+
+### Duck-Typed Member Resolution
+
+`resolve_implementation_candidates` finds every member of the implementation type matching an interface member by name (or operator kind) and constness — no parameter or return-type filtering. Each candidate is wrapped in an `implementation_candidate_call` keyed by its *own* signature, with `operator()` qualified const or not according to that specific candidate's own constness (not a single flag shared across the merged set) — the same way Ryan Keane's `rjk::duck` derives a per-candidate `Self` type. This matters: a stored type declaring both `foo()` and `foo() const` merges into ordinary C++ overloading-on-constness (as if both were declared directly in one class), so a non-const interface member correctly prefers the non-const overload instead of the merge becoming ambiguous. All candidates for one interface member are merged into a `candidate_overload_set` via `using Candidates::operator()...` — the same idiom `overloaded_calls` uses for an interface's own overload sets, applied here to the implementation's candidates instead. `make_candidate_overload_set` builds that merged type; `models_reflected_interface` checks it's invocable with the interface member's parameter types via `std::is_invocable_r_v`/`std::is_invocable_v`. That check is what `reflection_protocol_const_concept`/`reflection_protocol_concept` test, the reflection equivalents of the Python backend's generated `protocol_concept_`.
+
+Conformance is exactly what the concept says, and nothing more: a stored type either satisfies `reflection_protocol_concept`/`reflection_protocol_const_concept` or it doesn't, and `protocol`'s constructors are constrained on exactly that. What changed is how the concept checks callability — via real invocability against the merged candidate set, rather than hand-comparing signatures — exactly like the Python backend's `requires`-expression (itself a real call, e.g. `t.method(std::declval()...)`) and Ryan Keane's `rjk::duck` technique (https://ryanjk5.github.io/posts/rjk-duck/). `erased_call_thunk` uses the same merged type for the actual dispatch call, so whatever the concept accepted is exactly what runs.
+
+### Giving Vtable Entries Call Syntax
+
+C++26 reflection cannot splice a reflection as the name of a *new* declaration, so a member function actually named `name` cannot be spliced into existence the way a vtable entry's mangled name can. The backend works around this using the technique from Ryan Keane's `rjk::duck` library: since `define_aggregate` can declare a data member from a plain string, each interface member gets a data member (not a function) named after it, whose type is an empty `forwarding_call` wrapper with an exact-signature `operator()`. Because `a.name()` doesn't distinguish a member function from a data member whose type has `operator()`, this gives ordinary call syntax for free. The wrapper's `operator()` recovers its owning `protocol`/`protocol_view` via `static_cast(static_cast(this))`, valid only because every wrapper sits at offset zero of its owner; `forwarders_at_offset_zero` checks that layout assumption with `std::meta::offset_of` rather than assuming it. Operators (`operator+`, etc.) can't use this trick, since a data member can't be named `operator+`; each operator kind instead gets its own macro-stamped forwarder template with the operator symbol written literally in source (39 invocations, one per supported operator kind, including plain `operator=`).
+
+Assignment merges into `protocol`/`protocol_view` through the same mechanism as every other operator: several interface-declared `operator=` overloads, even of the same constness, resolve correctly through the merged set, exactly like `operator+` already does. Getting there needs one extra step that no other operator needs, though. Every class in the merge chain that doesn't declare any member of its own — `operator_join`, `combined_operator_joins`, and finally `protocol`/`protocol_view` themselves — still gets a compiler-generated `operator=`, whether it declares one explicitly or not. C++ hides an inherited member whenever a class has a member of the same name, and this rule applies even to that compiler-generated one with no source text at all. So each of those classes needs its own `using Base::operator=;`, bringing the inherited one back into scope alongside whatever that class declares itself. This is ordinary C++ name hiding, nothing specific to assignment or to reflection — it just never comes up for any other operator, because `operator=` is the only member name every level of this hierarchy ends up declaring one way or another.
+
+### Narrowing and Concurrency
+
+Sections 3 and 4 above apply unchanged: `protocol_vtable_traits` and `protocol_owning_vtable_traits` specializations plug the reflection-generated vtable types into the same `get_vtable`/`get_mutable_vtable`/`get_owning_vtable` registry that section 4 describes, so narrowing conversions, the vtable cache, and its concurrency guarantees are identical regardless of which backend produced the vtable being narrowed.
+
+### Known Limitations
+
+Interface members must be direct members of the stored type — members inherited from a base of the implementation are not found. An interface member function named `swap` or `valueless_after_move` would be silently hidden by `protocol`'s own member of that name (since the generated forwarders are inherited data members), so it is instead rejected with a `static_assert` naming the interface. Generated member functions are not marked `constexpr`, which is out of scope for this backend for now.
+
+[P2996R13]: https://isocpp.org/files/papers/P2996R13.html
diff --git a/interface_D.h b/interface_D.h
index de55025..22d7c66 100644
--- a/interface_D.h
+++ b/interface_D.h
@@ -17,6 +17,7 @@ struct D {
int operator~() const;
bool operator!() const;
void operator=(int x);
+ void operator=(double x);
bool operator<(int x) const;
bool operator>(int x) const;
void operator+=(int x);
diff --git a/protocol.h b/protocol.h
index f4d0403..17913c6 100644
--- a/protocol.h
+++ b/protocol.h
@@ -29,7 +29,7 @@ namespace xyz {
template
struct is_protocol : std::false_type {};
-template
+template >
class protocol;
template
@@ -58,7 +58,8 @@ const void* get_mapped_vtable(const void* source_vtable_pointer,
void* target));
template
-const typename protocol_vtable_traits::const_vtable* get_vtable(
+const typename protocol_vtable_traits::const_vtable*
+get_const_vtable(
const typename protocol_vtable_traits::const_vtable*
source_vtable_pointer) {
using FromVtable =
@@ -68,8 +69,8 @@ const typename protocol_vtable_traits::const_vtable* get_vtable(
static const char conversion_anchor = 0;
auto mapping_function = [](const void* source, void* target) {
- map_vtable_members(static_cast(source),
- static_cast(target));
+ map_const_vtable_members(static_cast(source),
+ static_cast(target));
};
return static_cast(
@@ -78,7 +79,7 @@ const typename protocol_vtable_traits::const_vtable* get_vtable(
}
template
-const typename protocol_vtable_traits::vtable* get_mutable_vtable(
+const typename protocol_vtable_traits::vtable* get_vtable(
const typename protocol_vtable_traits::vtable*
source_vtable_pointer) {
using FromVtable = typename protocol_vtable_traits::vtable;
@@ -87,8 +88,8 @@ const typename protocol_vtable_traits::vtable* get_mutable_vtable(
static const char conversion_anchor = 0;
auto mapping_function = [](const void* source, void* target) {
- map_mutable_vtable_members(static_cast(source),
- static_cast(target));
+ map_vtable_members(static_cast(source),
+ static_cast(target));
};
return static_cast(
@@ -123,7 +124,8 @@ get_owning_vtable(const typename protocol_owning_vtable_traits<
sizeof(ToVtable), mapping_function));
}
-template >
+#ifndef XYZ_PROTOCOL_ENABLE_REFLECTION
+template
class protocol {
static_assert(
sizeof(T) == 0,
@@ -164,7 +166,15 @@ class protocol_view {
"Note: protocol_view specializations are automatically generated "
"alongside protocol specializations.");
};
+#endif
} // namespace xyz
+// If reflection is enabled, the real xyz::protocol / xyz::protocol_view
+// implementations come from protocol_reflection.h instead of the
+// placeholder definitions above.
+#ifdef XYZ_PROTOCOL_ENABLE_REFLECTION
+#include "protocol_reflection.h"
+#endif
+
#endif // XYZ_PROTOCOL_H_
diff --git a/protocol_reflection.h b/protocol_reflection.h
new file mode 100644
index 0000000..8a9a78d
--- /dev/null
+++ b/protocol_reflection.h
@@ -0,0 +1,1041 @@
+/* Copyright (c) 2026 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 code-generation backend for xyz::protocol /
+// xyz::protocol_view. Instead of generating a per-interface header at build
+// time, this header generates the same machinery inside the compiler using
+// P2996 reflection. Any plain struct, class, or template instantiation works
+// as an interface type automatically; no macro, build step, or per-type
+// opt-in annotation is required.
+//
+// Requires GCC 16+ with `-std=c++26 -freflection`; fails to compile
+// otherwise, rather than silently no-op'ing. Defines xyz::protocol /
+// xyz::protocol_view as primary templates (protocol.h's own placeholder
+// primary templates must therefore not also be defined when this header
+// is included, or the two would conflict; see protocol.h for how it
+// arranges that).
+//
+// Conformance to an interface is checked by a concept
+// (reflection_protocol_concept / reflection_protocol_const_concept),
+// exactly as strictly as any C++ concept: a stored type either satisfies
+// it or it doesn't, and protocol's constructors are constrained on
+// exactly that. The concept is implemented via real invocability against
+// the stored type's own overload set -- every candidate with the
+// interface member's name is merged into one callable type and the
+// compiler decides what's callable -- rather than this backend
+// hand-comparing parameter types. This mirrors the Python backend's
+// `requires`-expression-based concept (itself a real call, e.g.
+// `t.method(std::declval()...)`) and Ryan Keane's rjk::duck technique
+// (https://ryanjk5.github.io/posts/rjk-duck/).
+//
+// Documented, deliberate limitations of this backend:
+// - Interface members must be implemented as direct members of the stored
+// type: member functions inherited from a base class of the implementation
+// are not found by the reflection-based member resolution.
+// - An interface member function named `swap` or `valueless_after_move`
+// would be hidden by protocol's own member of that name (undetectable via
+// the forwarders, which are inherited), so it is instead rejected with a
+// static_assert naming the interface.
+// - Generated member functions are not marked constexpr (out of scope for
+// this backend for now).
+#ifndef XYZ_PROTOCOL_REFLECTION_H_
+#define XYZ_PROTOCOL_REFLECTION_H_
+
+#ifndef __cpp_impl_reflection
+#error \
+ "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)."
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "protocol.h"
+#include "protocol_reflection_detail/conformance.h"
+#include "protocol_reflection_detail/members.h"
+#include "protocol_reflection_detail/naming.h"
+#include "protocol_reflection_detail/operator_forwarders.h"
+#include "protocol_reflection_detail/thunk.h"
+#include "protocol_reflection_detail/types.h"
+#include "protocol_reflection_detail/vtable_layout.h"
+
+namespace xyz {
+
+namespace reflection_detail {
+
+using std::meta::info;
+
+// ---------------------------------------------------------------------------
+// Vtable instances per (interface, implementation) pair
+// ---------------------------------------------------------------------------
+
+// Allocate storage for one Implementation via a rebound copy of `allocator`
+// and construct it from `arguments`, deallocating if construction throws.
+// Shared by the vtable lifetime operations and protocol's constructors.
+template
+void* allocate_and_construct(const Allocator& allocator,
+ Arguments&&... arguments) {
+ using implementation_allocator = typename std::allocator_traits<
+ Allocator>::template rebind_alloc;
+ using implementation_allocator_traits =
+ std::allocator_traits;
+ implementation_allocator rebound_allocator(allocator);
+ auto memory = implementation_allocator_traits::allocate(rebound_allocator, 1);
+ try {
+ implementation_allocator_traits::construct(
+ rebound_allocator, memory, std::forward(arguments)...);
+ return memory;
+ } catch (...) {
+ implementation_allocator_traits::deallocate(rebound_allocator, memory, 1);
+ throw;
+ }
+}
+
+// The allocator-aware lifetime operations, forming the fixed part of
+// vtable_impl.
+template
+struct allocator_lifetime {
+ using implementation_allocator = typename std::allocator_traits<
+ Allocator>::template rebind_alloc;
+ using implementation_allocator_traits =
+ std::allocator_traits;
+
+ static void* clone(void* erased, const Allocator& allocator) {
+ return allocate_and_construct(
+ allocator, *static_cast(erased));
+ }
+
+ static void* move_construct(void* erased, const Allocator& allocator) {
+ return allocate_and_construct(
+ allocator, std::move(*static_cast(erased)));
+ }
+
+ static void destroy(void* erased, const Allocator& allocator) {
+ auto* self = static_cast(erased);
+ implementation_allocator rebound_allocator(allocator);
+ implementation_allocator_traits::destroy(rebound_allocator, self);
+ implementation_allocator_traits::deallocate(rebound_allocator, self, 1);
+ }
+};
+
+// One vtable entry value: the address of the exactly-typed thunk for the
+// Index-th interface member in the given selection.
+template
+consteval auto make_vtable_entry() {
+ constexpr info member = interface_members[Index];
+ constexpr bool ConstErased = std::meta::is_const(member);
+ constexpr info merged =
+ make_candidate_overload_set(^^Implementation, member, ConstErased);
+ constexpr info erased_pointer_type = ConstErased ? ^^const void* : ^^void*;
+ using ThunkPointer = [:vtable_entry_pointer_type(member,
+ erased_pointer_type):];
+ if constexpr (merged == info{}) {
+ return ThunkPointer(nullptr);
+ } else {
+ using Signature = [:member_function_type(member):];
+ return ThunkPointer(
+ &erased_call_thunk::call);
+ }
+}
+
+template
+consteval auto make_view_entries(std::index_sequence) {
+ return typename view_vtable::view_entries{
+ make_vtable_entry()...};
+}
+
+template
+consteval auto make_owning_entries(std::index_sequence) {
+ return typename owning_vtable::owning_entries{
+ make_vtable_entry()...};
+}
+
+template
+inline constexpr typename view_vtable::vtable view_vtable_for = {
+ make_view_entries(
+ std::make_index_sequence.size()>())};
+
+template
+inline constexpr
+ typename owning_vtable::vtable owning_vtable_for = {
+ &allocator_lifetime::clone,
+ &allocator_lifetime::move_construct,
+ &allocator_lifetime::destroy,
+ &view_vtable_for,
+ make_owning_entries(
+ std::make_index_sequence.size()>())};
+
+// ---------------------------------------------------------------------------
+// Vtable narrowing maps, found by ADL from protocol.h's get_const_vtable /
+// get_vtable / get_owning_vtable. Entries are copied by name: every entry of
+// the target vtable must exist, identically named (same signature hash), in
+// the source vtable — which is exactly the subset relationship narrowing
+// conversions rely on.
+// ---------------------------------------------------------------------------
+
+template
+void copy_vtable_entries(const FromEntries& from, ToEntries& to) {
+ static constexpr auto to_members =
+ std::define_static_array(std::meta::nonstatic_data_members_of(
+ ^^ToEntries, std::meta::access_context::current()));
+ template for (constexpr info to_member : to_members) {
+ constexpr info from_member =
+ data_member_named(^^FromEntries, std::meta::identifier_of(to_member));
+ static_assert(from_member != info{},
+ "reflection backend: narrowing conversion requires every "
+ "target interface member to exist in the source interface "
+ "with an identical signature");
+ to.[:to_member:] = from.[:from_member:];
+ }
+}
+
+template
+concept reflection_view_vtable =
+ requires { typename Vtable::xyz_reflection_view_vtable_tag; };
+
+template
+concept reflection_owning_vtable =
+ requires { typename Vtable::xyz_reflection_owning_vtable_tag; };
+
+template
+void map_const_vtable_members(const FromVtable* from, ToVtable* to) {
+ copy_vtable_entries(from->entries, to->entries);
+}
+
+template
+void map_vtable_members(const FromVtable* from, ToVtable* to) {
+ copy_vtable_entries(from->entries, to->entries);
+}
+
+template
+void map_owning_vtable_members(const FromVtable* from, ToVtable* to) {
+ to->xyz_protocol_clone = from->xyz_protocol_clone;
+ to->xyz_protocol_move = from->xyz_protocol_move;
+ to->xyz_protocol_destroy = from->xyz_protocol_destroy;
+ to->view_vt = get_vtable(from->view_vt);
+ copy_vtable_entries(from->entries, to->entries);
+}
+
+// ---------------------------------------------------------------------------
+// Forwarding call wrappers
+//
+// GCC 16 cannot splice a reflection as the declarator-id of a new member
+// function, so per-method forwarders are synthesized as data members (named
+// by the interface member's identifier) whose type overloads operator() with
+// the exact interface signature. Overloaded interface names become one data
+// member whose type merges one wrapper per overload.
+//
+// Recovering the owning protocol / protocol_view object from inside that
+// wrapper's operator() is two ordinary, always-well-defined casts, not one
+// address-equality assumption. named_forwarders::single_member_wrapper holds exactly one
+// data member: that group's (possibly overload-merged) forwarding_call. A
+// standard-layout class with exactly one non-static data member shares that
+// member's address ([class.mem]), so casting from the member back to its
+// enclosing single_member_wrapper via void* is always well-defined, with no
+// [[no_unique_address]] or layout hoping involved. From there, casting up
+// from single_member_wrapper to Owner is an ordinary base-to-derived
+// static_cast: Owner (transitively, through named_forwarders<...>::type =
+// forwarder_group, ...>) really does derive from
+// every single_member_wrapper, so the compiler applies whatever pointer
+// adjustment that base's actual offset needs -- offset zero is never
+// required or assumed for this step. This mirrors Duck's trace_to_duck
+// (https://github.com/RyanJK5/rjk-duck), whose vtable_function recovers its
+// owning duck the same two-step way, through its own vtable_function_wrapper
+// (Duck's equivalent of single_member_wrapper, hand-written there rather
+// than reflected, since Duck doesn't need one generated per interface member
+// of an arbitrary type at compile time).
+//
+// define_aggregate can only run from inside a `consteval { ... }` block, and
+// that block cannot have another class's scope intervening between it and
+// the incomplete type it completes -- so single_member_wrapper is nested
+// inside named_forwarders itself (one uniquely-named member, one nested
+// class, completed by the consteval block right below it, sharing its
+// scope), rather than living at namespace scope alongside forwarder_group.
+// ---------------------------------------------------------------------------
+
+template
+struct forwarding_call;
+
+template
+struct forwarding_call {
+ ReturnType operator()(ParameterTypes... parameters) noexcept(IsNoexcept) {
+ void* voided = this;
+ auto* wrapper =
+ static_cast*>(voided);
+ auto* owner = static_cast(wrapper);
+ return owner->template dispatch_reflected_member(
+ std::forward(parameters)...);
+ }
+};
+
+template
+struct forwarding_call {
+ ReturnType operator()(ParameterTypes... parameters) const
+ noexcept(IsNoexcept) {
+ const void* voided = this;
+ const auto* wrapper =
+ static_cast*>(
+ voided);
+ const auto* owner =
+ static_cast(wrapper);
+ return owner->template dispatch_reflected_member(
+ std::forward(parameters)...);
+ }
+};
+
+template
+struct overloaded_calls : Overloads... {
+ using Overloads::operator()...;
+};
+
+// Interface members grouped for forwarder generation, preserving declaration
+// order within and across groups: named members group by identifier,
+// operator members by operator kind. Each call selects one population via
+// group_operators — the two populations need different downstream mechanisms
+// (a data member cannot be named `operator+`), but share this grouping.
+consteval std::vector> grouped_interface_members(
+ const std::vector& members, bool group_operators) {
+ std::vector> groups;
+ std::vector grouped(members.size(), false);
+ for (std::size_t index = 0; index < members.size(); ++index) {
+ if (grouped[index]) continue;
+ if (std::meta::has_identifier(members[index]) == group_operators) continue;
+ std::vector group;
+ for (std::size_t other = index; other < members.size(); ++other) {
+ if (grouped[other]) continue;
+ if (std::meta::has_identifier(members[other]) == group_operators) {
+ continue;
+ }
+ bool same_group = group_operators
+ ? std::meta::operator_of(members[other]) ==
+ std::meta::operator_of(members[index])
+ : std::meta::identifier_of(members[other]) ==
+ std::meta::identifier_of(members[index]);
+ if (!same_group) continue;
+ grouped[other] = true;
+ group.push_back(members[other]);
+ }
+ groups.push_back(group);
+ }
+ return groups;
+}
+
+// Combines one single_member_wrapper per uniquely-named interface member
+// through ordinary multiple inheritance.
+template
+struct forwarder_group : public Wrappers... {};
+
+// One base per uniquely-named interface member (operators are handled
+// separately, since a data member cannot be named `operator+`), combined
+// via forwarder_group. ForceConstCall makes every wrapper's operator() const
+// regardless of the interface member's constness, matching the generated
+// protocol_view classes whose forwarders are all const.
+template
+struct named_forwarders {
+ using owner_type = Owner;
+
+ // GroupKeyMember is a group's first interface member: its own info both
+ // identifies the group and, via identifier_of, names the data member
+ // below. Declared here, completed by the consteval block that follows,
+ // sharing this scope as define_aggregate requires.
+ template
+ struct single_member_wrapper;
+
+ consteval static info wrapper_type_for(const std::vector& overloads) {
+ std::vector overload_wrappers;
+ for (info member : overloads) {
+ overload_wrappers.push_back(std::meta::substitute(
+ ^^forwarding_call,
+ {
+ ^^named_forwarders, std::meta::reflect_constant(member),
+ std::meta::reflect_constant(overloads.front()),
+ member_function_type(member),
+ std::meta::reflect_constant(ForceConstCall ||
+ std::meta::is_const(member)),
+ std::meta::reflect_constant(std::meta::is_noexcept(member))}));
+ }
+ return overload_wrappers.size() == 1
+ ? overload_wrappers.front()
+ : std::meta::substitute(^^overloaded_calls, overload_wrappers);
+ }
+
+ consteval {
+ for (const std::vector& overloads : grouped_interface_members(
+ interface_member_functions(^^Interface, ConstOnly), false)) {
+ std::meta::define_aggregate(
+ std::meta::substitute(
+ ^^single_member_wrapper,
+ {
+ std::meta::reflect_constant(overloads.front())}),
+ {std::meta::data_member_spec(
+ wrapper_type_for(overloads),
+ {.name = std::meta::identifier_of(overloads.front()),
+ .no_unique_address = true})});
+ }
+ }
+
+ consteval static std::vector bases() {
+ std::vector result;
+ for (const std::vector& overloads : grouped_interface_members(
+ interface_member_functions(^^Interface, ConstOnly), false)) {
+ result.push_back(std::meta::substitute(
+ ^^single_member_wrapper,
+ {
+ std::meta::reflect_constant(overloads.front())}));
+ }
+ return result;
+ }
+
+ using type = typename[:std::meta::substitute(^^forwarder_group, bases()):];
+};
+
+// ---------------------------------------------------------------------------
+// Operator forwarding
+//
+// A data member cannot be named `operator+`, so interface operators cannot
+// use the named-forwarder mechanism above. Instead, operator_forwarder and
+// operator_join (protocol_reflection_detail/operator_forwarders.h) are
+// generated once per operator kind offline, rather than reimplemented here:
+// forwarding an operator needs a real `operator` declaration, with
+// the symbol itself a literal token in source, the same limitation
+// https://github.com/RyanJK5/rjk-duck works around with its own
+// generate_operators.py. The per-interface set of operator forwarders is
+// combined into an empty base class of protocol / protocol_view below.
+//
+// operator= needs one extra thing the other operators don't: protocol and
+// protocol_view each need their own operator= (hand-written for value
+// semantics, or left to the compiler to generate), and in C++ a class's own
+// operator=, even a compiler-generated one, hides any operator= it would
+// otherwise inherit from a base class. So every class in this merge chain
+// that doesn't declare a member of its own needs an explicit
+// using-declaration bringing the inherited operator= back into scope:
+// combined_operator_joins below, and each of protocol, protocol_view, and protocol_view further down this file.
+// ---------------------------------------------------------------------------
+
+// Every level of this merge hierarchy that doesn't declare its own members
+// gets an implicitly-declared operator= of its own (ordinary C++), which
+// would otherwise hide any operator= a Join brings in from its Forwarders --
+// hence the using-declaration below, mirroring the one operator_join itself
+// already needs for the same reason.
+template
+struct combined_operator_joins : Joins... {
+ using Joins::operator=...;
+};
+
+consteval info make_operator_forwarders(info interface_type, info owner_type,
+ bool const_only,
+ bool force_const_call) {
+ std::vector joins;
+ for (const std::vector& overloads : grouped_interface_members(
+ interface_member_functions(interface_type, const_only), true)) {
+ std::meta::operators kind = std::meta::operator_of(overloads.front());
+ std::vector join_arguments;
+ join_arguments.push_back(std::meta::reflect_constant(kind));
+ for (info member : overloads) {
+ join_arguments.push_back(std::meta::substitute(
+ ^^operator_forwarder,
+ {
+ owner_type, std::meta::reflect_constant(member),
+ member_function_type(member), std::meta::reflect_constant(kind),
+ std::meta::reflect_constant(force_const_call ||
+ std::meta::is_const(member)),
+ std::meta::reflect_constant(std::meta::is_noexcept(member))}));
+ }
+ joins.push_back(std::meta::substitute(^^operator_join, join_arguments));
+ }
+ return std::meta::substitute(^^combined_operator_joins, joins);
+}
+
+template
+struct operator_forwarders {
+ using type = typename[:make_operator_forwarders(^^Interface, ^^Owner,
+ ConstOnly, ForceConstCall):];
+};
+
+// Every forwarder base subobject, every forwarder data member inside those
+// bases (the named forwarding wrappers), and their bases recursively are
+// expected to collapse to offset zero of the owning object: forwarding_call
+// and operator_forwarder never hold state, so every wrapper is empty and
+// [[no_unique_address]] should let them all overlap. Neither the two-step
+// cast in forwarding_call nor the direct static_cast(this) in
+// operator_forwarder depends on that collapse for correctness: both apply
+// whatever adjustment the real offset needs. But [[no_unique_address]] is
+// only a request, not a guarantee, so this asserts the collapse happened
+// rather than silently paying for padding an implementation is free to
+// insert if it ever doesn't.
+consteval bool forwarders_at_offset_zero(info class_type) {
+ for (info base :
+ std::meta::bases_of(class_type, std::meta::access_context::current())) {
+ if (std::meta::offset_of(base).bytes != 0) return false;
+ info base_type = std::meta::dealias(std::meta::type_of(base));
+ for (info data_member : std::meta::nonstatic_data_members_of(
+ base_type, std::meta::access_context::current())) {
+ if (std::meta::offset_of(data_member).bytes != 0) return false;
+ }
+ if (!forwarders_at_offset_zero(base_type)) return false;
+ }
+ return true;
+}
+
+} // namespace reflection_detail
+
+// ---------------------------------------------------------------------------
+// Registry hookup: matches the shape expected by get_const_vtable /
+// get_vtable / get_owning_vtable in protocol.h, so they work unmodified
+// regardless of backend.
+// ---------------------------------------------------------------------------
+
+template
+struct protocol_vtable_traits {
+ using const_vtable = typename reflection_detail::view_vtable::vtable;
+ using vtable = typename reflection_detail::view_vtable::vtable;
+};
+
+template
+struct protocol_owning_vtable_traits {
+ using vtable =
+ typename reflection_detail::owning_vtable::vtable;
+};
+
+// ---------------------------------------------------------------------------
+// protocol — primary template definition. The constructors,
+// assignment, swap, and destructor are hand-written here (define_aggregate
+// can only produce data members, so this part isn't reflection-generated);
+// the per-interface concepts and vtable types they use are the reflection
+// equivalents built above. Named-method forwarding comes from the
+// named_forwarders empty base.
+// ---------------------------------------------------------------------------
+
+template
+class protocol
+ : public reflection_detail::named_forwarders,
+ false, false>::type,
+ public reflection_detail::operator_forwarders,
+ false, false>::type {
+ friend class protocol_view;
+ friend class protocol_view;
+ template
+ friend class protocol;
+ template
+ friend struct protocol_owning_vtable_traits;
+ template
+ friend struct reflection_detail::forwarding_call;
+ template
+ friend struct reflection_detail::operator_forwarder;
+
+ static_assert(!reflection_detail::has_reserved_interface_member_name(^^T),
+ "xyz::protocol: interface must not declare a member function "
+ "named 'swap' or 'valueless_after_move' - these names are "
+ "reserved for protocol's own public members and would be "
+ "silently hidden by ordinary C++ name-hiding rules");
+ static_assert(
+ !reflection_detail::has_rvalue_qualified_interface_member(^^T),
+ "xyz::protocol: interface must not declare an rvalue-qualified (&&) "
+ "member function - this backend does not support them");
+
+ using vtable =
+ typename reflection_detail::owning_vtable::vtable;
+
+ template
+ decltype(auto) dispatch_reflected_member(Arguments&&... arguments) {
+ static_assert(reflection_detail::forwarders_at_offset_zero(^^protocol));
+ constexpr std::meta::info entry = reflection_detail::data_member_named(
+ ^^typename reflection_detail::owning_vtable::owning_entries,
+ reflection_detail::vtable_entry_name(Member));
+ return vtable_->entries.[:entry:](p_,
+ std::forward(arguments)...);
+ }
+
+ template
+ decltype(auto) dispatch_reflected_member(Arguments&&... arguments) const {
+ static_assert(reflection_detail::forwarders_at_offset_zero(^^protocol));
+ constexpr std::meta::info entry = reflection_detail::data_member_named(
+ ^^typename reflection_detail::owning_vtable::owning_entries,
+ reflection_detail::vtable_entry_name(Member));
+ return vtable_->entries.[:entry:](p_,
+ std::forward(arguments)...);
+ }
+
+ using allocator_traits = std::allocator_traits;
+
+ template
+ [[nodiscard]] void* create_storage(Ts&&... ts) const {
+ return reflection_detail::allocate_and_construct(
+ alloc_, std::forward(ts)...);
+ }
+
+ void* p_;
+ const vtable* vtable_;
+ [[no_unique_address]] Allocator alloc_;
+
+ public:
+ template
+ requires(!std::same_as)
+ protocol(protocol&& other) noexcept(
+ allocator_traits::is_always_equal::value)
+ : alloc_(other.alloc_) {
+ if (alloc_ == other.alloc_) {
+ p_ = std::exchange(other.p_, nullptr);
+ vtable_ = get_owning_vtable(
+ std::exchange(other.vtable_, nullptr));
+ } else {
+ if (!other.valueless_after_move()) {
+ p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_);
+ vtable_ = get_owning_vtable(other.vtable_);
+ other.vtable_->xyz_protocol_destroy(other.p_, other.alloc_);
+ other.p_ = nullptr;
+ other.vtable_ = nullptr;
+ } else {
+ p_ = nullptr;
+ vtable_ = nullptr;
+ }
+ }
+ }
+
+ template
+ requires(!std::same_as)
+ protocol(const protocol& other)
+ : alloc_(allocator_traits::select_on_container_copy_construction(
+ other.alloc_)) {
+ if (!other.valueless_after_move()) {
+ p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_);
+ vtable_ = get_owning_vtable(other.vtable_);
+ } else {
+ p_ = nullptr;
+ vtable_ = nullptr;
+ }
+ }
+
+ template
+ requires(!std::same_as)
+ protocol(std::allocator_arg_t, const Allocator& alloc,
+ const protocol& other)
+ : alloc_(alloc) {
+ if (!other.valueless_after_move()) {
+ p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_);
+ vtable_ = get_owning_vtable(other.vtable_);
+ } else {
+ p_ = nullptr;
+ vtable_ = nullptr;
+ }
+ }
+
+ template
+ requires(!std::same_as)
+ protocol(std::allocator_arg_t, const Allocator& alloc,
+ protocol&&
+ other) noexcept(allocator_traits::is_always_equal::value)
+ : alloc_(alloc) {
+ if (alloc_ == other.alloc_) {
+ p_ = std::exchange(other.p_, nullptr);
+ vtable_ = get_owning_vtable(
+ std::exchange(other.vtable_, nullptr));
+ } else {
+ if (!other.valueless_after_move()) {
+ p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_);
+ vtable_ = get_owning_vtable(other.vtable_);
+ other.vtable_->xyz_protocol_destroy(other.p_, other.alloc_);
+ other.p_ = nullptr;
+ other.vtable_ = nullptr;
+ } else {
+ p_ = nullptr;
+ vtable_ = nullptr;
+ }
+ }
+ }
+
+ explicit protocol()
+ requires std::default_initializable &&
+ reflection_protocol_concept && std::copy_constructible
+ : protocol(std::allocator_arg_t{}, Allocator{}) {}
+
+ template
+ explicit protocol(U&& u)
+ requires(!std::same_as>) &&
+ not_protocol_or_view &&
+ std::copy_constructible> &&
+ reflection_protocol_concept
+ : protocol(std::allocator_arg_t{}, Allocator{}, std::forward(u)) {}
+
+ template
+ explicit protocol(std::in_place_type_t, Ts&&... ts)
+ requires std::same_as, U> &&
+ not_protocol_or_view && std::constructible_from &&
+ std::copy_constructible &&
+ std::default_initializable &&
+ reflection_protocol_concept
+ : protocol(std::allocator_arg_t{}, Allocator{}, std::in_place_type,
+ std::forward(ts)...) {}
+
+ template
+ explicit protocol(std::in_place_type_t, std::initializer_list ilist,
+ Ts&&... ts)
+ requires std::same_as, U> &&
+ not_protocol_or_view &&
+ std::constructible_from, Ts&&...> &&
+ std::copy_constructible &&
+ std::default_initializable &&
+ reflection_protocol_concept
+ : protocol(std::allocator_arg_t{}, Allocator{}, std::in_place_type,
+ ilist, std::forward(ts)...) {}
+
+ protocol(const protocol& other)
+ : protocol(std::allocator_arg_t{},
+ allocator_traits::select_on_container_copy_construction(
+ other.alloc_),
+ other) {}
+
+ protocol(protocol&& other) noexcept(allocator_traits::is_always_equal::value)
+ : protocol(std::allocator_arg_t{}, other.alloc_, std::move(other)) {}
+
+ explicit protocol(std::allocator_arg_t, const Allocator& alloc)
+ requires std::default_initializable && std::copy_constructible
+ : alloc_(alloc) {
+ p_ = create_storage();
+ vtable_ = &reflection_detail::owning_vtable_for;
+ }
+
+ template
+ explicit protocol(std::allocator_arg_t, const Allocator& alloc, U&& u)
+ requires(!std::same_as>) &&
+ not_protocol_or_view &&
+ std::copy_constructible> &&
+ reflection_protocol_concept
+ : alloc_(alloc) {
+ p_ = create_storage>(std::forward(u));
+ vtable_ = &reflection_detail::owning_vtable_for>;
+ }
+
+ template
+ explicit protocol(std::allocator_arg_t, const Allocator& alloc,
+ std::in_place_type_t, Ts&&... ts)
+ requires std::same_as, U> &&
+ not_protocol_or_view && std::constructible_from &&
+ std::copy_constructible && reflection_protocol_concept
+ : alloc_(alloc) {
+ p_ = create_storage(std::forward(ts)...);
+ vtable_ = &reflection_detail::owning_vtable_for;
+ }
+
+ template
+ explicit protocol(std::allocator_arg_t, const Allocator& alloc,
+ std::in_place_type_t, std::initializer_list ilist,
+ Ts&&... ts)
+ requires std::same_as, U> &&
+ not_protocol_or_view &&
+ std::constructible_from, Ts&&...> &&
+ std::copy_constructible && reflection_protocol_concept
+ : alloc_(alloc) {
+ p_ = create_storage(ilist, std::forward(ts)...);
+ vtable_ = &reflection_detail::owning_vtable_for;
+ }
+
+ protocol(std::allocator_arg_t, const Allocator& alloc, const protocol& other)
+ : alloc_(alloc) {
+ if (!other.valueless_after_move()) {
+ p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_);
+ vtable_ = other.vtable_;
+ } else {
+ p_ = nullptr;
+ vtable_ = nullptr;
+ }
+ }
+
+ protocol(std::allocator_arg_t, const Allocator& alloc,
+ protocol&& other) noexcept(allocator_traits::is_always_equal::value)
+ : alloc_(alloc) {
+ if constexpr (allocator_traits::is_always_equal::value) {
+ p_ = std::exchange(other.p_, nullptr);
+ vtable_ = std::exchange(other.vtable_, nullptr);
+ } else {
+ if (alloc_ == other.alloc_) {
+ p_ = std::exchange(other.p_, nullptr);
+ vtable_ = std::exchange(other.vtable_, nullptr);
+ } else {
+ if (!other.valueless_after_move()) {
+ p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_);
+ vtable_ = other.vtable_;
+ } else {
+ p_ = nullptr;
+ vtable_ = nullptr;
+ }
+ }
+ }
+ }
+
+ bool valueless_after_move() const noexcept { return p_ == nullptr; }
+
+ ~protocol() {
+ if (p_ != nullptr) {
+ vtable_->xyz_protocol_destroy(p_, alloc_);
+ }
+ }
+
+ protocol& operator=(protocol other) noexcept(
+ allocator_traits::is_always_equal::value) {
+ std::swap(p_, other.p_);
+ std::swap(vtable_, other.vtable_);
+ if constexpr (!allocator_traits::is_always_equal::value) {
+ std::swap(alloc_, other.alloc_);
+ }
+ return *this;
+ }
+
+ // Interface-declared operator= overloads are merged by operator_forwarders
+ // the same way every other operator is; the copy-assignment operator above
+ // would otherwise hide them entirely (ordinary C++ name hiding, not
+ // anything specific to assignment: a derived class's own operator=, even
+ // an implicitly-declared one, hides any operator= it would otherwise
+ // inherit, unless brought back into scope by a using-declaration).
+ using reflection_detail::operator_forwarders