diff --git a/CMakeLists.txt b/CMakeLists.txt index 0626252..3a63ea8 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() @@ -171,6 +200,10 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) --flags=-I${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) endif() + + if(XYZ_PROTOCOL_BUILD_REFLECTION_IMPLEMENTATION) + add_subdirectory(reflection) + endif() endif(${BUILD_TESTING}) endif() 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..a6c0e8c --- /dev/null +++ b/reflection/protocol.h @@ -0,0 +1,377 @@ +/* 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. +// +// Member function stubs are synthesised at compile time for every public +// non-special member function declared in the Interface type. The stubs +// are attached to protocol and protocol_view through data members that +// provide ordinary member-function call syntax via the "vanishing this +// pointer" technique described in tutorials/2_vanishing_this_pointer.cc +// (section 5): each per-method wrapper sits as the sole member of a +// dedicated base struct; the wrapper's operator() recovers the enclosing +// base address through a static_cast and hands it to the derived class. +// +// The stubs are intentionally unimplemented beyond the signature: they +// call std::unreachable() so that the type-system plumbing can be +// developed before the vtable dispatch layer exists. + +#include +#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 not_protocol_or_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; +} + +// --------------------------------------------------------------------------- +// Vanishing-this-pointer thunks for synthesised member stubs. +// +// Each thunk type carries a single operator() whose signature mirrors one +// method of the Interface type. The thunk lives as a [[no_unique_address]] +// data member inside a per-method base struct (section 5 of the tutorial); +// operator() recovers the base struct's address from its own address (valid +// because the thunk is the sole, offset-zero member of that base) and then +// casts further to the protocol or protocol_view that inherits from it. +// +// The four partial specialisations below cover the four combinations of +// const/non-const × noexcept/potentially-throwing that an interface method +// can declare. +// --------------------------------------------------------------------------- + +// Helper alias used by method_thunk_specs to build function-pointer types +// that encode the method's return type and parameter types. +template +using fn_ptr_t = R (*)(Args...); + +// Non-const, potentially-throwing method thunk. +// OwnerBase is the dedicated base struct holding this thunk; the derived +// class (protocol or protocol_view) inherits from OwnerBase. +template +struct method_thunk_mutable; + +template +struct method_thunk_mutable { + // Provides member-function call syntax. Recovers the OwnerBase pointer + // through the vanishing-this-pointer cast, then calls the protocol's + // stored vtable entry (not yet implemented: stub calls std::unreachable). + R operator()(Args... /*args*/) { + // Vanishing this pointer: this thunk is at offset 0 of OwnerBase. + [[maybe_unused]] auto* base = + static_cast(static_cast(this)); + std::unreachable(); // stub — vtable dispatch not yet implemented + } +}; + +// Const, potentially-throwing method thunk. +template +struct method_thunk_const; + +template +struct method_thunk_const { + R operator()(Args... /*args*/) const { + [[maybe_unused]] const auto* base = + static_cast(static_cast(this)); + std::unreachable(); // stub — vtable dispatch not yet implemented + } +}; + +// Non-const, noexcept method thunk. +template +struct method_thunk_mutable_noexcept; + +template +struct method_thunk_mutable_noexcept { + R operator()(Args... /*args*/) noexcept { + [[maybe_unused]] auto* base = + static_cast(static_cast(this)); + std::unreachable(); // stub — vtable dispatch not yet implemented + } +}; + +// Const, noexcept method thunk. +template +struct method_thunk_const_noexcept; + +template +struct method_thunk_const_noexcept { + R operator()(Args... /*args*/) const noexcept { + [[maybe_unused]] const auto* base = + static_cast(static_cast(this)); + std::unreachable(); // stub — vtable dispatch not yet implemented + } +}; + +// --------------------------------------------------------------------------- +// Compile-time helper: build a list of data_member_spec values, one per +// public non-special member function of interface_type. +// +// Each spec names the data member after the interface method (giving the +// p.method_name(args) call syntax) and sets its type to the matching thunk +// template specialisation. [[no_unique_address]] is requested so that +// thunks for methods returning void add no storage overhead. +// +// Methods that share a name (overloads) are each given their own data +// member. Because C++ disallows two data members with the same name inside +// the same class, overloaded methods whose names collide are skipped after +// the first occurrence and excluded from the synthesised bases. A future +// revision can handle overloads by encoding the parameter types into the +// member name (see tutorials/3_reflection.cc section 5). +// --------------------------------------------------------------------------- +consteval std::vector method_thunk_specs( + std::meta::info interface_type, std::meta::info owner_base_type) { + std::vector specs; + std::vector seen_names; + + for (std::meta::info member : + members_of(interface_type, + std::meta::access_context::unprivileged())) { + if (!is_function(member)) continue; + if (is_special_member_function(member)) continue; + if (!has_identifier(member)) continue; + + std::string_view name = identifier_of(member); + + // Skip overloads: only the first declaration with a given name is + // included. See the comment above for why. + bool already_seen = false; + for (std::string_view seen : seen_names) { + if (seen == name) { + already_seen = true; + break; + } + } + if (already_seen) continue; + seen_names.push_back(name); + + // Build the function-pointer type R(*)(Args...) from the method's + // return type and parameter types. + std::vector fn_args{dealias(return_type_of(member))}; + for (std::meta::info parameter : parameters_of(member)) { + fn_args.push_back(dealias(type_of(parameter))); + } + std::meta::info fn_ptr_type = substitute(^^fn_ptr_t, fn_args); + + // Choose the thunk template based on the method's constness and + // noexcept specification. + std::meta::info thunk_template; + if (is_const(member) && is_noexcept(member)) { + thunk_template = ^^method_thunk_const_noexcept; + } else if (is_const(member)) { + thunk_template = ^^method_thunk_const; + } else if (is_noexcept(member)) { + thunk_template = ^^method_thunk_mutable_noexcept; + } else { + thunk_template = ^^method_thunk_mutable; + } + + std::meta::info thunk_type = + substitute(thunk_template, {fn_ptr_type, owner_base_type}); + + specs.push_back(data_member_spec( + thunk_type, + std::meta::data_member_options{.name = name, .no_unique_address = true})); + } + return specs; +} + +} // 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; + if (is_special_member_function(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(); + +// --------------------------------------------------------------------------- +// Synthesised member-stub bases. +// +// protocol_member_stubs and protocol_view_member_stubs are +// incomplete classes that are completed by a consteval block inside the +// protocol / protocol_view class template body. Each block calls +// define_aggregate to inject one [[no_unique_address]] thunk data member +// per public non-special member function of T, giving protocol and +// protocol_view the member-function call syntax described at the top of +// this file. +// --------------------------------------------------------------------------- + +// Base injected into protocol. +template +struct protocol_member_stubs; + +// Base injected into protocol_view. +template +struct protocol_view_member_stubs; + +// --------------------------------------------------------------------------- +// protocol +// --------------------------------------------------------------------------- +template > +class protocol : public protocol_member_stubs { + // Synthesise the member stubs for this specialisation. The consteval + // block runs during translation when the class template is instantiated. + // define_aggregate completes protocol_member_stubs by injecting one + // thunk data member per method of T. The thunk's OwnerBase parameter is + // protocol_member_stubs itself, matching the vanishing-this-pointer + // requirement that the thunk is the offset-zero member of its base. + consteval { + define_aggregate(^^protocol_member_stubs, + detail::method_thunk_specs(^^T, ^^protocol_member_stubs)); + } + + 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> && + not_protocol_or_view + explicit protocol(U&& value); +}; + +// --------------------------------------------------------------------------- +// protocol_view +// --------------------------------------------------------------------------- +template +class protocol_view : public protocol_view_member_stubs { + // Synthesise the member stubs for this specialisation. + consteval { + define_aggregate( + ^^protocol_view_member_stubs, + detail::method_thunk_specs(^^T, ^^protocol_view_member_stubs)); + } + + 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> && + not_protocol_or_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..f62848a --- /dev/null +++ b/reflection/protocol_test.cc @@ -0,0 +1,446 @@ +// Tests for the C++26-reflection-based implementation of protocol and +// protocol_view. +// +// Specifically covers: +// - Special member function availability. +// - Constructability from conforming/non-conforming types. +// - Conformance checking via conforms_to<>. +// - Member function stub invocability via the vanishing-this-pointer +// synthesised members. + +#include "protocol.h" + +#include + +#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()); +} + +// --------------------------------------------------------------------------- +// Member function stub invocability tests. +// +// These tests verify that protocol and protocol_view +// expose member function stubs with the correct signatures, as synthesised +// by the vanishing-this-pointer approach in protocol.h. +// +// The stubs always call std::unreachable() internally, so the tests only +// check that the call expressions compile; they do not invoke the stubs at +// runtime (which would be undefined behaviour before the vtable layer +// exists). +// --------------------------------------------------------------------------- + +// Verify that a protocol with a single const method exposes that method. +TEST(MemberStubTest, ConstMethodIsSynthesisedOnProtocol) { + struct Interface { + int get_value() const; + }; + + // The synthesised member must be accessible and have the right signature. + // Use decltype to confirm the return type without executing the stub. + static_assert( + std::is_same_v>().get_value()), + int>); +} + +// Verify that a protocol with a non-const method exposes that method. +TEST(MemberStubTest, MutableMethodIsSynthesisedOnProtocol) { + struct Interface { + void update(int value); + }; + + static_assert( + std::is_same_v>().update(0)), + void>); +} + +// Verify that a noexcept method stub is marked noexcept on protocol. +TEST(MemberStubTest, NoexceptMethodStubIsNoexceptOnProtocol) { + struct Interface { + double compute(double input) noexcept; + }; + + static_assert( + noexcept(std::declval>().compute(0.0))); +} + +// Verify that a const noexcept method stub is noexcept on protocol. +TEST(MemberStubTest, ConstNoexceptMethodStubIsNoexceptOnProtocol) { + struct Interface { + std::string_view name() const noexcept; + }; + + static_assert( + noexcept(std::declval>().name())); +} + +// Verify that a protocol_view exposes a const method stub. +TEST(MemberStubTest, ConstMethodIsSynthesisedOnProtocolView) { + struct Interface { + int get_value() const; + }; + + static_assert( + std::is_same_v>().get_value()), + int>); +} + +// Verify that a protocol_view exposes a non-const method stub. +TEST(MemberStubTest, MutableMethodIsSynthesisedOnProtocolView) { + struct Interface { + void update(int value); + }; + + static_assert( + std::is_same_v>().update(0)), + void>); +} + +// Verify multi-parameter method stubs compile with the correct signature. +TEST(MemberStubTest, MultiParameterMethodIsSynthesised) { + struct Interface { + int add(int a, int b) const; + }; + + static_assert( + std::is_same_v< + decltype(std::declval>().add(1, 2)), int>); +} + +// Verify that a method returning void is synthesised correctly. +TEST(MemberStubTest, VoidReturnMethodIsSynthesised) { + struct Interface { + void reset(); + }; + + static_assert( + std::is_same_v>().reset()), + void>); +} + +} // namespace diff --git a/scripts/cmake.py b/scripts/cmake.py index ecfca13..9ccac60 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,13 @@ 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). Defaults to g++-16/gcc-16 unless CXX/CC are " + "already set in the environment.", + ) 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 +82,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 +92,18 @@ 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. Use setdefault so that a CXX/CC already + # present in the caller's environment takes precedence over the defaults. + configure_env = os.environ.copy() + if args.reflection: + configure_env.setdefault("CXX", "g++-16") + configure_env.setdefault("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"]