From aee03f3eea776db67e9b9fa698cf32492cb42f52 Mon Sep 17 00:00:00 2001 From: Philip Craig <689193+philipcraig@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:48:11 +0000 Subject: [PATCH 1/3] Split protocol_test.cc into four translation units protocol_test.cc was the longest test TU to compile and set the floor for a parallel build. Move the conformance tests, the protocol_view member function and views-of-protocols tests, and the protocol member function tests into their own files. Tests are moved unchanged. Part of #423. --- BUILD.bazel | 3 + CMakeLists.txt | 3 + conformance_tests.cc | 563 ++++++++ protocol_member_function_tests.cc | 608 ++++++++ protocol_test.cc | 2146 ++--------------------------- protocol_view_tests.cc | 866 ++++++++++++ 6 files changed, 2127 insertions(+), 2062 deletions(-) create mode 100644 conformance_tests.cc create mode 100644 protocol_member_function_tests.cc create mode 100644 protocol_view_tests.cc diff --git a/BUILD.bazel b/BUILD.bazel index b4a613c..7b73f73 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -35,11 +35,14 @@ cc_test( size = "small", srcs = [ "allocator_tests.cc", + "conformance_tests.cc", "consteval_check_test.cc", "forwarding_test.cc", "name_mangling_tests.cc", + "protocol_member_function_tests.cc", "protocol_operator_tests.cc", "protocol_test.cc", + "protocol_view_tests.cc", "tagged_allocator.h", "test_helpers.h", "tracking_allocator.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index e5d2888..88227d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,7 +182,10 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) xyz_protocol::protocol FILES protocol_test.cc + protocol_member_function_tests.cc protocol_operator_tests.cc + protocol_view_tests.cc + conformance_tests.cc allocator_tests.cc consteval_check_test.cc forwarding_test.cc diff --git a/conformance_tests.cc b/conformance_tests.cc new file mode 100644 index 0000000..65e5ed4 --- /dev/null +++ b/conformance_tests.cc @@ -0,0 +1,563 @@ +// Tests for is_protocol_conformant. + +#include + +#include +#include +#include +#include + +#include "protocol.hh" + +using xyz::reflection::is_protocol_conformant; + +namespace { + +// --------------------------------------------------------------------------- +// Conformance check tests. +// --------------------------------------------------------------------------- + +// Returns `true` if checking conformance of `Candidate` against `Interface` +// throws during constant evaluation, as it does for a ref-qualified +// interface member function. Observing the rejection means catching the +// exception at constant evaluation time (P3068 constexpr exceptions), which +// GCC trunk implements but the clang-p2996 fork used for clang-tidy does +// not; the rejection itself does not depend on P3068. +#ifdef __cpp_constexpr_exceptions +template +consteval bool conformance_check_rejects() { + try { + (void)is_protocol_conformant(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} +#endif // __cpp_constexpr_exceptions + +TEST(ConformsToTest, EmptyInterfaceIsAlwaysSatisfied) { + struct EmptyInterface {}; + + struct Candidate {}; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, CandidateTypeConformsWhenAllMethodsMatch) { + struct Interface { + std::string_view name() const noexcept; + int count(); + }; + + struct Candidate { + std::string_view name() const noexcept; + int count(); + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, CandidateTypeConformsWithExtraMethodsPresent) { + struct Interface { + void process(); + }; + + struct CandidateWithExtra { + void process(); + void extra_method(); + int another() const; + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, CandidateTypeMissingMethodDoesNotConform) { + struct Interface { + void foo(); + void bar(); + }; + + struct MissingBar { + void foo(); + }; + + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, WrongConstnessDoesNotConform) { + struct Interface { + int value() const; + }; + + struct NonConst { + int value(); // not const — does not match the interface + }; + + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, WrongReturnTypeDoesNotConform) { + struct Interface { + int compute(); + }; + + struct WrongReturn { + double compute(); + }; + + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, WrongParameterTypeDoesNotConform) { + struct Interface { + void process(int value); + }; + + struct WrongParam { + void process(double value); + }; + + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, WrongParameterCountDoesNotConform) { + struct Interface { + void process(int a, int b); + }; + + struct WrongArity { + void process(int a); + }; + + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, MultipleParametersMatchCorrectly) { + struct Interface { + void write(int length, double value); + }; + + struct Candidate { + void write(int length, double value); + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, CandidateTypeConformsForTypicalInterfaceB) { + struct InterfaceB { + void process(const std::string& input); + std::vector get_results() const; + bool is_ready() const; + }; + + struct CandidateB { + void process(const std::string& input); + std::vector get_results() const; + bool is_ready() const; + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, NoexceptInterfaceRequiresNoexceptCandidate) { + struct Interface { + void f() noexcept; + }; + + struct Conforming { + void f() noexcept; + }; + + struct NonNoexcept { + void f(); + }; + + static_assert(is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, NonNoexceptInterfaceAcceptsNoexceptCandidate) { + struct Interface { + void f(); + }; + + struct NoexceptCandidate { + void f() noexcept; + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, RefQualifiedInterfaceMembersAreRejected) { + struct LvalueRefInterface { + void f() &; + }; + + struct RvalueRefInterface { + void f() &&; + }; + + struct MatchingLvalueRefCandidate { + void f() &; + }; + + struct MatchingRvalueRefCandidate { + void f() &&; + }; + + struct UnqualifiedCandidate { + void f(); + }; + +#ifdef __cpp_constexpr_exceptions + static_assert(conformance_check_rejects()); + static_assert( + conformance_check_rejects()); + static_assert(conformance_check_rejects()); + static_assert( + conformance_check_rejects()); +#endif // __cpp_constexpr_exceptions +} + +TEST(ConformsToTest, UnsupportedOperatorsAreRejected) { + struct EqualsInterface { + EqualsInterface& operator=(int); + }; + + struct CoAwaitInterface { + int operator co_await(); + }; + + struct Candidate {}; + +#ifdef __cpp_constexpr_exceptions + static_assert(conformance_check_rejects()); + static_assert(conformance_check_rejects()); +#endif // __cpp_constexpr_exceptions +} + +TEST(ConformsToTest, UnaryAmpersandInterfaceMemberIsRejected) { + struct UnaryAmpersandInterface { + // NOLINTBEGIN(google-runtime-operator): the rejection is what's under + // test. + int* operator&(); + // NOLINTEND(google-runtime-operator) + }; + + struct BinaryAmpersandInterface { + int operator&(int rhs); + }; + + struct Candidate { + int operator&(int rhs) { return rhs; } + }; + +#ifdef __cpp_constexpr_exceptions + static_assert( + conformance_check_rejects()); +#endif // __cpp_constexpr_exceptions + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, ExplicitObjectInterfaceMembersAreRejected) { + struct ExplicitObjectInterface { + int f(this const ExplicitObjectInterface&); + }; + + struct Candidate { + int f() const { return 0; } + }; + +#ifdef __cpp_constexpr_exceptions + static_assert( + conformance_check_rejects()); +#endif // __cpp_constexpr_exceptions +} + +TEST(ConformsToTest, UnqualifiedInterfaceDoesNotMatchRefQualifiedCandidate) { + struct Interface { + void f(); + }; + + struct LvalueRefCandidate { + void f() &; + }; + + struct RvalueRefCandidate { + void f() &&; + }; + + static_assert(!is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, OverloadedMemberFunctionsConform) { + struct Interface { + int compute(int value); + double compute(double value); + std::string compute(const std::string& value) const; + }; + + struct Conforming { + int compute(int value) { return value; } + + double compute(double value) { return value; } + + std::string compute(const std::string& value) const { return value; } + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, CandidateMissingOverloadDoesNotConform) { + struct Interface { + int compute(int value); + double compute(double value); + std::string compute(const std::string& value) const; + }; + + struct MissingOverloads { + int compute(int value) { return value; } + }; + + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, CandidateWithExtraOverloadsConforms) { + struct Interface { + int compute(int value); + }; + + struct CandidateWithExtraOverload { + int compute(int value) { return value; } + + double compute(double value) { return value; } + }; + + static_assert( + is_protocol_conformant()); +} + +TEST(ConformsToTest, OverloadsByArityConform) { + struct Interface { + int f(int a); + int f(int a, int b); + }; + + struct Conforming { + int f(int a) { return a; } + + int f(int a, int b) { return a + b; } + }; + + struct MissingUnaryOverload { + int f(int a, int b) { return a + b; } + }; + + static_assert(is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, ConstAndNonConstOverloadPairConforms) { + struct Interface { + int f() const; + int f(); + }; + + struct Conforming { + int f() const { return 1; } + + int f() { return 2; } + }; + + struct ConstOnly { + int f() const { return 1; } + }; + + static_assert(is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, CallOperatorConforms) { + struct Interface { + int operator()(int x) const; + }; + + struct Conforming { + int operator()(int x) const { return x + 1; } + }; + + static_assert(is_protocol_conformant()); + + auto lambda = [](int x) { return x + 1; }; + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, CallOperatorWithWrongSignatureDoesNotConform) { + struct Interface { + int operator()(int x) const; + }; + + struct WrongParam { + int operator()(double x) const { return static_cast(x); } + }; + + struct NonConst { + int operator()(int x) { return x; } + }; + + static_assert(!is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, MutableLambdaConformsToNonConstCallOperator) { + struct Interface { + int operator()(int x); + }; + + auto mutable_lambda = [](int x) mutable { return x + 1; }; + auto const_lambda = [](int x) { return x + 1; }; + + static_assert(is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, OverloadedCallOperatorsConform) { + struct Interface { + int operator()(int x); + double operator()(double x); + std::string operator()(const std::string& x) const; + }; + + struct Conforming { + int operator()(int x) { return x * 2; } + + double operator()(double x) { return x * 3.0; } + + std::string operator()(const std::string& x) const { return x + x; } + }; + + struct MissingOverloads { + int operator()(int x) { return x * 2; } + }; + + static_assert(is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, StaticCandidateConformsToConstMember) { + struct Interface { + int value() const; + }; + + struct Conforming { + static int value(); + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, StaticCandidateConformsToNonConstMember) { + struct Interface { + int next(); + }; + + struct Conforming { + static int next(); + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, RefQualifiedInterfaceMemberRejectedForStaticCandidate) { + struct Interface { + int take() &&; + }; + + struct Conforming { + static int take(); + }; + +#ifdef __cpp_constexpr_exceptions + static_assert(conformance_check_rejects()); +#endif // __cpp_constexpr_exceptions +} + +TEST(ConformsToTest, StaticCandidateWithWrongSignatureDoesNotConform) { + struct Interface { + int value(int x) const; + }; + + struct WrongParam { + static int value(double x); + }; + + struct WrongReturn { + static double value(int x); + }; + + static_assert(!is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, NoexceptInterfaceRequiresNoexceptStaticCandidate) { + struct Interface { + int value() const noexcept; + }; + + struct Conforming { + static int value() noexcept; + }; + + struct NonNoexcept { + static int value(); + }; + + static_assert(is_protocol_conformant()); + static_assert(!is_protocol_conformant()); +} + +TEST(ConformsToTest, InterfaceStaticMembersAreIgnored) { + struct Interface { + static int helper(); + int value() const; + }; + + struct Conforming { + int value() const { return 1; } + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, StaticCallOperatorConforms) { + struct Interface { + int operator()(int x) const; + }; + + struct Conforming { + static int operator()(int x) { return x + 1; } + }; + + static_assert(is_protocol_conformant()); +} + +TEST(ConformsToTest, StaticOverloadConformsAlongsideNonStatic) { + struct Interface { + int f(int x) const; + int f(double x) const; + }; + + struct Conforming { + int f(int x) const { return x; } + + static int f(double x) { return static_cast(x); } + }; + + static_assert(is_protocol_conformant()); +} + +} // namespace diff --git a/protocol_member_function_tests.cc b/protocol_member_function_tests.cc new file mode 100644 index 0000000..0178309 --- /dev/null +++ b/protocol_member_function_tests.cc @@ -0,0 +1,608 @@ +// Tests for member function calls through protocol. + +#include + +#include +#include +#include +#include + +#include "protocol.hh" +#include "tracking_allocator.h" + +using xyz::reflection::protocol; + +namespace { + +// Concepts for negative member function tests: a requires-expression naming a +// member that does not exist is only a substitution failure in a template. +template +concept has_update = requires(P& p) { p.update(0); }; + +template +concept has_get_int = requires(P& p) { p.get(0); }; + +template +concept has_get = requires(P& p) { p.get(); }; + +// Member function forwarding tests for protocol. + +TEST(ReflectionProtocolTest, ConstMemberFunction) { + struct Interface { + int get_value() const; + }; + + struct Conforming { + int get_value() const { return 42; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.get_value(), 42); +} + +TEST(ReflectionProtocolTest, NonConstMemberFunctionNotInvocableFromConst) { + struct Interface { + void update(int value); + }; + + // `protocol` propagates const: a const protocol exposes only the const + // member functions of the interface. + static_assert( + !std::is_invocable_v< + decltype((std::declval&>().update)), int>); + static_assert(has_update>); + static_assert(!has_update>); +} + +TEST(ReflectionProtocolTest, SingleParameterMemberFunction) { + struct Interface { + void update(int value); + int get() const; + }; + + struct Conforming { + int last_value = 0; + + void update(int value) { last_value = value; } + + int get() const { return last_value; } + }; + + protocol p(Conforming{}); + p.update(42); + EXPECT_EQ(p.get(), 42); + static_assert(!noexcept(p.update(1))); +} + +TEST(ReflectionProtocolTest, NoexceptMemberFunction) { + struct Interface { + double compute(double input) noexcept; + }; + + struct Conforming { + double compute(double input) noexcept { return input * 2.0; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.compute(21.0), 42.0); + static_assert(noexcept(p.compute(1.0))); +} + +TEST(ReflectionProtocolTest, MultiParameterMemberFunction) { + struct Interface { + int add(int a, int b) const; + }; + + struct Conforming { + int add(int a, int b) const { return a + b; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.add(1, 2), 3); +} + +TEST(ReflectionProtocolTest, VoidMemberFunction) { + struct Interface { + void reset(); + bool was_reset() const; + }; + + struct Conforming { + bool reset_flag = false; + + void reset() { reset_flag = true; } + + bool was_reset() const { return reset_flag; } + }; + + protocol p(Conforming{}); + p.reset(); + EXPECT_TRUE(p.was_reset()); +} + +TEST(ReflectionProtocolTest, MultipleMemberFunctions) { + struct Interface { + double add(double x, double y) const noexcept; + double multiply(double x, double y) const noexcept; + }; + + struct Conforming { + double add(double x, double y) const noexcept { return x + y; } + + double multiply(double x, double y) const noexcept { return x * y; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.add(1.0, 2.0), 3.0); + EXPECT_EQ(p.multiply(3.0, 4.0), 12.0); +} + +TEST(ReflectionProtocolTest, MixedConstAndMutatingMemberFunctions) { + struct Interface { + int get() const; + void set(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.get(), 0); + p.set(7); + EXPECT_EQ(p.get(), 7); +} + +TEST(ReflectionProtocolTest, StaticMemberFunction) { + struct Interface { + int value() const; + }; + + struct Conforming { + static int value() { return 42; } + }; + + protocol p(std::in_place_type); + EXPECT_EQ(p.value(), 42); +} + +TEST(ReflectionProtocolTest, StaticMemberFunctionSatisfiesNonConstMember) { + struct Interface { + int next(); + }; + + struct Conforming { + static int next() { + static int counter = 0; + return ++counter; + } + }; + + protocol p(std::in_place_type); + EXPECT_EQ(p.next(), 1); + EXPECT_EQ(p.next(), 2); +} + +TEST(ReflectionProtocolTest, ForwardingAfterCopyConstruction) { + struct Interface { + int get() const; + void set(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value; } + }; + + protocol a(Conforming{}); + a.set(1); + // NOLINTBEGIN(performance-unnecessary-copy-initialization): the test + // exercises copy construction on purpose. + protocol b(a); + // NOLINTEND(performance-unnecessary-copy-initialization) + b.set(2); + + // protocol owns a copy of the underlying object, so copies are + // independent of one another. + EXPECT_EQ(a.get(), 1); + EXPECT_EQ(b.get(), 2); +} + +TEST(ReflectionProtocolTest, ForwardingAfterMoveConstruction) { + struct Interface { + int get() const; + void set(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value; } + }; + + protocol a(Conforming{}); + a.set(5); + protocol b(std::move(a)); + + EXPECT_EQ(b.get(), 5); + // NOLINTBEGIN(bugprone-use-after-move,hicpp-invalid-access-moved): the + // test exercises the moved-from state on purpose. + EXPECT_TRUE(valueless_after_move(a)); + // NOLINTEND(bugprone-use-after-move,hicpp-invalid-access-moved) +} + +TEST(ReflectionProtocolTest, ForwardingAfterCopyAssignment) { + struct Interface { + int get() const; + void set(int value); + }; + + // Counter and Doubler both conform to Interface but have different + // semantics for set(), so we can tell whether copy assignment updated + // the vtable pointer. + struct Counter { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value; } + }; + + struct Doubler { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value * 2; } + }; + + protocol a(Counter{}); + protocol b(Doubler{}); + + a = b; + a.set(10); + EXPECT_EQ(a.get(), 20); // a now has Doubler's semantics. +} + +TEST(ReflectionProtocolTest, ForwardingAfterMoveAssignment) { + struct Interface { + int get() const; + void set(int value); + }; + + struct Counter { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value; } + }; + + struct Doubler { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value * 2; } + }; + + protocol a(Counter{}); + protocol b(Doubler{}); + + a = std::move(b); + a.set(10); + EXPECT_EQ(a.get(), 20); // a now has Doubler's semantics. + // NOLINTBEGIN(bugprone-use-after-move,hicpp-invalid-access-moved): the + // test exercises the moved-from state on purpose. + EXPECT_TRUE(valueless_after_move(b)); + // NOLINTEND(bugprone-use-after-move,hicpp-invalid-access-moved) +} + +TEST(ReflectionProtocolTest, ForwardingAfterSwap) { + struct Interface { + int get() const; + void set(int value); + }; + + struct Counter { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value; } + }; + + struct Doubler { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value * 2; } + }; + + protocol a(Counter{}); + protocol b(Doubler{}); + + using std::swap; + swap(a, b); + + a.set(10); + EXPECT_EQ(a.get(), 20); // a now behaves like Doubler. + + b.set(10); + EXPECT_EQ(b.get(), 10); // b now behaves like Counter. +} + +TEST(ReflectionProtocolTest, ForwardingWithInPlaceConstruction) { + struct Interface { + int get() const; + }; + + struct Conforming { + int value; + + explicit Conforming(int initial_value) : value(initial_value) {} + + int get() const { return value; } + }; + + protocol p(std::in_place_type, 42); + EXPECT_EQ(p.get(), 42); +} + +TEST(ReflectionProtocolTest, ForwardingWithCustomAllocator) { + struct Interface { + int get() const; + }; + + struct Conforming { + int value; + + explicit Conforming(int initial_value) : value(initial_value) {} + + int get() const { return value; } + }; + + unsigned allocs = 0; + unsigned deallocs = 0; + xyz::TrackingAllocator alloc{&allocs, &deallocs}; + + protocol> p( + std::allocator_arg, alloc, Conforming(42)); + + EXPECT_EQ(p.get(), 42); + EXPECT_EQ(allocs, 1); +} + +TEST(ReflectionProtocolTest, ConstMemberFunctionOnConstProtocol) { + struct Interface { + int get_value() const; + }; + + struct Conforming { + int get_value() const { return 42; } + }; + + const protocol p(Conforming{}); + EXPECT_EQ(p.get_value(), 42); + + // A non-const member function is still not invocable on a const protocol; + // that assertion is already covered above by + // NonConstMemberFunctionNotInvocableFromConst. +} + +TEST(ReflectionProtocolTest, OverloadsByParameterType) { + struct Interface { + int compute(int x); + double compute(double x); + std::string compute(const std::string& x) const; + }; + + struct Conforming { + int compute(int x) { return x * 2; } + + double compute(double x) { return x * 3.0; } + + std::string compute(const std::string& x) const { return x + x; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.compute(5), 10); + EXPECT_EQ(p.compute(5.0), 15.0); + + const auto& const_p = p; + EXPECT_EQ(const_p.compute(std::string("A")), "AA"); +} + +TEST(ReflectionProtocolTest, OverloadsByArity) { + struct Interface { + int add(int a); + int add(int a, int b); + }; + + struct Conforming { + int add(int a) { return a; } + + int add(int a, int b) { return a + b; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.add(1), 1); + EXPECT_EQ(p.add(1, 2), 3); +} + +TEST(ReflectionProtocolTest, ConstAndNonConstOverloadPair) { + struct Interface { + int value() const; + int value(); + }; + + struct Conforming { + int value() const { return 1; } + + int value() { return 2; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.value(), 2); + + const protocol& const_p = p; + EXPECT_EQ(const_p.value(), 1); +} + +TEST(ReflectionProtocolTest, ConstProtocolExposesOnlyConstOverloads) { + struct Interface { + int get() const; + void get(int value); + }; + + static_assert(!has_get_int>); + static_assert(has_get_int>); + + static_assert(has_get>); + static_assert(has_get>); +} + +TEST(ReflectionProtocolTest, MemberThunksCannotBeDetachedForOverloads) { + struct Interface { + int compute(int x); + double compute(double x); + std::string compute(const std::string& x) const; + }; + + struct Conforming { + int compute(int x) { return x * 2; } + + double compute(double x) { return x * 3.0; } + + std::string compute(const std::string& x) const { return x + x; } + }; + + protocol p(Conforming{}); + + 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_default_constructible_v); + static_assert(!std::is_destructible_v); + static_assert(std::is_trivially_copyable_v); +} + +TEST(ReflectionProtocolTest, OverloadsThroughThunkReference) { + struct Interface { + int compute(int x); + double compute(double x); + std::string compute(const std::string& x) const; + }; + + struct Conforming { + int compute(int x) { return x * 2; } + + double compute(double x) { return x * 3.0; } + + std::string compute(const std::string& x) const { return x + x; } + }; + + protocol p(Conforming{}); + + // Const propagates through `protocol`: the non-const overloads need a + // non-const reference to the thunk. + auto& compute = p.compute; + EXPECT_EQ(compute(5), 10); + EXPECT_EQ(compute(5.0), 15.0); + const auto& const_compute = p.compute; + EXPECT_EQ(const_compute(std::string("A")), "AA"); +} + +TEST(ReflectionProtocolTest, NoexceptOverload) { + struct Interface { + int f(int x) noexcept; + int f(double x); + }; + + struct Conforming { + int f(int x) noexcept { return x * 2; } + + int f(double x) { return static_cast(x * 3.0); } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.f(5), 10); + EXPECT_EQ(p.f(5.0), 15); + static_assert(noexcept(p.f(1))); + static_assert(!noexcept(p.f(1.0))); +} + +// Tests that dispatching fails gracefully for a moved-from protocol, for +// named member functions. Operator dispatch has the same tests in +// protocol_operator_tests.cc. +#if (defined(_MSC_VER) && defined(_DEBUG)) || (!defined(NDEBUG)) + +TEST(ReflectionProtocolTest, MutableValuelessCall) { + struct Interface { + int foo(); + }; + + struct TypeA { + int foo() { return 5; } + }; + + protocol p(TypeA{}); + EXPECT_EQ(p.foo(), 5); + + auto _ = std::move(p); + // NOLINTBEGIN(bugprone-use-after-move,hicpp-invalid-access-moved): the + // test exercises the moved-from state on purpose. + EXPECT_TRUE(valueless_after_move(p)); + + EXPECT_DEATH(p.foo(), "cannot call member function of valueless protocol"); + // NOLINTEND(bugprone-use-after-move,hicpp-invalid-access-moved) +} + +TEST(ReflectionProtocolTest, ConstValuelessCall) { + struct Interface { + int foo() const; + }; + + struct TypeA { + int foo() const { return 5; } + }; + + protocol p(TypeA{}); + EXPECT_EQ(p.foo(), 5); + + auto _ = std::move(p); + // NOLINTBEGIN(bugprone-use-after-move,hicpp-invalid-access-moved): the + // test exercises the moved-from state on purpose. + EXPECT_TRUE(valueless_after_move(p)); + + EXPECT_DEATH(p.foo(), "cannot call member function of valueless protocol"); + // NOLINTEND(bugprone-use-after-move,hicpp-invalid-access-moved) +} + +#endif + +TEST(ReflectionProtocolTest, VolatileConformingType) { + struct Interface { + int foo(); + }; + + struct Conforming { + int foo() volatile { return 10; } + }; + + protocol p(Conforming{}); + EXPECT_EQ(p.foo(), 10); +} + +} // namespace diff --git a/protocol_test.cc b/protocol_test.cc index 0574d76..0fe902e 100644 --- a/protocol_test.cc +++ b/protocol_test.cc @@ -1,23 +1,18 @@ // Tests for the C++26-reflection-based implementation of protocol and -// protocol_view. +// protocol_view: type traits, special member functions, constructability, +// protocol_cast and target_type. #include "protocol.hh" #include #include -#include -#include +#include #include #include #include #include -#include -#include "test_helpers.h" -#include "tracking_allocator.h" - -using xyz::reflection::is_protocol_conformant; using xyz::reflection::is_protocol_v; using xyz::reflection::is_protocol_view_v; using xyz::reflection::is_valid_interface; @@ -26,21 +21,6 @@ using xyz::reflection::protocol_view; namespace { -// Concepts for negative member function tests: a requires-expression naming a -// member that does not exist is only a substitution failure in a template. -template -concept has_update = requires(P& p) { p.update(0); }; - -template -concept has_get_value = requires(P& p) { p.get_value(); }; - -// Concepts for overloaded `get`/`get(int)` negative tests. -template -concept has_get_int = requires(P& p) { p.get(0); }; - -template -concept has_get = requires(P& p) { p.get(); }; - // --------------------------------------------------------------------------- // Type trait tests. // --------------------------------------------------------------------------- @@ -391,706 +371,159 @@ TEST(ReflectionProtocolTest, SelfAssignmentLeavesValueUnchanged) { } // --------------------------------------------------------------------------- -// Conformance check tests. +// Constructability tests. // --------------------------------------------------------------------------- -// Returns `true` if checking conformance of `Candidate` against `Interface` -// throws during constant evaluation, as it does for a ref-qualified -// interface member function. Observing the rejection means catching the -// exception at constant evaluation time (P3068 constexpr exceptions), which -// GCC trunk implements but the clang-p2996 fork used for clang-tidy does -// not; the rejection itself does not depend on P3068. -#ifdef __cpp_constexpr_exceptions -template -consteval bool conformance_check_rejects() { - try { - (void)is_protocol_conformant(); - } catch (const std::runtime_error&) { - return true; - } - return false; -} -#endif // __cpp_constexpr_exceptions - -TEST(ConformsToTest, EmptyInterfaceIsAlwaysSatisfied) { - struct EmptyInterface {}; - - struct Candidate {}; - - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, CandidateTypeConformsWhenAllMethodsMatch) { +TEST(ReflectionProtocolTest, IsConstructibleFromConformingType) { struct Interface { std::string_view name() const noexcept; - int count(); }; - struct Candidate { + struct Conforming { std::string_view name() const noexcept; - int count(); - }; - - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, CandidateTypeConformsWithExtraMethodsPresent) { - struct Interface { - void process(); - }; - - struct CandidateWithExtra { - void process(); - void extra_method(); - int another() const; - }; - - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, CandidateTypeMissingMethodDoesNotConform) { - struct Interface { - void foo(); - void bar(); - }; - - struct MissingBar { - void foo(); - }; - - static_assert(!is_protocol_conformant()); -} - -TEST(ConformsToTest, WrongConstnessDoesNotConform) { - struct Interface { - int value() const; }; - struct NonConst { - int value(); // not const — does not match the interface - }; + struct NonConforming {}; - static_assert(!is_protocol_conformant()); + static_assert(std::is_constructible_v, Conforming>); + static_assert(!std::is_constructible_v, NonConforming>); } -TEST(ConformsToTest, WrongReturnTypeDoesNotConform) { +TEST(ReflectionProtocolTest, IsConstructibleFromMoveOnlyType) { struct Interface { - int compute(); - }; - - struct WrongReturn { - double compute(); - }; + Interface(const Interface&) = delete; + Interface(Interface&&) = delete; - static_assert(!is_protocol_conformant()); -} + Interface& operator=(const Interface&) = delete; + Interface& operator=(Interface&&) = delete; -TEST(ConformsToTest, WrongParameterTypeDoesNotConform) { - struct Interface { - void process(int value); + ~Interface() = default; }; - struct WrongParam { - void process(double value); - }; + static_assert(!std::is_move_constructible_v>); + static_assert(!std::is_move_assignable_v>); - static_assert(!is_protocol_conformant()); -} + struct Default {}; -TEST(ConformsToTest, WrongParameterCountDoesNotConform) { - struct Interface { - void process(int a, int b); - }; + static_assert(std::is_constructible_v, Default>); - struct WrongArity { - void process(int a); - }; + struct MoveOnly { + MoveOnly(const MoveOnly&) = delete; + MoveOnly(MoveOnly&&) = default; - static_assert(!is_protocol_conformant()); -} + MoveOnly& operator=(const MoveOnly&) = delete; + MoveOnly& operator=(MoveOnly&&) = default; -TEST(ConformsToTest, MultipleParametersMatchCorrectly) { - struct Interface { - void write(int length, double value); + ~MoveOnly() = default; }; - struct Candidate { - void write(int length, double value); - }; + static_assert(std::is_constructible_v, MoveOnly>); - static_assert(is_protocol_conformant()); -} + struct Immovable { + Immovable(const Immovable&) = delete; + Immovable(Immovable&&) = delete; -TEST(ConformsToTest, CandidateTypeConformsForTypicalInterfaceB) { - struct InterfaceB { - void process(const std::string& input); - std::vector get_results() const; - bool is_ready() const; - }; + Immovable& operator=(const Immovable&) = delete; + Immovable& operator=(Immovable&&) = delete; - struct CandidateB { - void process(const std::string& input); - std::vector get_results() const; - bool is_ready() const; + ~Immovable() = default; }; - static_assert(is_protocol_conformant()); + static_assert(std::is_constructible_v, Immovable>); } -TEST(ConformsToTest, NoexceptInterfaceRequiresNoexceptCandidate) { +TEST(ReflectionProtocolViewTest, IsConstructibleFromConformingType) { struct Interface { - void f() noexcept; + std::string_view name() const noexcept; }; struct Conforming { - void f() noexcept; - }; - - struct NonNoexcept { - void f(); - }; - - static_assert(is_protocol_conformant()); - static_assert(!is_protocol_conformant()); -} - -TEST(ConformsToTest, NonNoexceptInterfaceAcceptsNoexceptCandidate) { - struct Interface { - void f(); - }; - - struct NoexceptCandidate { - void f() noexcept; - }; - - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, RefQualifiedInterfaceMembersAreRejected) { - struct LvalueRefInterface { - void f() &; - }; - - struct RvalueRefInterface { - void f() &&; - }; - - struct MatchingLvalueRefCandidate { - void f() &; - }; - - struct MatchingRvalueRefCandidate { - void f() &&; - }; - - struct UnqualifiedCandidate { - void f(); - }; - -#ifdef __cpp_constexpr_exceptions - static_assert(conformance_check_rejects()); - static_assert( - conformance_check_rejects()); - static_assert(conformance_check_rejects()); - static_assert( - conformance_check_rejects()); -#endif // __cpp_constexpr_exceptions -} - -TEST(ConformsToTest, UnsupportedOperatorsAreRejected) { - struct EqualsInterface { - EqualsInterface& operator=(int); - }; - - struct CoAwaitInterface { - int operator co_await(); - }; - - struct Candidate {}; - -#ifdef __cpp_constexpr_exceptions - static_assert(conformance_check_rejects()); - static_assert(conformance_check_rejects()); -#endif // __cpp_constexpr_exceptions -} - -TEST(ConformsToTest, UnaryAmpersandInterfaceMemberIsRejected) { - struct UnaryAmpersandInterface { - // NOLINTBEGIN(google-runtime-operator): the rejection is what's under - // test. - int* operator&(); - // NOLINTEND(google-runtime-operator) - }; - - struct BinaryAmpersandInterface { - int operator&(int rhs); - }; - - struct Candidate { - int operator&(int rhs) { return rhs; } - }; - -#ifdef __cpp_constexpr_exceptions - static_assert( - conformance_check_rejects()); -#endif // __cpp_constexpr_exceptions - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, ExplicitObjectInterfaceMembersAreRejected) { - struct ExplicitObjectInterface { - int f(this const ExplicitObjectInterface&); + std::string_view name() const noexcept; }; - struct Candidate { - int f() const { return 0; } - }; + struct NonConforming {}; -#ifdef __cpp_constexpr_exceptions + // protocol_view's constructor takes U&, so constructibility is checked + // from an lvalue, not a prvalue. + static_assert(std::is_constructible_v, Conforming&>); + static_assert(std::is_convertible_v>); static_assert( - conformance_check_rejects()); -#endif // __cpp_constexpr_exceptions -} - -TEST(ConformsToTest, UnqualifiedInterfaceDoesNotMatchRefQualifiedCandidate) { - struct Interface { - void f(); - }; - - struct LvalueRefCandidate { - void f() &; - }; - - struct RvalueRefCandidate { - void f() &&; - }; - - static_assert(!is_protocol_conformant()); - static_assert(!is_protocol_conformant()); + !std::is_constructible_v, NonConforming&>); + // A view of a temporary would dangle. + static_assert(!std::is_constructible_v, Conforming>); } -TEST(ConformsToTest, OverloadedMemberFunctionsConform) { +TEST(ReflectionProtocolViewTest, NotConstructibleFromConstObject) { + // protocol_view rejects a const object unconditionally: even though + // Interface has no non-const methods (so a const object would actually + // be safe to dispatch through), construction from one is still rejected, + // because the rule doesn't inspect Interface at all. struct Interface { - int compute(int value); - double compute(double value); - std::string compute(const std::string& value) const; + int get() const; }; struct Conforming { - int compute(int value) { return value; } - - double compute(double value) { return value; } - - std::string compute(const std::string& value) const { return value; } - }; - - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, CandidateMissingOverloadDoesNotConform) { - struct Interface { - int compute(int value); - double compute(double value); - std::string compute(const std::string& value) const; - }; - - struct MissingOverloads { - int compute(int value) { return value; } - }; - - static_assert(!is_protocol_conformant()); -} - -TEST(ConformsToTest, CandidateWithExtraOverloadsConforms) { - struct Interface { - int compute(int value); - }; - - struct CandidateWithExtraOverload { - int compute(int value) { return value; } - - double compute(double value) { return value; } + int get() const { return 0; } }; + static_assert(std::is_constructible_v, Conforming&>); static_assert( - is_protocol_conformant()); -} - -TEST(ConformsToTest, OverloadsByArityConform) { - struct Interface { - int f(int a); - int f(int a, int b); - }; - - struct Conforming { - int f(int a) { return a; } - - int f(int a, int b) { return a + b; } - }; - - struct MissingUnaryOverload { - int f(int a, int b) { return a + b; } - }; - - static_assert(is_protocol_conformant()); - static_assert(!is_protocol_conformant()); -} - -TEST(ConformsToTest, ConstAndNonConstOverloadPairConforms) { - struct Interface { - int f() const; - int f(); - }; - - struct Conforming { - int f() const { return 1; } - - int f() { return 2; } - }; - - struct ConstOnly { - int f() const { return 1; } - }; - - static_assert(is_protocol_conformant()); - static_assert(!is_protocol_conformant()); -} - -TEST(ConformsToTest, CallOperatorConforms) { - struct Interface { - int operator()(int x) const; - }; - - struct Conforming { - int operator()(int x) const { return x + 1; } - }; - - static_assert(is_protocol_conformant()); - - auto lambda = [](int x) { return x + 1; }; - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, CallOperatorWithWrongSignatureDoesNotConform) { - struct Interface { - int operator()(int x) const; - }; - - struct WrongParam { - int operator()(double x) const { return static_cast(x); } - }; - - struct NonConst { - int operator()(int x) { return x; } - }; - - static_assert(!is_protocol_conformant()); - static_assert(!is_protocol_conformant()); -} - -TEST(ConformsToTest, MutableLambdaConformsToNonConstCallOperator) { - struct Interface { - int operator()(int x); - }; - - auto mutable_lambda = [](int x) mutable { return x + 1; }; - auto const_lambda = [](int x) { return x + 1; }; - - static_assert(is_protocol_conformant()); - static_assert(!is_protocol_conformant()); + !std::is_constructible_v, const Conforming&>); } -TEST(ConformsToTest, OverloadedCallOperatorsConform) { +TEST(ReflectionProtocolViewTest, ConstViewIsConstructibleFromConstObject) { struct Interface { - int operator()(int x); - double operator()(double x); - std::string operator()(const std::string& x) const; + std::string_view name() const noexcept; }; struct Conforming { - int operator()(int x) { return x * 2; } - - double operator()(double x) { return x * 3.0; } - - std::string operator()(const std::string& x) const { return x + x; } + std::string_view name() const noexcept; }; - struct MissingOverloads { - int operator()(int x) { return x * 2; } - }; + struct NonConforming {}; - static_assert(is_protocol_conformant()); - static_assert(!is_protocol_conformant()); + static_assert(std::is_constructible_v, + const Conforming&>); + static_assert( + std::is_constructible_v, Conforming&>); + static_assert( + std::is_convertible_v>); + static_assert( + std::is_convertible_v>); + static_assert(!std::is_constructible_v, + const NonConforming&>); + // A view of a temporary would dangle. + static_assert( + !std::is_constructible_v, Conforming>); + static_assert(!std::is_constructible_v, + const Conforming>); } -TEST(ConformsToTest, StaticCandidateConformsToConstMember) { +TEST(ReflectionProtocolTest, IsConstructibleInPlaceFromConformingType) { struct Interface { - int value() const; + std::string_view name() const noexcept; }; struct Conforming { - static int value(); - }; - - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, StaticCandidateConformsToNonConstMember) { - struct Interface { - int next(); + std::string_view name() const noexcept; }; - struct Conforming { - static int next(); - }; + struct NonConforming {}; - static_assert(is_protocol_conformant()); + static_assert(std::is_constructible_v, + std::in_place_type_t>); + static_assert(!std::is_constructible_v, + std::in_place_type_t>); } -TEST(ConformsToTest, RefQualifiedInterfaceMemberRejectedForStaticCandidate) { +TEST(ReflectionProtocolTest, IsConstructibleInPlaceWithArguments) { struct Interface { - int take() &&; + std::string_view name() const noexcept; }; struct Conforming { - static int take(); - }; - -#ifdef __cpp_constexpr_exceptions - static_assert(conformance_check_rejects()); -#endif // __cpp_constexpr_exceptions -} - -TEST(ConformsToTest, StaticCandidateWithWrongSignatureDoesNotConform) { - struct Interface { - int value(int x) const; - }; - - struct WrongParam { - static int value(double x); - }; - - struct WrongReturn { - static double value(int x); - }; - - static_assert(!is_protocol_conformant()); - static_assert(!is_protocol_conformant()); -} - -TEST(ConformsToTest, NoexceptInterfaceRequiresNoexceptStaticCandidate) { - struct Interface { - int value() const noexcept; - }; - - struct Conforming { - static int value() noexcept; - }; - - struct NonNoexcept { - static int value(); - }; - - static_assert(is_protocol_conformant()); - static_assert(!is_protocol_conformant()); -} - -TEST(ConformsToTest, InterfaceStaticMembersAreIgnored) { - struct Interface { - static int helper(); - int value() const; - }; - - struct Conforming { - int value() const { return 1; } - }; - - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, StaticCallOperatorConforms) { - struct Interface { - int operator()(int x) const; - }; - - struct Conforming { - static int operator()(int x) { return x + 1; } - }; - - static_assert(is_protocol_conformant()); -} - -TEST(ConformsToTest, StaticOverloadConformsAlongsideNonStatic) { - struct Interface { - int f(int x) const; - int f(double x) const; - }; - - struct Conforming { - int f(int x) const { return x; } - - static int f(double x) { return static_cast(x); } - }; - - static_assert(is_protocol_conformant()); -} - -// --------------------------------------------------------------------------- -// Constructability tests. -// --------------------------------------------------------------------------- - -TEST(ReflectionProtocolTest, IsConstructibleFromConformingType) { - struct Interface { - std::string_view name() const noexcept; - }; - - struct Conforming { - std::string_view name() const noexcept; - }; - - struct NonConforming {}; - - static_assert(std::is_constructible_v, Conforming>); - static_assert(!std::is_constructible_v, NonConforming>); -} - -TEST(ReflectionProtocolTest, IsConstructibleFromMoveOnlyType) { - struct Interface { - Interface(const Interface&) = delete; - Interface(Interface&&) = delete; - - Interface& operator=(const Interface&) = delete; - Interface& operator=(Interface&&) = delete; - - ~Interface() = default; - }; - - static_assert(!std::is_move_constructible_v>); - static_assert(!std::is_move_assignable_v>); - - struct Default {}; - - static_assert(std::is_constructible_v, Default>); - - struct MoveOnly { - MoveOnly(const MoveOnly&) = delete; - MoveOnly(MoveOnly&&) = default; - - MoveOnly& operator=(const MoveOnly&) = delete; - MoveOnly& operator=(MoveOnly&&) = default; - - ~MoveOnly() = default; - }; - - static_assert(std::is_constructible_v, MoveOnly>); - - struct Immovable { - Immovable(const Immovable&) = delete; - Immovable(Immovable&&) = delete; - - Immovable& operator=(const Immovable&) = delete; - Immovable& operator=(Immovable&&) = delete; - - ~Immovable() = default; - }; - - static_assert(std::is_constructible_v, Immovable>); -} - -TEST(ReflectionProtocolViewTest, IsConstructibleFromConformingType) { - struct Interface { - std::string_view name() const noexcept; - }; - - struct Conforming { - std::string_view name() const noexcept; - }; - - struct NonConforming {}; - - // protocol_view's constructor takes U&, so constructibility is checked - // from an lvalue, not a prvalue. - static_assert(std::is_constructible_v, Conforming&>); - static_assert(std::is_convertible_v>); - static_assert( - !std::is_constructible_v, NonConforming&>); - // A view of a temporary would dangle. - static_assert(!std::is_constructible_v, Conforming>); -} - -TEST(ReflectionProtocolViewTest, NotConstructibleFromConstObject) { - // protocol_view rejects a const object unconditionally: even though - // Interface has no non-const methods (so a const object would actually - // be safe to dispatch through), construction from one is still rejected, - // because the rule doesn't inspect Interface at all. - struct Interface { - int get() const; - }; - - struct Conforming { - int get() const { return 0; } - }; - - static_assert(std::is_constructible_v, Conforming&>); - static_assert( - !std::is_constructible_v, const Conforming&>); -} - -TEST(ReflectionProtocolViewTest, ConstViewIsConstructibleFromConstObject) { - struct Interface { - std::string_view name() const noexcept; - }; - - struct Conforming { - std::string_view name() const noexcept; - }; - - struct NonConforming {}; - - static_assert(std::is_constructible_v, - const Conforming&>); - static_assert( - std::is_constructible_v, Conforming&>); - static_assert( - std::is_convertible_v>); - static_assert( - std::is_convertible_v>); - static_assert(!std::is_constructible_v, - const NonConforming&>); - // A view of a temporary would dangle. - static_assert( - !std::is_constructible_v, Conforming>); - static_assert(!std::is_constructible_v, - const Conforming>); -} - -TEST(ReflectionProtocolTest, IsConstructibleInPlaceFromConformingType) { - struct Interface { - std::string_view name() const noexcept; - }; - - struct Conforming { - std::string_view name() const noexcept; - }; - - struct NonConforming {}; - - static_assert(std::is_constructible_v, - std::in_place_type_t>); - static_assert(!std::is_constructible_v, - std::in_place_type_t>); -} - -TEST(ReflectionProtocolTest, IsConstructibleInPlaceWithArguments) { - struct Interface { - std::string_view name() const noexcept; - }; - - struct Conforming { - explicit Conforming(std::string_view value); - std::string_view name() const noexcept; + explicit Conforming(std::string_view value); + std::string_view name() const noexcept; }; static_assert(std::is_constructible_v, @@ -1098,1417 +531,6 @@ TEST(ReflectionProtocolTest, IsConstructibleInPlaceWithArguments) { std::string_view>); } -// Member function signature tests for protocol_view. - -TEST(ReflectionProtocolViewTest, ConstMemberFunction) { - struct Interface { - int get_value() const; - }; - - struct Conforming { - int get_value() const { return 42; } - }; - - Conforming c; - protocol_view p(c); - EXPECT_EQ(p.get_value(), 42); -} - -TEST(ReflectionProtocolViewTest, NonConstMemberFunctionInvocableFromConstView) { - struct Interface { - void update(int value); - }; - - // `protocol_view` has shallow const: a const view still exposes the - // non-const member functions of the interface. - static_assert(requires(const protocol_view& p) { - { p.update(0) } -> std::same_as; - }); - static_assert(has_update>); -} - -TEST(ReflectionProtocolViewTest, ConstViewExposesOnlyConstMemberFunctions) { - struct Interface { - int get_value() const; - void update(int value); - }; - - static_assert(has_get_value>); - static_assert(has_get_value>); - static_assert(!has_update>); - static_assert(!has_update>); - - static_assert(requires(const protocol_view& p) { - { p.get_value() } -> std::same_as; - }); -} - -TEST(ReflectionProtocolViewTest, NonConstMemberFunctionCalledThroughConstView) { - struct Interface { - void update(int value); - }; - - struct Conforming { - int last_value = 0; - - void update(int value) { last_value = value; } - }; - - Conforming c; - const protocol_view p(c); - p.update(42); - EXPECT_EQ(c.last_value, 42); -} - -TEST(ReflectionProtocolViewTest, ConstViewCallsConstMemberFunction) { - struct Interface { - int get_value() const; - void update(int value); - }; - - struct Conforming { - int value = 7; - - int get_value() const { return value; } - - void update(int new_value) { value = new_value; } - }; - - const Conforming const_object; - protocol_view view_of_const(const_object); - EXPECT_EQ(view_of_const.get_value(), 7); - - Conforming mutable_object; - mutable_object.update(9); - const protocol_view view_of_mutable(mutable_object); - EXPECT_EQ(view_of_mutable.get_value(), 9); -} - -TEST(ReflectionProtocolViewTest, ConstViewOfInterfaceWithNoConstMembers) { - struct Interface { - void update(int value); - }; - - // A const view of an interface with no const member functions is - // well-formed but exposes nothing. - static_assert(!has_update>); - static_assert(std::is_copy_constructible_v>); -} - -TEST(ReflectionProtocolViewTest, SingleParameterMemberFunction) { - struct Interface { - void update(int value); - }; - - struct Conforming { - int last_value = 0; - - void update(int value) { last_value = value; } - }; - - Conforming c; - protocol_view p(c); - p.update(42); - EXPECT_EQ(c.last_value, 42); - static_assert(!noexcept(c.update(1.0))); -} - -TEST(ReflectionProtocolViewTest, NoexceptMemberFunction) { - struct Interface { - double compute(double input) noexcept; - }; - - struct Conforming { - double compute(double input) noexcept { return input * 2.0; } - }; - - Conforming c; - protocol_view p(c); - EXPECT_EQ(p.compute(21.0), 42.0); - static_assert(noexcept(p.compute(1.0))); -} - -TEST(ReflectionProtocolViewTest, MultiParameterMemberFunction) { - struct Interface { - int add(int a, int b) const; - }; - - struct Conforming { - int add(int a, int b) const { return a + b; } - }; - - Conforming c; - protocol_view p(c); - EXPECT_EQ(p.add(1, 2), 3); -} - -TEST(ReflectionProtocolViewTest, VoidMemberFunction) { - struct Interface { - void reset(); - }; - - struct Conforming { - bool was_reset = false; - - void reset() { was_reset = true; } - }; - - Conforming c; - protocol_view p(c); - p.reset(); - EXPECT_TRUE(c.was_reset); -} - -TEST(ReflectionProtocolViewTest, MultipleMemberFunctions) { - struct Interface { - double add(double x, double y) const noexcept; - double multiply(double x, double y) const noexcept; - }; - - struct Conforming { - double add(double x, double y) const noexcept { return x + y; } - - double multiply(double x, double y) const noexcept { return x * y; } - }; - - Conforming c; - protocol_view p(c); - EXPECT_EQ(p.add(1.0, 2.0), 3.0); - EXPECT_EQ(p.multiply(3.0, 4.0), 12.0); -} - -TEST(ReflectionProtocolViewTest, MixedConstAndMutatingMemberFunctions) { - struct Interface { - int get() const; - void set(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value; } - }; - - Conforming c; - protocol_view p(c); - EXPECT_EQ(p.get(), 0); - p.set(7); - EXPECT_EQ(p.get(), 7); - EXPECT_EQ(c.value, 7); -} - -TEST(ReflectionProtocolViewTest, OverloadsByParameterType) { - struct Interface { - int compute(int x); - double compute(double x); - std::string compute(const std::string& x) const; - }; - - struct Conforming { - int compute(int x) { return x * 2; } - - double compute(double x) { return x * 3.0; } - - std::string compute(const std::string& x) const { return x + x; } - }; - - Conforming c; - protocol_view p(c); - EXPECT_EQ(p.compute(5), 10); - EXPECT_EQ(p.compute(5.0), 15.0); - EXPECT_EQ(p.compute(std::string("A")), "AA"); -} - -TEST(ReflectionProtocolViewTest, - ConstAndNonConstOverloadPairDispatchesToNonConst) { - struct Interface { - int value() const; - int value(); - }; - - struct Conforming { - int value() const { return 1; } - - int value() { return 2; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view.value(), 2); - - // Shallow const: a const protocol_view still dispatches to the non-const - // overload. - const protocol_view& const_view = view; - EXPECT_EQ(const_view.value(), 2); -} - -TEST(ReflectionProtocolViewTest, ConstViewDispatchesToConstOverload) { - struct Interface { - int value() const; - int value(); - }; - - struct Conforming { - int value() const { return 1; } - - int value() { return 2; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view.value(), 1); - - const Conforming const_c; - protocol_view const_view(const_c); - EXPECT_EQ(const_view.value(), 1); -} - -TEST(ReflectionProtocolViewTest, ConstViewExposesOnlyConstOverloads) { - struct Interface { - int get() const; - void get(int value); - }; - - static_assert(!has_get_int>); - static_assert(has_get_int>); - static_assert(has_get_int>); - - static_assert(has_get>); - static_assert(has_get>); - static_assert(has_get>); -} - -TEST(ReflectionProtocolViewTest, IsTriviallyCopyableWithOverloads) { - struct Interface { - int compute(int x); - double compute(double x); - std::string compute(const std::string& x) const; - }; - - static_assert(std::is_trivially_copyable_v>); - static_assert(std::is_trivially_copyable_v>); - static_assert(sizeof(protocol_view) == 2 * sizeof(void*)); -} - -TEST(ReflectionProtocolViewTest, MemberThunksCannotBeDetachedForOverloads) { - struct Interface { - int compute(int x); - double compute(double x); - std::string compute(const std::string& x) const; - }; - - struct Conforming { - int compute(int x) { return x * 2; } - - double compute(double x) { return x * 3.0; } - - std::string compute(const std::string& x) const { return x + x; } - }; - - Conforming c; - protocol_view p(c); - - 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_default_constructible_v); - static_assert(!std::is_destructible_v); - static_assert(std::is_trivially_copyable_v); -} - -TEST(ReflectionProtocolViewTest, OverloadsThroughThunkReference) { - struct Interface { - int compute(int x); - double compute(double x); - std::string compute(const std::string& x) const; - }; - - struct Conforming { - int compute(int x) { return x * 2; } - - double compute(double x) { return x * 3.0; } - - std::string compute(const std::string& x) const { return x + x; } - }; - - Conforming c; - protocol_view p(c); - - // `protocol_view` is shallow const: the non-const overloads are callable - // through a const reference to the thunk. - const auto& compute = p.compute; - EXPECT_EQ(compute(5), 10); - EXPECT_EQ(compute(5.0), 15.0); - EXPECT_EQ(compute(std::string("A")), "AA"); -} - -// Call operator tests for protocol_view. - -TEST(ReflectionProtocolViewTest, CallOperator) { - struct Interface { - int operator()(int x) const; - }; - - struct Conforming { - int operator()(int x) const { return x * 2; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view(21), 42); -} - -TEST(ReflectionProtocolViewTest, CallOperatorOverloadSetCannotBeDetached) { - struct Interface { - int operator()(int x) const; - }; - - struct Conforming { - int operator()(int x) const { return x * 2; } - }; - - Conforming c; - protocol_view view(c); - - // Slicing `view` into its `operator_overload_set` base would detach the - // rest of `view`'s layout, so the base's own `static_cast` - // back to the full object inside `operator()` would be undefined - // behaviour. - static_assert(!operator_overload_set_can_be_sliced_from); -} - -TEST(ReflectionProtocolViewTest, CallOperatorFromLambda) { - struct Interface { - int operator()(int x) const; - }; - - // protocol_view can be constructed from a lambda directly, like - // function_ref. - auto lambda = [](int x) { return x * 2; }; - protocol_view view(lambda); - EXPECT_EQ(view(21), 42); -} - -TEST(ReflectionProtocolViewTest, OverloadedCallOperators) { - struct Interface { - int operator()(int x); - double operator()(double x); - std::string operator()(const std::string& x) const; - }; - - struct Conforming { - int operator()(int x) { return x * 2; } - - double operator()(double x) { return x * 3.0; } - - std::string operator()(const std::string& x) const { return x + x; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view(5), 10); - EXPECT_EQ(view(5.0), 15.0); - EXPECT_EQ(view(std::string("A")), "AA"); -} - -TEST(ReflectionProtocolViewTest, - ConstAndNonConstCallOperatorPairDispatchesToNonConst) { - struct Interface { - int operator()() const; - int operator()(); - }; - - struct Conforming { - int operator()() const { return 1; } - - int operator()() { return 2; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view(), 2); - - // Shallow const: a const protocol_view still dispatches to the non-const - // overload. - const protocol_view& const_view = view; - EXPECT_EQ(const_view(), 2); -} - -TEST(ReflectionProtocolViewTest, ConstViewDispatchesToConstCallOperator) { - struct Interface { - int operator()() const; - int operator()(); - }; - - struct Conforming { - int operator()() const { return 1; } - - int operator()() { return 2; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view(), 1); - - const Conforming const_c; - protocol_view const_view(const_c); - EXPECT_EQ(const_view(), 1); -} - -TEST(ReflectionProtocolViewTest, ConstViewExposesOnlyConstCallOperators) { - struct Interface { - int operator()() const; - void operator()(int value); - }; - - static_assert(!is_callable_with_int>); - static_assert(is_callable_with_int>); - static_assert(is_callable_with_int>); - - static_assert(is_callable>); - static_assert(is_callable>); - static_assert(is_callable>); -} - -TEST(ReflectionProtocolViewTest, CallOperatorAlongsideNamedMembers) { - struct Interface { - int operator()(int x) const; - int get() const; - }; - - struct Conforming { - int operator()(int x) const { return x * 2; } - - int get() const { return 7; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view(2), 4); - EXPECT_EQ(view.get(), 7); -} - -TEST(ReflectionProtocolViewTest, IsTriviallyCopyableWithCallOperator) { - struct Interface { - int operator()(int x); - double operator()(double x); - std::string operator()(const std::string& x) const; - }; - - static_assert(std::is_trivially_copyable_v>); - static_assert(std::is_trivially_copyable_v>); - static_assert(sizeof(protocol_view) == 2 * sizeof(void*)); -} - -TEST(ReflectionProtocolViewTest, StaticMemberFunction) { - struct Interface { - int value() const; - }; - - struct Conforming { - static int value() { return 42; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view.value(), 42); -} - -TEST(ReflectionProtocolViewTest, ConstViewCallsStaticMemberFunction) { - struct Interface { - int value() const; - }; - - struct Conforming { - static int value() { return 42; } - }; - - const Conforming c; - protocol_view view(c); - EXPECT_EQ(view.value(), 42); -} - -TEST(ReflectionProtocolViewTest, StaticMemberFunctionSatisfiesNonConstMember) { - struct Interface { - int next(); - }; - - struct Conforming { - static int next() { - static int counter = 0; - return ++counter; - } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view.next(), 1); - EXPECT_EQ(view.next(), 2); -} - -TEST(ReflectionProtocolViewTest, - StaticMemberFunctionAlongsideNonStaticMembers) { - struct Interface { - int value() const; - int add(int x); - }; - - struct Conforming { - int total = 0; - - static int value() { return 3; } - - int add(int x) { - total += x; - return total; - } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view.value(), 3); - EXPECT_EQ(view.add(4), 4); - EXPECT_EQ(view.add(5), 9); -} - -TEST(ReflectionProtocolViewTest, StaticCallOperator) { - struct Interface { - int operator()(int x) const; - }; - - struct Conforming { - static int operator()(int x) { return x * 3; } - }; - - Conforming c; - protocol_view view(c); - EXPECT_EQ(view(5), 15); -} - -// Member function forwarding tests for protocol. - -TEST(ReflectionProtocolTest, ConstMemberFunction) { - struct Interface { - int get_value() const; - }; - - struct Conforming { - int get_value() const { return 42; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.get_value(), 42); -} - -TEST(ReflectionProtocolTest, NonConstMemberFunctionNotInvocableFromConst) { - struct Interface { - void update(int value); - }; - - // `protocol` propagates const: a const protocol exposes only the const - // member functions of the interface. - static_assert( - !std::is_invocable_v< - decltype((std::declval&>().update)), int>); - static_assert(has_update>); - static_assert(!has_update>); -} - -TEST(ReflectionProtocolTest, SingleParameterMemberFunction) { - struct Interface { - void update(int value); - int get() const; - }; - - struct Conforming { - int last_value = 0; - - void update(int value) { last_value = value; } - - int get() const { return last_value; } - }; - - protocol p(Conforming{}); - p.update(42); - EXPECT_EQ(p.get(), 42); - static_assert(!noexcept(p.update(1))); -} - -TEST(ReflectionProtocolTest, NoexceptMemberFunction) { - struct Interface { - double compute(double input) noexcept; - }; - - struct Conforming { - double compute(double input) noexcept { return input * 2.0; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.compute(21.0), 42.0); - static_assert(noexcept(p.compute(1.0))); -} - -TEST(ReflectionProtocolTest, MultiParameterMemberFunction) { - struct Interface { - int add(int a, int b) const; - }; - - struct Conforming { - int add(int a, int b) const { return a + b; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.add(1, 2), 3); -} - -TEST(ReflectionProtocolTest, VoidMemberFunction) { - struct Interface { - void reset(); - bool was_reset() const; - }; - - struct Conforming { - bool reset_flag = false; - - void reset() { reset_flag = true; } - - bool was_reset() const { return reset_flag; } - }; - - protocol p(Conforming{}); - p.reset(); - EXPECT_TRUE(p.was_reset()); -} - -TEST(ReflectionProtocolTest, MultipleMemberFunctions) { - struct Interface { - double add(double x, double y) const noexcept; - double multiply(double x, double y) const noexcept; - }; - - struct Conforming { - double add(double x, double y) const noexcept { return x + y; } - - double multiply(double x, double y) const noexcept { return x * y; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.add(1.0, 2.0), 3.0); - EXPECT_EQ(p.multiply(3.0, 4.0), 12.0); -} - -TEST(ReflectionProtocolTest, MixedConstAndMutatingMemberFunctions) { - struct Interface { - int get() const; - void set(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.get(), 0); - p.set(7); - EXPECT_EQ(p.get(), 7); -} - -TEST(ReflectionProtocolTest, StaticMemberFunction) { - struct Interface { - int value() const; - }; - - struct Conforming { - static int value() { return 42; } - }; - - protocol p(std::in_place_type); - EXPECT_EQ(p.value(), 42); -} - -TEST(ReflectionProtocolTest, StaticMemberFunctionSatisfiesNonConstMember) { - struct Interface { - int next(); - }; - - struct Conforming { - static int next() { - static int counter = 0; - return ++counter; - } - }; - - protocol p(std::in_place_type); - EXPECT_EQ(p.next(), 1); - EXPECT_EQ(p.next(), 2); -} - -TEST(ReflectionProtocolTest, ForwardingAfterCopyConstruction) { - struct Interface { - int get() const; - void set(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value; } - }; - - protocol a(Conforming{}); - a.set(1); - // NOLINTBEGIN(performance-unnecessary-copy-initialization): the test - // exercises copy construction on purpose. - protocol b(a); - // NOLINTEND(performance-unnecessary-copy-initialization) - b.set(2); - - // protocol owns a copy of the underlying object, so copies are - // independent of one another. - EXPECT_EQ(a.get(), 1); - EXPECT_EQ(b.get(), 2); -} - -TEST(ReflectionProtocolTest, ForwardingAfterMoveConstruction) { - struct Interface { - int get() const; - void set(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value; } - }; - - protocol a(Conforming{}); - a.set(5); - protocol b(std::move(a)); - - EXPECT_EQ(b.get(), 5); - // NOLINTBEGIN(bugprone-use-after-move,hicpp-invalid-access-moved): the - // test exercises the moved-from state on purpose. - EXPECT_TRUE(valueless_after_move(a)); - // NOLINTEND(bugprone-use-after-move,hicpp-invalid-access-moved) -} - -TEST(ReflectionProtocolTest, ForwardingAfterCopyAssignment) { - struct Interface { - int get() const; - void set(int value); - }; - - // Counter and Doubler both conform to Interface but have different - // semantics for set(), so we can tell whether copy assignment updated - // the vtable pointer. - struct Counter { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value; } - }; - - struct Doubler { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value * 2; } - }; - - protocol a(Counter{}); - protocol b(Doubler{}); - - a = b; - a.set(10); - EXPECT_EQ(a.get(), 20); // a now has Doubler's semantics. -} - -TEST(ReflectionProtocolTest, ForwardingAfterMoveAssignment) { - struct Interface { - int get() const; - void set(int value); - }; - - struct Counter { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value; } - }; - - struct Doubler { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value * 2; } - }; - - protocol a(Counter{}); - protocol b(Doubler{}); - - a = std::move(b); - a.set(10); - EXPECT_EQ(a.get(), 20); // a now has Doubler's semantics. - // NOLINTBEGIN(bugprone-use-after-move,hicpp-invalid-access-moved): the - // test exercises the moved-from state on purpose. - EXPECT_TRUE(valueless_after_move(b)); - // NOLINTEND(bugprone-use-after-move,hicpp-invalid-access-moved) -} - -TEST(ReflectionProtocolTest, ForwardingAfterSwap) { - struct Interface { - int get() const; - void set(int value); - }; - - struct Counter { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value; } - }; - - struct Doubler { - int value = 0; - - int get() const { return value; } - - void set(int new_value) { value = new_value * 2; } - }; - - protocol a(Counter{}); - protocol b(Doubler{}); - - using std::swap; - swap(a, b); - - a.set(10); - EXPECT_EQ(a.get(), 20); // a now behaves like Doubler. - - b.set(10); - EXPECT_EQ(b.get(), 10); // b now behaves like Counter. -} - -TEST(ReflectionProtocolTest, ForwardingWithInPlaceConstruction) { - struct Interface { - int get() const; - }; - - struct Conforming { - int value; - - explicit Conforming(int initial_value) : value(initial_value) {} - - int get() const { return value; } - }; - - protocol p(std::in_place_type, 42); - EXPECT_EQ(p.get(), 42); -} - -TEST(ReflectionProtocolTest, ForwardingWithCustomAllocator) { - struct Interface { - int get() const; - }; - - struct Conforming { - int value; - - explicit Conforming(int initial_value) : value(initial_value) {} - - int get() const { return value; } - }; - - unsigned allocs = 0; - unsigned deallocs = 0; - xyz::TrackingAllocator alloc{&allocs, &deallocs}; - - protocol> p( - std::allocator_arg, alloc, Conforming(42)); - - EXPECT_EQ(p.get(), 42); - EXPECT_EQ(allocs, 1); -} - -TEST(ReflectionProtocolTest, ConstMemberFunctionOnConstProtocol) { - struct Interface { - int get_value() const; - }; - - struct Conforming { - int get_value() const { return 42; } - }; - - const protocol p(Conforming{}); - EXPECT_EQ(p.get_value(), 42); - - // A non-const member function is still not invocable on a const protocol; - // that assertion is already covered above by - // NonConstMemberFunctionNotInvocableFromConst. -} - -TEST(ReflectionProtocolTest, OverloadsByParameterType) { - struct Interface { - int compute(int x); - double compute(double x); - std::string compute(const std::string& x) const; - }; - - struct Conforming { - int compute(int x) { return x * 2; } - - double compute(double x) { return x * 3.0; } - - std::string compute(const std::string& x) const { return x + x; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.compute(5), 10); - EXPECT_EQ(p.compute(5.0), 15.0); - - const auto& const_p = p; - EXPECT_EQ(const_p.compute(std::string("A")), "AA"); -} - -TEST(ReflectionProtocolTest, OverloadsByArity) { - struct Interface { - int add(int a); - int add(int a, int b); - }; - - struct Conforming { - int add(int a) { return a; } - - int add(int a, int b) { return a + b; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.add(1), 1); - EXPECT_EQ(p.add(1, 2), 3); -} - -TEST(ReflectionProtocolTest, ConstAndNonConstOverloadPair) { - struct Interface { - int value() const; - int value(); - }; - - struct Conforming { - int value() const { return 1; } - - int value() { return 2; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.value(), 2); - - const protocol& const_p = p; - EXPECT_EQ(const_p.value(), 1); -} - -TEST(ReflectionProtocolTest, ConstProtocolExposesOnlyConstOverloads) { - struct Interface { - int get() const; - void get(int value); - }; - - static_assert(!has_get_int>); - static_assert(has_get_int>); - - static_assert(has_get>); - static_assert(has_get>); -} - -TEST(ReflectionProtocolTest, MemberThunksCannotBeDetachedForOverloads) { - struct Interface { - int compute(int x); - double compute(double x); - std::string compute(const std::string& x) const; - }; - - struct Conforming { - int compute(int x) { return x * 2; } - - double compute(double x) { return x * 3.0; } - - std::string compute(const std::string& x) const { return x + x; } - }; - - protocol p(Conforming{}); - - 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_default_constructible_v); - static_assert(!std::is_destructible_v); - static_assert(std::is_trivially_copyable_v); -} - -TEST(ReflectionProtocolTest, OverloadsThroughThunkReference) { - struct Interface { - int compute(int x); - double compute(double x); - std::string compute(const std::string& x) const; - }; - - struct Conforming { - int compute(int x) { return x * 2; } - - double compute(double x) { return x * 3.0; } - - std::string compute(const std::string& x) const { return x + x; } - }; - - protocol p(Conforming{}); - - // Const propagates through `protocol`: the non-const overloads need a - // non-const reference to the thunk. - auto& compute = p.compute; - EXPECT_EQ(compute(5), 10); - EXPECT_EQ(compute(5.0), 15.0); - const auto& const_compute = p.compute; - EXPECT_EQ(const_compute(std::string("A")), "AA"); -} - -TEST(ReflectionProtocolTest, NoexceptOverload) { - struct Interface { - int f(int x) noexcept; - int f(double x); - }; - - struct Conforming { - int f(int x) noexcept { return x * 2; } - - int f(double x) { return static_cast(x * 3.0); } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.f(5), 10); - EXPECT_EQ(p.f(5.0), 15); - static_assert(noexcept(p.f(1))); - static_assert(!noexcept(p.f(1.0))); -} - -// Tests that dispatching fails gracefully for a moved-from protocol, for -// named member functions. Operator dispatch has the same tests in -// protocol_operator_tests.cc. -#if (defined(_MSC_VER) && defined(_DEBUG)) || (!defined(NDEBUG)) - -TEST(ReflectionProtocolTest, MutableValuelessCall) { - struct Interface { - int foo(); - }; - - struct TypeA { - int foo() { return 5; } - }; - - protocol p(TypeA{}); - EXPECT_EQ(p.foo(), 5); - - auto _ = std::move(p); - // NOLINTBEGIN(bugprone-use-after-move,hicpp-invalid-access-moved): the - // test exercises the moved-from state on purpose. - EXPECT_TRUE(valueless_after_move(p)); - - EXPECT_DEATH(p.foo(), "cannot call member function of valueless protocol"); - // NOLINTEND(bugprone-use-after-move,hicpp-invalid-access-moved) -} - -TEST(ReflectionProtocolTest, ConstValuelessCall) { - struct Interface { - int foo() const; - }; - - struct TypeA { - int foo() const { return 5; } - }; - - protocol p(TypeA{}); - EXPECT_EQ(p.foo(), 5); - - auto _ = std::move(p); - // NOLINTBEGIN(bugprone-use-after-move,hicpp-invalid-access-moved): the - // test exercises the moved-from state on purpose. - EXPECT_TRUE(valueless_after_move(p)); - - EXPECT_DEATH(p.foo(), "cannot call member function of valueless protocol"); - // NOLINTEND(bugprone-use-after-move,hicpp-invalid-access-moved) -} - -#endif - -TEST(ReflectionProtocolTest, VolatileConformingType) { - struct Interface { - int foo(); - }; - - struct Conforming { - int foo() volatile { return 10; } - }; - - protocol p(Conforming{}); - EXPECT_EQ(p.foo(), 10); -} - -// --------------------------------------------------------------------------- -// Views of protocols: a `protocol_view` or `protocol_view` views -// the object a `protocol` owns, sharing its vtable; a -// `protocol_view` can also be constructed from a -// `protocol_view`. -// --------------------------------------------------------------------------- - -TEST(ReflectionProtocolViewTest, IsConstructibleFromProtocol) { - struct Interface { - int get() const; - }; - - static_assert( - std::is_constructible_v, protocol&>); - static_assert( - std::is_convertible_v&, protocol_view>); - static_assert(!std::is_constructible_v, - const protocol&>); - static_assert( - !std::is_constructible_v, protocol>); -} - -TEST(ReflectionProtocolViewTest, ConstViewIsConstructibleFromProtocol) { - struct Interface { - int get() const; - }; - - static_assert(std::is_constructible_v, - const protocol&>); - static_assert(std::is_constructible_v, - protocol&>); - static_assert(std::is_convertible_v&, - protocol_view>); - static_assert(std::is_convertible_v&, - protocol_view>); - static_assert(!std::is_constructible_v, - protocol>); - static_assert(!std::is_constructible_v, - const protocol>); -} - -TEST(ReflectionProtocolViewTest, ViewOfProtocolCallsOwnedObject) { - struct Interface { - int get() const; - void update(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void update(int new_value) { value = new_value; } - }; - - protocol p(Conforming{}); - protocol_view view(p); - - view.update(7); - EXPECT_EQ(p.get(), 7); - - p.update(11); - EXPECT_EQ(view.get(), 11); -} - -TEST(ReflectionProtocolViewTest, ConstViewOfProtocolExposesOnlyConstMembers) { - struct Interface { - int get() const; - void update(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void update(int new_value) { value = new_value; } - }; - - const protocol p(Conforming{3}); - protocol_view view(p); - - EXPECT_EQ(view.get(), 3); - static_assert(!has_update>); -} - -TEST(ReflectionProtocolViewTest, ViewOfProtocolWithOverloadsAndCallOperator) { - struct Interface { - int get() const; - int get(int value) const; - int operator()(int value) const; - }; - - struct Conforming { - int get() const { return 1; } - - int get(int value) const { return value + 1; } - - int operator()(int value) const { return value * 2; } - }; - - protocol p(Conforming{}); - protocol_view view(p); - - EXPECT_EQ(view.get(), 1); - EXPECT_EQ(view.get(4), 5); - EXPECT_EQ(view(21), 42); -} - -TEST(ReflectionProtocolViewTest, ViewOfProtocolRemainsValidAfterMove) { - struct Interface { - int get() const; - }; - - struct Conforming { - int get() const { return 5; } - }; - - protocol p(Conforming{}); - protocol_view view(p); - - protocol moved_to(std::move(p)); - - EXPECT_EQ(view.get(), 5); -} - -TEST(ReflectionProtocolViewTest, ViewOfProtocolWithCustomAllocator) { - struct Interface { - int get() const; - }; - - struct Conforming { - int get() const { return 9; } - }; - - unsigned allocs = 0; - unsigned deallocs = 0; - xyz::TrackingAllocator alloc{&allocs, &deallocs}; - - protocol> p(std::allocator_arg, - alloc, Conforming{}); - protocol_view view(p); - - EXPECT_EQ(view.get(), 9); -} - -TEST(ReflectionProtocolViewTest, ConstViewIsConstructibleFromView) { - struct Interface { - int get() const; - void update(int value); - }; - - static_assert(std::is_convertible_v, - protocol_view>); - static_assert(!std::is_constructible_v, - protocol_view>); -} - -TEST(ReflectionProtocolViewTest, ConstViewOfViewCallsViewedObject) { - struct Interface { - int get() const; - void update(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void update(int new_value) { value = new_value; } - }; - - protocol p(Conforming{}); - protocol_view view(p); - protocol_view const_view(view); - - view.update(7); - EXPECT_EQ(const_view.get(), 7); - static_assert(!has_update>); -} - -TEST(ReflectionProtocolViewTest, ViewOfProtocolPassedByValue) { - struct Interface { - int get() const; - }; - - struct Conforming { - int value; - - int get() const { return value; } - }; - - auto read = [](protocol_view view) { return view.get(); }; - - protocol p(Conforming{5}); - EXPECT_EQ(read(p), 5); -} - -TEST(ReflectionProtocolViewTest, MutableViewOfProtocolPassedByValue) { - struct Interface { - int get() const; - void update(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void update(int new_value) { value = new_value; } - }; - - auto write = [](protocol_view view) { view.update(9); }; - - protocol p(Conforming{}); - write(p); - EXPECT_EQ(p.get(), 9); -} - -TEST(ReflectionProtocolViewTest, ViewOfConformingObjectPassedByValue) { - struct Interface { - int get() const; - void update(int value); - }; - - struct Conforming { - int value = 0; - - int get() const { return value; } - - void update(int new_value) { value = new_value; } - }; - - auto read = [](protocol_view view) { return view.get(); }; - auto write = [](protocol_view view) { view.update(4); }; - - Conforming c{}; - write(c); - EXPECT_EQ(read(c), 4); -} - TEST(ReflectionProtocolTest, ProtocolCast) { struct Interface {}; diff --git a/protocol_view_tests.cc b/protocol_view_tests.cc new file mode 100644 index 0000000..9d24d1e --- /dev/null +++ b/protocol_view_tests.cc @@ -0,0 +1,866 @@ +// Tests for member function calls through protocol_view, and for views of +// protocols. + +#include + +#include +#include +#include +#include +#include + +#include "protocol.hh" +#include "test_helpers.h" +#include "tracking_allocator.h" + +using xyz::reflection::protocol; +using xyz::reflection::protocol_view; + +namespace { + +// Concepts for negative member function tests: a requires-expression naming a +// member that does not exist is only a substitution failure in a template. +template +concept has_update = requires(P& p) { p.update(0); }; + +template +concept has_get_value = requires(P& p) { p.get_value(); }; + +template +concept has_get_int = requires(P& p) { p.get(0); }; + +template +concept has_get = requires(P& p) { p.get(); }; + +// Member function signature tests for protocol_view. + +TEST(ReflectionProtocolViewTest, ConstMemberFunction) { + struct Interface { + int get_value() const; + }; + + struct Conforming { + int get_value() const { return 42; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.get_value(), 42); +} + +TEST(ReflectionProtocolViewTest, NonConstMemberFunctionInvocableFromConstView) { + struct Interface { + void update(int value); + }; + + // `protocol_view` has shallow const: a const view still exposes the + // non-const member functions of the interface. + static_assert(requires(const protocol_view& p) { + { p.update(0) } -> std::same_as; + }); + static_assert(has_update>); +} + +TEST(ReflectionProtocolViewTest, ConstViewExposesOnlyConstMemberFunctions) { + struct Interface { + int get_value() const; + void update(int value); + }; + + static_assert(has_get_value>); + static_assert(has_get_value>); + static_assert(!has_update>); + static_assert(!has_update>); + + static_assert(requires(const protocol_view& p) { + { p.get_value() } -> std::same_as; + }); +} + +TEST(ReflectionProtocolViewTest, NonConstMemberFunctionCalledThroughConstView) { + struct Interface { + void update(int value); + }; + + struct Conforming { + int last_value = 0; + + void update(int value) { last_value = value; } + }; + + Conforming c; + const protocol_view p(c); + p.update(42); + EXPECT_EQ(c.last_value, 42); +} + +TEST(ReflectionProtocolViewTest, ConstViewCallsConstMemberFunction) { + struct Interface { + int get_value() const; + void update(int value); + }; + + struct Conforming { + int value = 7; + + int get_value() const { return value; } + + void update(int new_value) { value = new_value; } + }; + + const Conforming const_object; + protocol_view view_of_const(const_object); + EXPECT_EQ(view_of_const.get_value(), 7); + + Conforming mutable_object; + mutable_object.update(9); + const protocol_view view_of_mutable(mutable_object); + EXPECT_EQ(view_of_mutable.get_value(), 9); +} + +TEST(ReflectionProtocolViewTest, ConstViewOfInterfaceWithNoConstMembers) { + struct Interface { + void update(int value); + }; + + // A const view of an interface with no const member functions is + // well-formed but exposes nothing. + static_assert(!has_update>); + static_assert(std::is_copy_constructible_v>); +} + +TEST(ReflectionProtocolViewTest, SingleParameterMemberFunction) { + struct Interface { + void update(int value); + }; + + struct Conforming { + int last_value = 0; + + void update(int value) { last_value = value; } + }; + + Conforming c; + protocol_view p(c); + p.update(42); + EXPECT_EQ(c.last_value, 42); + static_assert(!noexcept(c.update(1.0))); +} + +TEST(ReflectionProtocolViewTest, NoexceptMemberFunction) { + struct Interface { + double compute(double input) noexcept; + }; + + struct Conforming { + double compute(double input) noexcept { return input * 2.0; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.compute(21.0), 42.0); + static_assert(noexcept(p.compute(1.0))); +} + +TEST(ReflectionProtocolViewTest, MultiParameterMemberFunction) { + struct Interface { + int add(int a, int b) const; + }; + + struct Conforming { + int add(int a, int b) const { return a + b; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.add(1, 2), 3); +} + +TEST(ReflectionProtocolViewTest, VoidMemberFunction) { + struct Interface { + void reset(); + }; + + struct Conforming { + bool was_reset = false; + + void reset() { was_reset = true; } + }; + + Conforming c; + protocol_view p(c); + p.reset(); + EXPECT_TRUE(c.was_reset); +} + +TEST(ReflectionProtocolViewTest, MultipleMemberFunctions) { + struct Interface { + double add(double x, double y) const noexcept; + double multiply(double x, double y) const noexcept; + }; + + struct Conforming { + double add(double x, double y) const noexcept { return x + y; } + + double multiply(double x, double y) const noexcept { return x * y; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.add(1.0, 2.0), 3.0); + EXPECT_EQ(p.multiply(3.0, 4.0), 12.0); +} + +TEST(ReflectionProtocolViewTest, MixedConstAndMutatingMemberFunctions) { + struct Interface { + int get() const; + void set(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.get(), 0); + p.set(7); + EXPECT_EQ(p.get(), 7); + EXPECT_EQ(c.value, 7); +} + +TEST(ReflectionProtocolViewTest, OverloadsByParameterType) { + struct Interface { + int compute(int x); + double compute(double x); + std::string compute(const std::string& x) const; + }; + + struct Conforming { + int compute(int x) { return x * 2; } + + double compute(double x) { return x * 3.0; } + + std::string compute(const std::string& x) const { return x + x; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.compute(5), 10); + EXPECT_EQ(p.compute(5.0), 15.0); + EXPECT_EQ(p.compute(std::string("A")), "AA"); +} + +TEST(ReflectionProtocolViewTest, + ConstAndNonConstOverloadPairDispatchesToNonConst) { + struct Interface { + int value() const; + int value(); + }; + + struct Conforming { + int value() const { return 1; } + + int value() { return 2; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view.value(), 2); + + // Shallow const: a const protocol_view still dispatches to the non-const + // overload. + const protocol_view& const_view = view; + EXPECT_EQ(const_view.value(), 2); +} + +TEST(ReflectionProtocolViewTest, ConstViewDispatchesToConstOverload) { + struct Interface { + int value() const; + int value(); + }; + + struct Conforming { + int value() const { return 1; } + + int value() { return 2; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view.value(), 1); + + const Conforming const_c; + protocol_view const_view(const_c); + EXPECT_EQ(const_view.value(), 1); +} + +TEST(ReflectionProtocolViewTest, ConstViewExposesOnlyConstOverloads) { + struct Interface { + int get() const; + void get(int value); + }; + + static_assert(!has_get_int>); + static_assert(has_get_int>); + static_assert(has_get_int>); + + static_assert(has_get>); + static_assert(has_get>); + static_assert(has_get>); +} + +TEST(ReflectionProtocolViewTest, IsTriviallyCopyableWithOverloads) { + struct Interface { + int compute(int x); + double compute(double x); + std::string compute(const std::string& x) const; + }; + + static_assert(std::is_trivially_copyable_v>); + static_assert(std::is_trivially_copyable_v>); + static_assert(sizeof(protocol_view) == 2 * sizeof(void*)); +} + +TEST(ReflectionProtocolViewTest, MemberThunksCannotBeDetachedForOverloads) { + struct Interface { + int compute(int x); + double compute(double x); + std::string compute(const std::string& x) const; + }; + + struct Conforming { + int compute(int x) { return x * 2; } + + double compute(double x) { return x * 3.0; } + + std::string compute(const std::string& x) const { return x + x; } + }; + + Conforming c; + protocol_view p(c); + + 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_default_constructible_v); + static_assert(!std::is_destructible_v); + static_assert(std::is_trivially_copyable_v); +} + +TEST(ReflectionProtocolViewTest, OverloadsThroughThunkReference) { + struct Interface { + int compute(int x); + double compute(double x); + std::string compute(const std::string& x) const; + }; + + struct Conforming { + int compute(int x) { return x * 2; } + + double compute(double x) { return x * 3.0; } + + std::string compute(const std::string& x) const { return x + x; } + }; + + Conforming c; + protocol_view p(c); + + // `protocol_view` is shallow const: the non-const overloads are callable + // through a const reference to the thunk. + const auto& compute = p.compute; + EXPECT_EQ(compute(5), 10); + EXPECT_EQ(compute(5.0), 15.0); + EXPECT_EQ(compute(std::string("A")), "AA"); +} + +// Call operator tests for protocol_view. + +TEST(ReflectionProtocolViewTest, CallOperator) { + struct Interface { + int operator()(int x) const; + }; + + struct Conforming { + int operator()(int x) const { return x * 2; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view(21), 42); +} + +TEST(ReflectionProtocolViewTest, CallOperatorOverloadSetCannotBeDetached) { + struct Interface { + int operator()(int x) const; + }; + + struct Conforming { + int operator()(int x) const { return x * 2; } + }; + + Conforming c; + protocol_view view(c); + + // Slicing `view` into its `operator_overload_set` base would detach the + // rest of `view`'s layout, so the base's own `static_cast` + // back to the full object inside `operator()` would be undefined + // behaviour. + static_assert(!operator_overload_set_can_be_sliced_from); +} + +TEST(ReflectionProtocolViewTest, CallOperatorFromLambda) { + struct Interface { + int operator()(int x) const; + }; + + // protocol_view can be constructed from a lambda directly, like + // function_ref. + auto lambda = [](int x) { return x * 2; }; + protocol_view view(lambda); + EXPECT_EQ(view(21), 42); +} + +TEST(ReflectionProtocolViewTest, OverloadedCallOperators) { + struct Interface { + int operator()(int x); + double operator()(double x); + std::string operator()(const std::string& x) const; + }; + + struct Conforming { + int operator()(int x) { return x * 2; } + + double operator()(double x) { return x * 3.0; } + + std::string operator()(const std::string& x) const { return x + x; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view(5), 10); + EXPECT_EQ(view(5.0), 15.0); + EXPECT_EQ(view(std::string("A")), "AA"); +} + +TEST(ReflectionProtocolViewTest, + ConstAndNonConstCallOperatorPairDispatchesToNonConst) { + struct Interface { + int operator()() const; + int operator()(); + }; + + struct Conforming { + int operator()() const { return 1; } + + int operator()() { return 2; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view(), 2); + + // Shallow const: a const protocol_view still dispatches to the non-const + // overload. + const protocol_view& const_view = view; + EXPECT_EQ(const_view(), 2); +} + +TEST(ReflectionProtocolViewTest, ConstViewDispatchesToConstCallOperator) { + struct Interface { + int operator()() const; + int operator()(); + }; + + struct Conforming { + int operator()() const { return 1; } + + int operator()() { return 2; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view(), 1); + + const Conforming const_c; + protocol_view const_view(const_c); + EXPECT_EQ(const_view(), 1); +} + +TEST(ReflectionProtocolViewTest, ConstViewExposesOnlyConstCallOperators) { + struct Interface { + int operator()() const; + void operator()(int value); + }; + + static_assert(!is_callable_with_int>); + static_assert(is_callable_with_int>); + static_assert(is_callable_with_int>); + + static_assert(is_callable>); + static_assert(is_callable>); + static_assert(is_callable>); +} + +TEST(ReflectionProtocolViewTest, CallOperatorAlongsideNamedMembers) { + struct Interface { + int operator()(int x) const; + int get() const; + }; + + struct Conforming { + int operator()(int x) const { return x * 2; } + + int get() const { return 7; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view(2), 4); + EXPECT_EQ(view.get(), 7); +} + +TEST(ReflectionProtocolViewTest, IsTriviallyCopyableWithCallOperator) { + struct Interface { + int operator()(int x); + double operator()(double x); + std::string operator()(const std::string& x) const; + }; + + static_assert(std::is_trivially_copyable_v>); + static_assert(std::is_trivially_copyable_v>); + static_assert(sizeof(protocol_view) == 2 * sizeof(void*)); +} + +TEST(ReflectionProtocolViewTest, StaticMemberFunction) { + struct Interface { + int value() const; + }; + + struct Conforming { + static int value() { return 42; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view.value(), 42); +} + +TEST(ReflectionProtocolViewTest, ConstViewCallsStaticMemberFunction) { + struct Interface { + int value() const; + }; + + struct Conforming { + static int value() { return 42; } + }; + + const Conforming c; + protocol_view view(c); + EXPECT_EQ(view.value(), 42); +} + +TEST(ReflectionProtocolViewTest, StaticMemberFunctionSatisfiesNonConstMember) { + struct Interface { + int next(); + }; + + struct Conforming { + static int next() { + static int counter = 0; + return ++counter; + } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view.next(), 1); + EXPECT_EQ(view.next(), 2); +} + +TEST(ReflectionProtocolViewTest, + StaticMemberFunctionAlongsideNonStaticMembers) { + struct Interface { + int value() const; + int add(int x); + }; + + struct Conforming { + int total = 0; + + static int value() { return 3; } + + int add(int x) { + total += x; + return total; + } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view.value(), 3); + EXPECT_EQ(view.add(4), 4); + EXPECT_EQ(view.add(5), 9); +} + +TEST(ReflectionProtocolViewTest, StaticCallOperator) { + struct Interface { + int operator()(int x) const; + }; + + struct Conforming { + static int operator()(int x) { return x * 3; } + }; + + Conforming c; + protocol_view view(c); + EXPECT_EQ(view(5), 15); +} + +// --------------------------------------------------------------------------- +// Views of protocols: a `protocol_view` or `protocol_view` views +// the object a `protocol` owns, sharing its vtable; a +// `protocol_view` can also be constructed from a +// `protocol_view`. +// --------------------------------------------------------------------------- + +TEST(ReflectionProtocolViewTest, IsConstructibleFromProtocol) { + struct Interface { + int get() const; + }; + + static_assert( + std::is_constructible_v, protocol&>); + static_assert( + std::is_convertible_v&, protocol_view>); + static_assert(!std::is_constructible_v, + const protocol&>); + static_assert( + !std::is_constructible_v, protocol>); +} + +TEST(ReflectionProtocolViewTest, ConstViewIsConstructibleFromProtocol) { + struct Interface { + int get() const; + }; + + static_assert(std::is_constructible_v, + const protocol&>); + static_assert(std::is_constructible_v, + protocol&>); + static_assert(std::is_convertible_v&, + protocol_view>); + static_assert(std::is_convertible_v&, + protocol_view>); + static_assert(!std::is_constructible_v, + protocol>); + static_assert(!std::is_constructible_v, + const protocol>); +} + +TEST(ReflectionProtocolViewTest, ViewOfProtocolCallsOwnedObject) { + struct Interface { + int get() const; + void update(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void update(int new_value) { value = new_value; } + }; + + protocol p(Conforming{}); + protocol_view view(p); + + view.update(7); + EXPECT_EQ(p.get(), 7); + + p.update(11); + EXPECT_EQ(view.get(), 11); +} + +TEST(ReflectionProtocolViewTest, ConstViewOfProtocolExposesOnlyConstMembers) { + struct Interface { + int get() const; + void update(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void update(int new_value) { value = new_value; } + }; + + const protocol p(Conforming{3}); + protocol_view view(p); + + EXPECT_EQ(view.get(), 3); + static_assert(!has_update>); +} + +TEST(ReflectionProtocolViewTest, ViewOfProtocolWithOverloadsAndCallOperator) { + struct Interface { + int get() const; + int get(int value) const; + int operator()(int value) const; + }; + + struct Conforming { + int get() const { return 1; } + + int get(int value) const { return value + 1; } + + int operator()(int value) const { return value * 2; } + }; + + protocol p(Conforming{}); + protocol_view view(p); + + EXPECT_EQ(view.get(), 1); + EXPECT_EQ(view.get(4), 5); + EXPECT_EQ(view(21), 42); +} + +TEST(ReflectionProtocolViewTest, ViewOfProtocolRemainsValidAfterMove) { + struct Interface { + int get() const; + }; + + struct Conforming { + int get() const { return 5; } + }; + + protocol p(Conforming{}); + protocol_view view(p); + + protocol moved_to(std::move(p)); + + EXPECT_EQ(view.get(), 5); +} + +TEST(ReflectionProtocolViewTest, ViewOfProtocolWithCustomAllocator) { + struct Interface { + int get() const; + }; + + struct Conforming { + int get() const { return 9; } + }; + + unsigned allocs = 0; + unsigned deallocs = 0; + xyz::TrackingAllocator alloc{&allocs, &deallocs}; + + protocol> p(std::allocator_arg, + alloc, Conforming{}); + protocol_view view(p); + + EXPECT_EQ(view.get(), 9); +} + +TEST(ReflectionProtocolViewTest, ConstViewIsConstructibleFromView) { + struct Interface { + int get() const; + void update(int value); + }; + + static_assert(std::is_convertible_v, + protocol_view>); + static_assert(!std::is_constructible_v, + protocol_view>); +} + +TEST(ReflectionProtocolViewTest, ConstViewOfViewCallsViewedObject) { + struct Interface { + int get() const; + void update(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void update(int new_value) { value = new_value; } + }; + + protocol p(Conforming{}); + protocol_view view(p); + protocol_view const_view(view); + + view.update(7); + EXPECT_EQ(const_view.get(), 7); + static_assert(!has_update>); +} + +TEST(ReflectionProtocolViewTest, ViewOfProtocolPassedByValue) { + struct Interface { + int get() const; + }; + + struct Conforming { + int value; + + int get() const { return value; } + }; + + auto read = [](protocol_view view) { return view.get(); }; + + protocol p(Conforming{5}); + EXPECT_EQ(read(p), 5); +} + +TEST(ReflectionProtocolViewTest, MutableViewOfProtocolPassedByValue) { + struct Interface { + int get() const; + void update(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void update(int new_value) { value = new_value; } + }; + + auto write = [](protocol_view view) { view.update(9); }; + + protocol p(Conforming{}); + write(p); + EXPECT_EQ(p.get(), 9); +} + +TEST(ReflectionProtocolViewTest, ViewOfConformingObjectPassedByValue) { + struct Interface { + int get() const; + void update(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void update(int new_value) { value = new_value; } + }; + + auto read = [](protocol_view view) { return view.get(); }; + auto write = [](protocol_view view) { view.update(4); }; + + Conforming c{}; + write(c); + EXPECT_EQ(read(c), 4); +} + +} // namespace From ea4c7dc9d6e56da5ea5ab5304a965623adf2a317 Mon Sep 17 00:00:00 2001 From: Philip Craig <689193+philipcraig@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:54:51 +0000 Subject: [PATCH 2/3] List the new test files in the consteval coverage translation units --- scripts/consteval_coverage.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/consteval_coverage.py b/scripts/consteval_coverage.py index a58fe0d..96429bb 100644 --- a/scripts/consteval_coverage.py +++ b/scripts/consteval_coverage.py @@ -81,9 +81,12 @@ # only arms the probe points the ones before it left uncovered. DEFAULT_TRANSLATION_UNITS = [ "name_mangling_tests.cc", + "conformance_tests.cc", "allocator_tests.cc", - "forwarding_test.cc", "protocol_test.cc", + "protocol_member_function_tests.cc", + "protocol_view_tests.cc", + "forwarding_test.cc", ] ProbeKey = tuple[int, int] # (header index, line number) From 3febdd16027b0131ebb38d0fa01667a6e86a8aa9 Mon Sep 17 00:00:00 2001 From: Philip Craig <689193+philipcraig@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:41:39 +0000 Subject: [PATCH 3/3] Leave the member function tests out of the consteval coverage probe --- scripts/consteval_coverage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/consteval_coverage.py b/scripts/consteval_coverage.py index 96429bb..ee2b1d6 100644 --- a/scripts/consteval_coverage.py +++ b/scripts/consteval_coverage.py @@ -79,12 +79,13 @@ # Translation units that instantiate protocols and so drive the consteval # machinery in the instrumented headers, cheapest to compile first: each # only arms the probe points the ones before it left uncovered. +# protocol_member_function_tests.cc is left out: it reaches no probe point +# the others miss. DEFAULT_TRANSLATION_UNITS = [ "name_mangling_tests.cc", "conformance_tests.cc", "allocator_tests.cc", "protocol_test.cc", - "protocol_member_function_tests.cc", "protocol_view_tests.cc", "forwarding_test.cc", ]