diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml
index bcad3fd..e66776d 100644
--- a/.github/workflows/cmake.yml
+++ b/.github/workflows/cmake.yml
@@ -211,6 +211,18 @@ jobs:
std: 20,
},
}
+ - {
+ name: "Ubuntu GCC-16 (C++26 Reflection)",
+ os: ubuntu-24.04,
+ compiler:
+ {
+ type: GCC16,
+ version: 16,
+ cc: "gcc-16",
+ cxx: "g++-16",
+ std: 26,
+ },
+ }
steps:
- uses: actions/checkout@v4
- uses: seanmiddleditch/gha-setup-ninja@master
@@ -231,9 +243,15 @@ jobs:
with:
version: ${{ matrix.settings.compiler.version }}
platform: x64
+ - name: Install GCC 16 from ubuntu-toolchain-r PPA
+ if: matrix.settings.compiler.type == 'GCC16'
+ run: |
+ sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
+ sudo apt-get update
+ sudo apt-get install -y --no-install-recommends g++-16 gcc-16
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Set up Python
run: uv sync
- name: Run CMake
- run: ./scripts/cmake.sh --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }}
+ run: ./scripts/cmake.sh --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }} ${{ matrix.settings.compiler.type == 'GCC16' && '--reflection' || '' }}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0626252..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..e05214c
--- /dev/null
+++ b/reflection/protocol.h
@@ -0,0 +1,165 @@
+/* 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 {
+
+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
+// 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);
+ 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, constness, ref-qualifier, and noexcept).
+template
+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;
+ break;
+ }
+ }
+ if (!found) return false;
+ }
+ return true;
+}
+
+// Variable template for use in requires clauses.
+template
+inline constexpr bool conforms_to_v = conforms_to();
+
+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 type U that conforms to the Interface T.
+ template
+ requires conforms_to_v> &&
+ is_neither_protocol_nor_protocol_view
+ explicit protocol(U&& 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 type U that conforms to the Interface T.
+ template
+ requires conforms_to_v> &&
+ is_neither_protocol_nor_protocol_view
+ explicit protocol_view(const U& 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..a20b70d
--- /dev/null
+++ b/reflection/protocol_test.cc
@@ -0,0 +1,337 @@
+// 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>);
+}
+
+// ---------------------------------------------------------------------------
+// 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>);
+}
+
+// ---------------------------------------------------------------------------
+// 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.
+// ---------------------------------------------------------------------------
+
+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());
+}
+
+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
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"]