From 567592f19128cb2358d55d648619938ff0c8b7ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 07:24:02 +0000 Subject: [PATCH 1/9] Initial plan From 9aefd5ecb17664a0b4a45067e6b1131c99f43f35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 07:38:20 +0000 Subject: [PATCH 2/9] Add C++26 reflection conformance checks for protocol and protocol_view Co-authored-by: jbcoe <777363+jbcoe@users.noreply.github.com> --- CMakeLists.txt | 33 +++++ cmake/xyz_add_test.cmake | 2 +- reflection/CMakeLists.txt | 17 +++ reflection/protocol.h | 124 +++++++++++++++++++ reflection/protocol_test.cc | 234 ++++++++++++++++++++++++++++++++++++ 5 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 reflection/CMakeLists.txt create mode 100644 reflection/protocol.h create mode 100644 reflection/protocol_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 0626252..07a2e33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,35 @@ 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_IMPLEMENTATION + "Also build and test the C++26-reflection-based implementation of protocol, \ +which requires a compiler with C++26 P2996 reflection support \ +(GCC 16+ with -freflection)." + OFF) + +if(XYZ_PROTOCOL_BUILD_REFLECTION_IMPLEMENTATION) + 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_IMPLEMENTATION 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() @@ -98,6 +127,10 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) enable_testing() + if(XYZ_PROTOCOL_BUILD_REFLECTION_IMPLEMENTATION) + add_subdirectory(reflection) + endif() + xyz_generate_protocol( CLASS_NAME A INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_A.h HEADER interface_A.h 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/reflection/CMakeLists.txt b/reflection/CMakeLists.txt new file mode 100644 index 0000000..59a4420 --- /dev/null +++ b/reflection/CMakeLists.txt @@ -0,0 +1,17 @@ +xyz_add_library( + NAME reflection_protocol + ALIAS xyz_protocol::reflection_protocol) +target_sources( + reflection_protocol INTERFACE + $) + +xyz_add_test( + NAME + reflection_protocol_test + VERSION + 26 + LINK_LIBRARIES + xyz_protocol::reflection_protocol + FILES + protocol_test.cc) +target_compile_options(reflection_protocol_test PRIVATE -freflection) diff --git a/reflection/protocol.h b/reflection/protocol.h new file mode 100644 index 0000000..ebb9967 --- /dev/null +++ b/reflection/protocol.h @@ -0,0 +1,124 @@ +/* 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. +==============================================================================*/ +#ifndef XYZ_REFLECTION_PROTOCOL_H_ +#define XYZ_REFLECTION_PROTOCOL_H_ + +// A C++26-reflection-based implementation of protocol and protocol_view. + +#include +#include +#include +#include +#include + +namespace xyz::reflection { + +namespace detail { + +// Returns true if both member functions share the same name, return type, +// parameter types, and constness. +consteval bool member_function_signatures_match(std::meta::info lhs, + std::meta::info rhs) { + if (!has_identifier(lhs) || !has_identifier(rhs)) return false; + if (identifier_of(lhs) != identifier_of(rhs)) return false; + if (is_const(lhs) != is_const(rhs)) return false; + if (dealias(return_type_of(lhs)) != dealias(return_type_of(rhs))) + return false; + std::vector lhs_params = parameters_of(lhs); + std::vector rhs_params = parameters_of(rhs); + if (lhs_params.size() != rhs_params.size()) return false; + for (std::size_t i = 0; i < lhs_params.size(); ++i) { + if (dealias(type_of(lhs_params[i])) != dealias(type_of(rhs_params[i]))) + return false; + } + return true; +} + +} // namespace detail + +// Returns true at compile time if every public member function declared in +// Interface is present in Concrete with a matching signature (name, return +// type, parameter types, and constness). +template +consteval bool conforms_to() { + for (std::meta::info interface_member : + members_of(^^Interface, std::meta::access_context::unprivileged())) { + if (!is_member_function(interface_member)) continue; + bool found = false; + for (std::meta::info concrete_member : + members_of(^^Concrete, std::meta::access_context::unprivileged())) { + if (!is_member_function(concrete_member)) continue; + if (detail::member_function_signatures_match(interface_member, + concrete_member)) { + found = true; + break; + } + } + if (!found) return false; + } + return true; +} + +template > +class protocol { + public: + // Special member functions. + protocol() = delete; + + protocol(const protocol&) + requires std::is_copy_constructible_v; + + protocol(protocol&&) + requires std::is_move_constructible_v; + + protocol& operator=(const protocol&) + requires std::is_copy_assignable_v; + + protocol& operator=(protocol&&) + requires std::is_move_assignable_v; + + ~protocol(); // Unconstrained. + + // Construct from any Concrete type that conforms to the Interface T. + template + requires conforms_to>() + explicit protocol(Concrete&& value); +}; + +template +class protocol_view { + public: + // Special member functions. + protocol_view() = delete; + protocol_view(const protocol_view&) = default; + protocol_view(protocol_view&&) = default; + protocol_view& operator=(const protocol_view&) = default; + protocol_view& operator=(protocol_view&&) = default; + ~protocol_view() = default; + + // Construct from any Concrete type that conforms to the Interface T. + template + requires conforms_to>() + explicit protocol_view(const Concrete& object); +}; + +} // namespace xyz::reflection +#endif // XYZ_REFLECTION_PROTOCOL_H_ diff --git a/reflection/protocol_test.cc b/reflection/protocol_test.cc new file mode 100644 index 0000000..d2f9335 --- /dev/null +++ b/reflection/protocol_test.cc @@ -0,0 +1,234 @@ +// Tests for the C++26-reflection-based implementation of protocol and +// protocol_view. + +#include "protocol.h" + +#include + +#include +#include +#include + +using xyz::reflection::conforms_to; +using xyz::reflection::protocol; +using xyz::reflection::protocol_view; + +namespace { + +// --------------------------------------------------------------------------- +// Special member function tests (protocol_view). +// --------------------------------------------------------------------------- + +TEST(ReflectionProtocolViewTest, CheckSpecialMembers) { + // protocol_view is not default-constructible but can be copied, moved, + // assigned, move assigned and destroyed. + struct A {}; + + static_assert(!std::is_default_constructible_v>); + static_assert(std::is_copy_constructible_v>); + static_assert(std::is_move_constructible_v>); + static_assert(std::is_copy_assignable_v>); + static_assert(std::is_move_assignable_v>); + static_assert(std::is_destructible_v>); +} + +TEST(ReflectionProtocolViewTest, + CheckSpecialMembersForStructWithDeletedSpecialMembers) { + // protocol_view's special member functions do not depend on those of the + // viewed type. + struct D { + D() = delete; + D(const D&) = delete; + D(D&&) = delete; + D& operator=(const D&) = delete; + D& operator=(D&&) = delete; + ~D() = delete; + }; + + static_assert(!std::is_default_constructible_v>); + static_assert(std::is_copy_constructible_v>); + static_assert(std::is_move_constructible_v>); + static_assert(std::is_copy_assignable_v>); + static_assert(std::is_move_assignable_v>); + static_assert(std::is_destructible_v>); +} + +// --------------------------------------------------------------------------- +// Special member function tests (protocol). +// --------------------------------------------------------------------------- + +TEST(ReflectionProtocolTest, CheckSpecialMembers) { + // protocol is not default-constructible but can be copied, moved, assigned + // and move assigned if the underlying type can be. + struct A {}; + + static_assert(!std::is_default_constructible_v>); + static_assert(std::is_copy_constructible_v>); + static_assert(std::is_move_constructible_v>); + static_assert(std::is_copy_assignable_v>); + static_assert(std::is_move_assignable_v>); +} + +TEST(ReflectionProtocolTest, + CheckSpecialMembersForStructWithDeletedSpecialMembers) { + // protocol is not default-constructible and cannot be copied, moved, + // assigned, or move assigned if the underlying type cannot be. + struct D { + D() = delete; + D(const D&) = delete; + D(D&&) = delete; + D& operator=(const D&) = delete; + D& operator=(D&&) = delete; + }; + + static_assert(!std::is_default_constructible_v>); + static_assert(!std::is_copy_constructible_v>); + static_assert(!std::is_move_constructible_v>); + static_assert(!std::is_copy_assignable_v>); + static_assert(!std::is_move_assignable_v>); +} + +// --------------------------------------------------------------------------- +// Conformance check tests. +// --------------------------------------------------------------------------- + +TEST(ConformsToTest, EmptyInterfaceIsAlwaysSatisfied) { + struct EmptyInterface {}; + struct Concrete {}; + + static_assert(conforms_to()); +} + +TEST(ConformsToTest, ConcreteTypeConformsWhenAllMethodsMatch) { + struct Interface { + std::string_view name() const noexcept; + int count(); + }; + struct Concrete { + std::string_view name() const noexcept { return "test"; } + int count() { return 42; } + }; + + static_assert(conforms_to()); +} + +TEST(ConformsToTest, ConcreteTypeConformsWithExtraMethodsPresent) { + struct Interface { + void process(); + }; + struct ConcreteWithExtra { + void process() {} + void extra_method() {} + int another() const { return 0; } + }; + + static_assert(conforms_to()); +} + +TEST(ConformsToTest, ConcreteTypeMissingMethodDoesNotConform) { + struct Interface { + void foo(); + void bar(); + }; + struct MissingBar { + void foo() {} + }; + + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, WrongConstnessDoesNotConform) { + struct Interface { + int value() const; + }; + struct NonConst { + int value(); // not const — does not match the interface + }; + + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, ConstMethodNotSatisfiedByNonConstDoesNotConform) { + struct Interface { + void process() const; + }; + struct NonConstProcess { + void process() {} // missing const qualifier + }; + + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, WrongReturnTypeDoesNotConform) { + struct Interface { + int compute(); + }; + struct WrongReturn { + double compute() { return 0.0; } + }; + + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, WrongParameterTypeDoesNotConform) { + struct Interface { + void process(int value); + }; + struct WrongParam { + void process(double value) {} + }; + + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, WrongParameterCountDoesNotConform) { + struct Interface { + void process(int a, int b); + }; + struct WrongArity { + void process(int a) {} + }; + + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, MultipleParametersMatchCorrectly) { + struct Interface { + void write(int length, double value); + }; + struct Concrete { + void write(int length, double value) {} + }; + + static_assert(conforms_to()); +} + +TEST(ConformsToTest, ConcreteTypeConformsForTypicalInterfaceA) { + struct InterfaceA { + std::string_view name() const noexcept; + int count(); + }; + struct ConcreteA { + std::string_view name() const noexcept { return "concrete"; } + int count() { return 1; } + }; + + static_assert(conforms_to()); +} + +TEST(ConformsToTest, ConcreteTypeConformsForTypicalInterfaceB) { + struct InterfaceB { + void process(const std::string& input); + std::vector get_results() const; + bool is_ready() const; + }; + struct ConcreteB { + void process(const std::string& input) {} + std::vector get_results() const { return {}; } + bool is_ready() const { return true; } + }; + + static_assert(conforms_to()); +} + +} // namespace From e491e1525202913644cbca27cdcbbef4f9700fc1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:37:11 +0000 Subject: [PATCH 3/9] Fix clang-format: use conforms_to_v variable template in requires clauses Co-authored-by: jbcoe <777363+jbcoe@users.noreply.github.com> --- reflection/protocol.h | 8 ++++++-- reflection/protocol_test.cc | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/reflection/protocol.h b/reflection/protocol.h index ebb9967..70f1063 100644 --- a/reflection/protocol.h +++ b/reflection/protocol.h @@ -77,6 +77,10 @@ consteval bool conforms_to() { return true; } +// Variable template for use in requires clauses. +template +inline constexpr bool conforms_to_v = conforms_to(); + template > class protocol { public: @@ -99,7 +103,7 @@ class protocol { // Construct from any Concrete type that conforms to the Interface T. template - requires conforms_to>() + requires conforms_to_v> explicit protocol(Concrete&& value); }; @@ -116,7 +120,7 @@ class protocol_view { // Construct from any Concrete type that conforms to the Interface T. template - requires conforms_to>() + requires conforms_to_v> explicit protocol_view(const Concrete& object); }; diff --git a/reflection/protocol_test.cc b/reflection/protocol_test.cc index d2f9335..600af3e 100644 --- a/reflection/protocol_test.cc +++ b/reflection/protocol_test.cc @@ -94,6 +94,7 @@ TEST(ReflectionProtocolTest, TEST(ConformsToTest, EmptyInterfaceIsAlwaysSatisfied) { struct EmptyInterface {}; + struct Concrete {}; static_assert(conforms_to()); @@ -104,8 +105,10 @@ TEST(ConformsToTest, ConcreteTypeConformsWhenAllMethodsMatch) { std::string_view name() const noexcept; int count(); }; + struct Concrete { std::string_view name() const noexcept { return "test"; } + int count() { return 42; } }; @@ -116,9 +119,12 @@ TEST(ConformsToTest, ConcreteTypeConformsWithExtraMethodsPresent) { struct Interface { void process(); }; + struct ConcreteWithExtra { void process() {} + void extra_method() {} + int another() const { return 0; } }; @@ -130,6 +136,7 @@ TEST(ConformsToTest, ConcreteTypeMissingMethodDoesNotConform) { void foo(); void bar(); }; + struct MissingBar { void foo() {} }; @@ -141,6 +148,7 @@ TEST(ConformsToTest, WrongConstnessDoesNotConform) { struct Interface { int value() const; }; + struct NonConst { int value(); // not const — does not match the interface }; @@ -152,6 +160,7 @@ TEST(ConformsToTest, ConstMethodNotSatisfiedByNonConstDoesNotConform) { struct Interface { void process() const; }; + struct NonConstProcess { void process() {} // missing const qualifier }; @@ -163,6 +172,7 @@ TEST(ConformsToTest, WrongReturnTypeDoesNotConform) { struct Interface { int compute(); }; + struct WrongReturn { double compute() { return 0.0; } }; @@ -174,6 +184,7 @@ TEST(ConformsToTest, WrongParameterTypeDoesNotConform) { struct Interface { void process(int value); }; + struct WrongParam { void process(double value) {} }; @@ -185,6 +196,7 @@ TEST(ConformsToTest, WrongParameterCountDoesNotConform) { struct Interface { void process(int a, int b); }; + struct WrongArity { void process(int a) {} }; @@ -196,6 +208,7 @@ TEST(ConformsToTest, MultipleParametersMatchCorrectly) { struct Interface { void write(int length, double value); }; + struct Concrete { void write(int length, double value) {} }; @@ -208,8 +221,10 @@ TEST(ConformsToTest, ConcreteTypeConformsForTypicalInterfaceA) { std::string_view name() const noexcept; int count(); }; + struct ConcreteA { std::string_view name() const noexcept { return "concrete"; } + int count() { return 1; } }; @@ -222,9 +237,12 @@ TEST(ConformsToTest, ConcreteTypeConformsForTypicalInterfaceB) { std::vector get_results() const; bool is_ready() const; }; + struct ConcreteB { void process(const std::string& input) {} + std::vector get_results() const { return {}; } + bool is_ready() const { return true; } }; From 0bfac4e1360fb3d2bb35de7b9f923ffa1b606214 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:44:58 +0000 Subject: [PATCH 4/9] Use U instead of Concrete in constructor templates; add constructability tests Co-authored-by: jbcoe <777363+jbcoe@users.noreply.github.com> --- reflection/protocol.h | 16 ++++++++-------- reflection/protocol_test.cc | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/reflection/protocol.h b/reflection/protocol.h index 70f1063..a65fa30 100644 --- a/reflection/protocol.h +++ b/reflection/protocol.h @@ -101,10 +101,10 @@ class protocol { ~protocol(); // Unconstrained. - // Construct from any Concrete type that conforms to the Interface T. - template - requires conforms_to_v> - explicit protocol(Concrete&& value); + // Construct from any type U that conforms to the Interface T. + template + requires conforms_to_v> + explicit protocol(U&& value); }; template @@ -118,10 +118,10 @@ class protocol_view { protocol_view& operator=(protocol_view&&) = default; ~protocol_view() = default; - // Construct from any Concrete type that conforms to the Interface T. - template - requires conforms_to_v> - explicit protocol_view(const Concrete& object); + // Construct from any type U that conforms to the Interface T. + template + requires conforms_to_v> + explicit protocol_view(const U& object); }; } // namespace xyz::reflection diff --git a/reflection/protocol_test.cc b/reflection/protocol_test.cc index 600af3e..66134ff 100644 --- a/reflection/protocol_test.cc +++ b/reflection/protocol_test.cc @@ -88,6 +88,41 @@ TEST(ReflectionProtocolTest, static_assert(!std::is_move_assignable_v>); } +// --------------------------------------------------------------------------- +// Constructability tests. +// --------------------------------------------------------------------------- + +TEST(ReflectionProtocolTest, IsConstructibleFromConformingType) { + struct Interface { + std::string_view name() const noexcept; + }; + + struct Conforming { + std::string_view name() const noexcept { return "conforming"; } + }; + + struct NonConforming {}; + + static_assert(std::is_constructible_v, Conforming>); + static_assert(!std::is_constructible_v, NonConforming>); +} + +TEST(ReflectionProtocolViewTest, IsConstructibleFromConformingType) { + struct Interface { + std::string_view name() const noexcept; + }; + + struct Conforming { + std::string_view name() const noexcept { return "conforming"; } + }; + + struct NonConforming {}; + + static_assert(std::is_constructible_v, Conforming>); + static_assert( + !std::is_constructible_v, NonConforming>); +} + // --------------------------------------------------------------------------- // Conformance check tests. // --------------------------------------------------------------------------- From e118a913e4da48a48c069be74b4bccec52de5d75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:51:54 +0000 Subject: [PATCH 5/9] Add C++26 reflection CI: --reflection flag in cmake.py and GCC-16 matrix in cmake.yml Co-authored-by: jbcoe <777363+jbcoe@users.noreply.github.com> --- .github/workflows/cmake.yml | 20 +++++++++++++++++++- scripts/cmake.py | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) 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/scripts/cmake.py b/scripts/cmake.py index ecfca13..888ab37 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 the C++26 reflection-based implementation (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_IMPLEMENTATION=" + + ("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_IMPLEMENTATION=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"] From b88fa43f781d4197226d18759b93895b9d2b350b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:31:07 +0000 Subject: [PATCH 6/9] Check noexcept and ref-qualifier in conforms_to; add tests Co-authored-by: jbcoe <777363+jbcoe@users.noreply.github.com> --- reflection/protocol.h | 16 +++++-- reflection/protocol_test.cc | 90 +++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/reflection/protocol.h b/reflection/protocol.h index a65fa30..2e1bd26 100644 --- a/reflection/protocol.h +++ b/reflection/protocol.h @@ -33,13 +33,23 @@ namespace xyz::reflection { namespace detail { -// Returns true if both member functions share the same name, return type, -// parameter types, and constness. +// Returns true if the concrete member function (rhs) satisfies the interface +// member function (lhs) with respect to: name, return type, parameter types, +// constness, ref-qualifier, and noexcept. For noexcept, the rule is: +// - If the interface requires noexcept, the concrete must also be noexcept. +// - If the interface does not require noexcept, the concrete may be either +// (a noexcept concrete is still conformant with a non-noexcept interface). +// Ref-qualifiers (none, &, &&) must match exactly. consteval bool member_function_signatures_match(std::meta::info lhs, std::meta::info rhs) { if (!has_identifier(lhs) || !has_identifier(rhs)) return false; if (identifier_of(lhs) != identifier_of(rhs)) return false; if (is_const(lhs) != is_const(rhs)) return false; + if (is_lvalue_reference_qualified(lhs) != is_lvalue_reference_qualified(rhs)) + return false; + if (is_rvalue_reference_qualified(lhs) != is_rvalue_reference_qualified(rhs)) + return false; + if (is_noexcept(lhs) && !is_noexcept(rhs)) return false; if (dealias(return_type_of(lhs)) != dealias(return_type_of(rhs))) return false; std::vector lhs_params = parameters_of(lhs); @@ -56,7 +66,7 @@ consteval bool member_function_signatures_match(std::meta::info lhs, // Returns true at compile time if every public member function declared in // Interface is present in Concrete with a matching signature (name, return -// type, parameter types, and constness). +// type, parameter types, constness, ref-qualifier, and noexcept). template consteval bool conforms_to() { for (std::meta::info interface_member : diff --git a/reflection/protocol_test.cc b/reflection/protocol_test.cc index 66134ff..d373d90 100644 --- a/reflection/protocol_test.cc +++ b/reflection/protocol_test.cc @@ -284,4 +284,94 @@ TEST(ConformsToTest, ConcreteTypeConformsForTypicalInterfaceB) { static_assert(conforms_to()); } +TEST(ConformsToTest, NoexceptInterfaceRequiresNoexceptConcrete) { + struct Interface { + void f() noexcept; + }; + + struct Conforming { + void f() noexcept {} + }; + + struct NonNoexcept { + void f() {} + }; + + static_assert(conforms_to()); + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, NonNoexceptInterfaceAcceptsNoexceptConcrete) { + struct Interface { + void f(); + }; + + struct NoexceptConcrete { + void f() noexcept {} + }; + + static_assert(conforms_to()); +} + +TEST(ConformsToTest, LvalueRefQualifierMustMatch) { + struct Interface { + void f() &; + }; + + struct Conforming { + void f() & {} + }; + + struct UnqualifiedConcrete { + void f() {} + }; + + struct RvalueRefConcrete { + void f() && {} + }; + + static_assert(conforms_to()); + static_assert(!conforms_to()); + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, RvalueRefQualifierMustMatch) { + struct Interface { + void f() &&; + }; + + struct Conforming { + void f() && {} + }; + + struct UnqualifiedConcrete { + void f() {} + }; + + struct LvalueRefConcrete { + void f() & {} + }; + + static_assert(conforms_to()); + static_assert(!conforms_to()); + static_assert(!conforms_to()); +} + +TEST(ConformsToTest, UnqualifiedInterfaceDoesNotMatchRefQualifiedConcrete) { + struct Interface { + void f(); + }; + + struct LvalueRefConcrete { + void f() & {} + }; + + struct RvalueRefConcrete { + void f() && {} + }; + + static_assert(!conforms_to()); + static_assert(!conforms_to()); +} + } // namespace From e95c191e4789d01e577075d77f9b0f4477dfd42b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:45:05 +0000 Subject: [PATCH 7/9] Fix CI: replace is_member_function with is_function (not in GCC-16 P2996) Co-authored-by: jbcoe <777363+jbcoe@users.noreply.github.com> --- reflection/protocol.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reflection/protocol.h b/reflection/protocol.h index 2e1bd26..96dedae 100644 --- a/reflection/protocol.h +++ b/reflection/protocol.h @@ -71,11 +71,11 @@ template consteval bool conforms_to() { for (std::meta::info interface_member : members_of(^^Interface, std::meta::access_context::unprivileged())) { - if (!is_member_function(interface_member)) continue; + if (!is_function(interface_member)) continue; bool found = false; for (std::meta::info concrete_member : members_of(^^Concrete, std::meta::access_context::unprivileged())) { - if (!is_member_function(concrete_member)) continue; + if (!is_function(concrete_member)) continue; if (detail::member_function_signatures_match(interface_member, concrete_member)) { found = true; From f104592fbb5f1dac9c00835dac463da897802302 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:57:01 +0000 Subject: [PATCH 8/9] Fix GCC16 reflection conformance and constructor constraints Co-authored-by: jbcoe <777363+jbcoe@users.noreply.github.com> --- reflection/protocol.h | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/reflection/protocol.h b/reflection/protocol.h index 96dedae..e05214c 100644 --- a/reflection/protocol.h +++ b/reflection/protocol.h @@ -31,6 +31,29 @@ SOFTWARE. namespace xyz::reflection { +template +class protocol; + +template +class protocol_view; + +template +struct is_protocol : std::false_type {}; + +template +struct is_protocol> : std::true_type {}; + +template +struct is_protocol_view : std::false_type {}; + +template +struct is_protocol_view> : std::true_type {}; + +template +concept is_neither_protocol_nor_protocol_view = + !is_protocol>::value && + !is_protocol_view>::value; + namespace detail { // Returns true if the concrete member function (rhs) satisfies the interface @@ -72,10 +95,12 @@ consteval bool conforms_to() { for (std::meta::info interface_member : members_of(^^Interface, std::meta::access_context::unprivileged())) { if (!is_function(interface_member)) continue; + if (!has_identifier(interface_member)) continue; bool found = false; for (std::meta::info concrete_member : members_of(^^Concrete, std::meta::access_context::unprivileged())) { if (!is_function(concrete_member)) continue; + if (!has_identifier(concrete_member)) continue; if (detail::member_function_signatures_match(interface_member, concrete_member)) { found = true; @@ -113,7 +138,8 @@ class protocol { // Construct from any type U that conforms to the Interface T. template - requires conforms_to_v> + requires conforms_to_v> && + is_neither_protocol_nor_protocol_view explicit protocol(U&& value); }; @@ -130,7 +156,8 @@ class protocol_view { // Construct from any type U that conforms to the Interface T. template - requires conforms_to_v> + requires conforms_to_v> && + is_neither_protocol_nor_protocol_view explicit protocol_view(const U& object); }; From 70f705db325d30fb666d334da48006030c555050 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:04:05 +0000 Subject: [PATCH 9/9] Remove tests for functions without identifiers (out of scope) Co-authored-by: jbcoe <777363+jbcoe@users.noreply.github.com> --- reflection/protocol_test.cc | 40 ------------------------------------- 1 file changed, 40 deletions(-) diff --git a/reflection/protocol_test.cc b/reflection/protocol_test.cc index d373d90..a20b70d 100644 --- a/reflection/protocol_test.cc +++ b/reflection/protocol_test.cc @@ -32,27 +32,6 @@ TEST(ReflectionProtocolViewTest, CheckSpecialMembers) { static_assert(std::is_destructible_v>); } -TEST(ReflectionProtocolViewTest, - CheckSpecialMembersForStructWithDeletedSpecialMembers) { - // protocol_view's special member functions do not depend on those of the - // viewed type. - struct D { - D() = delete; - D(const D&) = delete; - D(D&&) = delete; - D& operator=(const D&) = delete; - D& operator=(D&&) = delete; - ~D() = delete; - }; - - static_assert(!std::is_default_constructible_v>); - static_assert(std::is_copy_constructible_v>); - static_assert(std::is_move_constructible_v>); - static_assert(std::is_copy_assignable_v>); - static_assert(std::is_move_assignable_v>); - static_assert(std::is_destructible_v>); -} - // --------------------------------------------------------------------------- // Special member function tests (protocol). // --------------------------------------------------------------------------- @@ -69,25 +48,6 @@ TEST(ReflectionProtocolTest, CheckSpecialMembers) { static_assert(std::is_move_assignable_v>); } -TEST(ReflectionProtocolTest, - CheckSpecialMembersForStructWithDeletedSpecialMembers) { - // protocol is not default-constructible and cannot be copied, moved, - // assigned, or move assigned if the underlying type cannot be. - struct D { - D() = delete; - D(const D&) = delete; - D(D&&) = delete; - D& operator=(const D&) = delete; - D& operator=(D&&) = delete; - }; - - static_assert(!std::is_default_constructible_v>); - static_assert(!std::is_copy_constructible_v>); - static_assert(!std::is_move_constructible_v>); - static_assert(!std::is_copy_assignable_v>); - static_assert(!std::is_move_assignable_v>); -} - // --------------------------------------------------------------------------- // Constructability tests. // ---------------------------------------------------------------------------