diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b9cbdc..8713751 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,7 +212,9 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) 26 FILES protocol_reflection_detail/naming.hxx - protocol_reflection_detail/naming_test.cc) + protocol_reflection_detail/naming_test.cc + protocol_reflection_detail/members.hxx + protocol_reflection_detail/members_test.cc) target_compile_options(protocol_reflection_detail_test PRIVATE -freflection) target_include_directories(protocol_reflection_detail_test diff --git a/protocol_reflection_detail/members.hxx b/protocol_reflection_detail/members.hxx new file mode 100644 index 0000000..79b2990 --- /dev/null +++ b/protocol_reflection_detail/members.hxx @@ -0,0 +1,52 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +// Enumerating an interface type's dispatchable member functions. +#ifndef XYZ_PROTOCOL_REFLECTION_DETAIL_MEMBERS_HXX_ +#define XYZ_PROTOCOL_REFLECTION_DETAIL_MEMBERS_HXX_ + +#include +#include + +namespace xyz::reflection_detail { + +consteval bool is_interface_member_function(std::meta::info member) { + return std::meta::is_function(member) && std::meta::is_public(member) && + !std::meta::is_static_member(member) && + !std::meta::is_special_member_function(member) && + !std::meta::is_function_template(member); +} + +// Every member of `type` for which is_interface_member_function holds, in +// declaration order. +consteval std::vector interface_member_functions( + std::meta::info type) { + std::vector result; + for (std::meta::info member : + std::meta::members_of(type, std::meta::access_context::current())) { + if (is_interface_member_function(member)) { + result.push_back(member); + } + } + return result; +} + +} // namespace xyz::reflection_detail + +#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_MEMBERS_HXX_ diff --git a/protocol_reflection_detail/members_test.cc b/protocol_reflection_detail/members_test.cc new file mode 100644 index 0000000..aed6c17 --- /dev/null +++ b/protocol_reflection_detail/members_test.cc @@ -0,0 +1,147 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#include "protocol_reflection_detail/members.hxx" + +#include + +#include +#include + +namespace { + +using xyz::reflection_detail::interface_member_functions; +using xyz::reflection_detail::is_interface_member_function; + +struct Fixture { + Fixture() = default; + + int value() const { return 0; } + + void set_value(int) {} + + Fixture operator+(const Fixture& other) const { return other; } + + static void static_helper() {} + + template + void templated(T) {} + + private: + void private_helper() {} + + public: + int data_member = 0; +}; + +TEST(IsInterfaceMemberFunction, AcceptsOrdinaryPublicMembers) { + static_assert(is_interface_member_function(^^Fixture::value)); + static_assert(is_interface_member_function(^^Fixture::set_value)); +} + +TEST(IsInterfaceMemberFunction, AcceptsOrdinaryOperatorOverloads) { + // operator+ is an ordinary (non-special) operator: an interface member + // function like any other, just found by kind rather than by name. + bool found_ordinary_operator = false; + template for (constexpr std::meta::info member : + std::define_static_array(std::meta::members_of( + ^^Fixture, std::meta::access_context::current()))) { + if (std::meta::is_operator_function(member) && + !std::meta::is_special_member_function(member)) { + EXPECT_TRUE(is_interface_member_function(member)); + found_ordinary_operator = true; + } + } + EXPECT_TRUE(found_ordinary_operator); +} + +TEST(IsInterfaceMemberFunction, RejectsSpecialOperatorsLikeAssignment) { + // Fixture's implicit copy/move assignment operators are both "operator + // functions" and "special member functions" at once; the special-member + // exclusion applies regardless of the operator check, unlike operator+. + bool found_special_operator = false; + template for (constexpr std::meta::info member : + std::define_static_array(std::meta::members_of( + ^^Fixture, std::meta::access_context::current()))) { + if (std::meta::is_operator_function(member) && + std::meta::is_special_member_function(member)) { + EXPECT_FALSE(is_interface_member_function(member)); + found_special_operator = true; + } + } + EXPECT_TRUE(found_special_operator); +} + +TEST(IsInterfaceMemberFunction, RejectsSpecialMemberFunctions) { + // A constructor's name denotes an overload set, so it can't be reflected + // directly (^^Fixture::Fixture); find it by walking members and matching + // on is_constructor instead. + bool found_constructor = false; + template for (constexpr std::meta::info member : + std::define_static_array(std::meta::members_of( + ^^Fixture, std::meta::access_context::current()))) { + if (std::meta::is_constructor(member)) { + EXPECT_FALSE(is_interface_member_function(member)); + found_constructor = true; + } + } + EXPECT_TRUE(found_constructor); +} + +TEST(IsInterfaceMemberFunction, RejectsStaticMembers) { + static_assert(!is_interface_member_function(^^Fixture::static_helper)); +} + +TEST(IsInterfaceMemberFunction, RejectsMemberFunctionTemplates) { + static_assert(!is_interface_member_function(^^Fixture::templated)); +} + +TEST(IsInterfaceMemberFunction, RejectsDataMembers) { + static_assert(!is_interface_member_function(^^Fixture::data_member)); +} + +TEST(InterfaceMemberFunctions, FindsExactlyTheDispatchableMembers) { + // value, set_value, and operator+: not the constructor, static_helper, + // templated, private_helper (inaccessible from this scope), or + // data_member. + static constexpr auto members = + std::define_static_array(interface_member_functions(^^Fixture)); + static_assert(members.size() == 3); + + bool has_value = false; + bool has_set_value = false; + bool has_operator = false; + template for (constexpr std::meta::info member : members) { + // if constexpr, not if: identifier_of is consteval, so a plain if's + // untaken branch would still have to evaluate it for operator+ + // (which has no identifier) even though that branch never runs. + if constexpr (!std::meta::has_identifier(member)) { + has_operator = true; + } else { + std::string_view name = std::meta::identifier_of(member); + if (name == "value") has_value = true; + if (name == "set_value") has_set_value = true; + } + } + EXPECT_TRUE(has_value); + EXPECT_TRUE(has_set_value); + EXPECT_TRUE(has_operator); +} + +} // namespace