diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e5ea96..25167f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -252,6 +252,7 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) protocol.h protocol_reflection.hxx interface_A.h + interface_C.h protocol_reflection_smoke_test.cc) target_compile_options(protocol_reflection_smoke_test PRIVATE -freflection) diff --git a/protocol_reflection.hxx b/protocol_reflection.hxx index 7883538..5ae319f 100644 --- a/protocol_reflection.hxx +++ b/protocol_reflection.hxx @@ -47,54 +47,100 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace xyz { namespace reflection_detail { -// The data member of `struct_type` named `name`, as produced by -// naming.hxx's escaping: for splicing a value into a reflection-built -// vtable's entry, once the entry's own name (not just its position) is -// the only thing identifying it. -consteval std::meta::info find_data_member(std::meta::info struct_type, - std::string_view name) { - for (std::meta::info member : std::meta::nonstatic_data_members_of( - struct_type, std::meta::access_context::current())) { - if (std::meta::identifier_of(member) == name) return member; +// Every member of `interface` sharing `member`'s own name, in declaration +// order: the sibling overloads a forwarder's single named data member has +// to merge into one callable (e.g. interface C's overloaded `compute`s). +consteval std::vector members_named_like( + std::meta::info interface, std::meta::info member) { + std::string_view name = std::meta::identifier_of(member); + std::vector result; + for (std::meta::info candidate : interface_member_functions(interface)) { + if (std::meta::has_identifier(candidate) && + std::meta::identifier_of(candidate) == name) { + result.push_back(candidate); + } } - throw std::meta::exception("data member not found", ^^void); + return result; } -// A per-interface-member forwarder, giving protocol -// ordinary call syntax for that member with no splicing at any call site. -// operator() is a member function template because its return type and -// arguments vary per Member, which a single fixed declaration can't -// express; unlike conformance.hxx's candidate forwarders, there is exactly -// one operator() per Wrapper here, so a generic template creates no -// overload-resolution ambiguity to worry about. -template -struct protocol_member_wrapper { - template - auto operator()(Args&&... args) const - noexcept(std::meta::is_noexcept(Member)); +// A concrete, non-generic forwarder for one interface overload: its +// operator() has that overload's own parameter types rather than a +// forwarding template, so overload resolution across several such +// forwarders (protocol_member_wrapper_combinator) ranks them the way it +// would rank the interface's own overloads. Same reason +// conformance.hxx's single_candidate_forwarder is concrete, one level up: +// there it merges an implementation's candidates for one interface member; +// here it merges an interface's own overloads sharing one name. +template +struct protocol_single_overload_wrapper { + R operator()(Ps... ps) const noexcept(std::meta::is_noexcept(Member)); +}; + +// Merges N single-overload forwarders into one callable type via +// `using Bases::operator()...`, exactly conformance.hxx's +// candidate_overload_set technique, applied to an interface's own +// overloads instead of an implementation's candidates. +template +struct protocol_member_wrapper_combinator : Bases... { + using Bases::operator()...; }; +// The merged forwarder type for the whole name-group `member` belongs to +// (a single overload if `member` isn't overloaded). A plain function, not +// a function template keyed only on Member: every member in the group +// produces the same result, so protocol_single_overload_wrapper's +// operator() can recompute it from just its own Member, without +// protocol_bases_type threading the whole group through separately. +consteval std::meta::info protocol_member_wrapper_type( + std::meta::info interface, std::meta::info allocator, + std::meta::info member) { + std::vector base_types; + for (std::meta::info sibling : members_named_like(interface, member)) { + std::vector args{ + interface, allocator, std::meta::reflect_constant(sibling), + std::meta::dealias(std::meta::return_type_of(sibling))}; + for (std::meta::info parameter_type : parameter_types_of(sibling)) { + args.push_back(parameter_type); + } + base_types.push_back( + std::meta::substitute(^^protocol_single_overload_wrapper, args)); + } + return std::meta::substitute(^^protocol_member_wrapper_combinator, + base_types); +} + +template +using protocol_member_wrapper = + typename[:protocol_member_wrapper_type(^^Interface, ^^Allocator, Member):]; + template consteval std::meta::info protocol_bases_type() { std::vector bases; for (std::meta::info member : interface_member_functions(^^Interface)) { - std::meta::info wrapper_type = std::meta::substitute( - ^^protocol_member_wrapper, - { - ^^Interface, ^^Allocator, std::meta::reflect_constant(member)}); + // Every member in a name-group produces the same + // members_named_like(...)[0]; only process the group once, at its + // first member, so an overloaded name doesn't get a forwarder_base + // (and hence a same-named data member) built once per overload. + std::meta::info representative = members_named_like(^^Interface, member)[0]; + if (representative != member) { + continue; + } + std::meta::info wrapper_type = + protocol_member_wrapper_type(^^Interface, ^^Allocator, member); bases.push_back(std::meta::substitute( ^^forwarder_base, { - wrapper_type, std::meta::reflect_constant(member)})); + wrapper_type, std::meta::reflect_constant(representative)})); } return forwarders_type(bases); } // protocol's own base list: one forwarder_base per -// interface member, combined via forwarders.hxx's combinator. protocol -// inherits this directly (not through an intermediate type) so that each +// distinct member name, combined via forwarders.hxx's combinator. protocol +// inherits this directly, not through an intermediate type, so each // Wrapper's base-to-derived static_cast to protocol -// (defined out of line below, once protocol is complete) is valid. +// is valid once protocol is complete. template using protocol_bases = typename[:protocol_bases_type():]; @@ -102,8 +148,8 @@ using protocol_bases = typename[:protocol_bases_type():]; template class protocol : public reflection_detail::protocol_bases { - template - friend struct reflection_detail::protocol_member_wrapper; + template + friend struct reflection_detail::protocol_single_overload_wrapper; using clone_or_move_fn = void* (*)(void*, const Allocator&); using destroy_fn = void (*)(void*, const Allocator&); @@ -184,7 +230,7 @@ class protocol : public reflection_detail::protocol_bases { template for (constexpr std::meta::info member : std::define_static_array( reflection_detail::interface_member_functions(^^T))) { constexpr std::meta::info entry = reflection_detail::find_data_member( - ^^vtable, reflection_detail::vtable_entry_name(member)); + ^^vtable, reflection_detail::vtable_slot_name(member)); constexpr std::meta::info merged_type = reflection_detail::candidate_overload_set_type( reflection_detail::resolve_implementation_candidates( @@ -277,17 +323,27 @@ class protocol : public reflection_detail::protocol_bases { namespace reflection_detail { -template -template -auto protocol_member_wrapper::operator()( - Args&&... args) const noexcept(std::meta::is_noexcept(Member)) { +template +R protocol_single_overload_wrapper::operator()(Ps... ps) const + noexcept(std::meta::is_noexcept(Member)) { using Owner = protocol; - using Base = forwarder_base; - const auto* base = static_cast(static_cast(this)); + using Combined = protocol_member_wrapper; + // The same representative every group member recomputes identically, so + // this names the exact forwarder_base specialization Combined is wrapped + // in as its sole member, regardless of which sibling overload this + // wrapper is for. + constexpr std::meta::info representative = + members_named_like(^^Interface, Member)[0]; + using Base = forwarder_base; + const auto* combined = static_cast(this); + const auto* base = + static_cast(static_cast(combined)); const auto* owner = static_cast(base); constexpr std::meta::info entry = - find_data_member(^^typename Owner::vtable, vtable_entry_name(Member)); - return owner->vtable_->[:entry:](owner->p_, std::forward(args)...); + find_data_member(^^typename Owner::vtable, vtable_slot_name(Member)); + return owner->vtable_->[:entry:](owner->p_, std::forward(ps)...); } } // namespace reflection_detail diff --git a/protocol_reflection_detail/naming.hxx b/protocol_reflection_detail/naming.hxx index c534c27..4e60c2c 100644 --- a/protocol_reflection_detail/naming.hxx +++ b/protocol_reflection_detail/naming.hxx @@ -58,11 +58,38 @@ constexpr std::string identifier_safe_string(std::string_view s) { // The generated vtable-entry name for a reflected member function or data // member, found by name. Operators, which have no identifier_of, need -// separate handling. +// separate handling. Deliberately identifier-only, not signature-qualified: +// this names a forwarder's single public-facing data member (forwarders.hxx), +// which must stay callable as e.g. `.compute(...)` regardless of how many +// overloads `compute` has. Every overload of the same name shares this +// name on purpose. consteval std::string vtable_entry_name(std::meta::info member) { return identifier_safe_string(std::meta::identifier_of(member)); } +// A generated vtable struct's entry name for `member`, unique per exact +// overload. Unlike vtable_entry_name above, this qualifies by the member's +// full display string (return type, parameter types, constness), not just +// its identifier: a vtable struct is an internal, never user-visible +// implementation detail with one data member per overload, so two +// overloads sharing a name (e.g. interface C's three `compute`s) need two +// distinct entries here, even though they share one forwarder name. +consteval std::string vtable_slot_name(std::meta::info member) { + return identifier_safe_string(std::meta::display_string_of(member)); +} + +// The data member of `struct_type` named `name`, for reading or writing +// a reflection-built struct's entry by name (a vtable_slot_name or +// vtable_entry_name result) rather than position. +consteval std::meta::info find_data_member(std::meta::info struct_type, + std::string_view name) { + for (std::meta::info member : std::meta::nonstatic_data_members_of( + struct_type, std::meta::access_context::current())) { + if (std::meta::identifier_of(member) == name) return member; + } + throw std::meta::exception("data member not found", ^^void); +} + } // namespace xyz::reflection_detail #endif // XYZ_PROTOCOL_REFLECTION_DETAIL_NAMING_HXX_ diff --git a/protocol_reflection_detail/naming_test.cc b/protocol_reflection_detail/naming_test.cc index ba3c0d0..d7cfe02 100644 --- a/protocol_reflection_detail/naming_test.cc +++ b/protocol_reflection_detail/naming_test.cc @@ -29,6 +29,7 @@ namespace { using xyz::reflection_detail::identifier_safe_string; using xyz::reflection_detail::vtable_entry_name; +using xyz::reflection_detail::vtable_slot_name; TEST(IdentifierSafeString, AlphanumericPassesThroughUnchanged) { EXPECT_EQ(identifier_safe_string("abcXYZ123"), "abcXYZ123"); @@ -83,4 +84,40 @@ TEST(VtableEntryName, NamesAnOrdinaryMemberByItsIdentifier) { EXPECT_STREQ(set_value_name, "set_5fvalue"); } +struct Overloaded { + int compute(int x) { return x; } + + double compute(double x) const { return x; } +}; + +consteval std::meta::info compute_overload(bool take_double) { + for (std::meta::info member : std::meta::members_of( + ^^Overloaded, std::meta::access_context::current())) { + if (std::meta::has_identifier(member) && + std::meta::identifier_of(member) == "compute" && + std::meta::is_const(member) == take_double) { + return member; + } + } + throw std::meta::exception("overload not found", ^^void); +} + +TEST(VtableSlotName, DistinctOverloadsGetDistinctNames) { + constexpr const char* int_overload = + std::define_static_string(vtable_slot_name(compute_overload(false))); + constexpr const char* double_overload = + std::define_static_string(vtable_slot_name(compute_overload(true))); + + EXPECT_STRNE(int_overload, double_overload); +} + +TEST(VtableSlotName, SameOverloadGivesTheSameNameEveryTime) { + constexpr const char* first = + std::define_static_string(vtable_slot_name(compute_overload(false))); + constexpr const char* second = + std::define_static_string(vtable_slot_name(compute_overload(false))); + + EXPECT_STREQ(first, second); +} + } // namespace diff --git a/protocol_reflection_detail/vtable_layout.hxx b/protocol_reflection_detail/vtable_layout.hxx index 4e9427f..1697db3 100644 --- a/protocol_reflection_detail/vtable_layout.hxx +++ b/protocol_reflection_detail/vtable_layout.hxx @@ -44,7 +44,11 @@ namespace xyz::reflection_detail { // One data_member_spec per dispatchable member of `interface` (const-only // if `const_only`), each a named function pointer of the member's vtable -// entry type, erased through `erased_pointer_type`. +// entry type, erased through `erased_pointer_type`. Named by +// vtable_slot_name, not vtable_entry_name: a vtable is an internal struct +// with one data member per exact overload, so overloads sharing an +// identifier (e.g. interface C's overloaded `compute`s) still need +// distinct entries here. consteval std::vector define_vtable_entries( std::meta::info interface, std::meta::info erased_pointer_type, bool const_only) { @@ -55,7 +59,7 @@ consteval std::vector define_vtable_entries( } specs.push_back(std::meta::data_member_spec( vtable_entry_pointer_type(member, erased_pointer_type), - {.name = vtable_entry_name(member)})); + {.name = vtable_slot_name(member)})); } return specs; } diff --git a/protocol_reflection_detail/vtable_layout_test.cc b/protocol_reflection_detail/vtable_layout_test.cc index e19d3a0..d758f30 100644 --- a/protocol_reflection_detail/vtable_layout_test.cc +++ b/protocol_reflection_detail/vtable_layout_test.cc @@ -27,7 +27,9 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace { using xyz::reflection_detail::const_view_vtable; +using xyz::reflection_detail::find_data_member; using xyz::reflection_detail::view_vtable; +using xyz::reflection_detail::vtable_slot_name; struct Fixture { int value() const { return 0; } @@ -35,10 +37,26 @@ struct Fixture { void set_value(int) {} }; +// A template-id (e.g. const_view_vtable) can't be reflected +// directly with ^^ when the template is itself an alias template ("'^^' +// cannot be applied to a using-declaration"). Naming the instantiation +// through a plain, non-template alias first works around it. +using ConstViewVtableFixture = const_view_vtable; +using ViewVtableFixture = view_vtable; + +// Entries are looked up by name rather than typed as literal members, +// because vtable_slot_name's output isn't a fixed, hand-writable identifier +// once an interface has overloads: it qualifies by full signature +// (naming.hxx). These tests use the same lookup real callers use in +// dispatch, rather than assuming a nice literal name. + TEST(ConstViewVtable, HasOneEntryPerConstMemberTakingAConstErasedPointer) { + constexpr std::meta::info value_entry = find_data_member( + ^^ConstViewVtableFixture, vtable_slot_name(^^Fixture::value)); static_assert( - std::is_same_v>().value), - int (*)(const void*)>); + std::is_same_v< + decltype(std::declval>().[:value_entry:]), + int (*)(const void*)>); static_assert(sizeof(const_view_vtable) == sizeof(void (*)())); } @@ -46,25 +64,36 @@ TEST(ViewVtable, HasAConstViewSubobjectAndOneEntryPerMember) { static_assert( std::is_same_v>().const_view), const_view_vtable>); - static_assert( - std::is_same_v>().value), - int (*)(void*)>); - // set_value's own literal underscore is escaped too (naming.hxx escapes - // every non-alphanumeric byte, including "_" itself): set_5fvalue. - static_assert( - std::is_same_v>().set_5fvalue), - void (*)(void*, int)>); + + constexpr std::meta::info value_entry = + find_data_member(^^ViewVtableFixture, vtable_slot_name(^^Fixture::value)); + static_assert(std::is_same_v< + decltype(std::declval>().[:value_entry:]), + int (*)(void*)>); + + constexpr std::meta::info set_value_entry = find_data_member( + ^^ViewVtableFixture, vtable_slot_name(^^Fixture::set_value)); + static_assert(std::is_same_v>() + .[:set_value_entry:]), + void (*)(void*, int)>); } TEST(ViewVtable, EntriesAreCallableFunctionPointers) { - view_vtable vtable{ - .const_view = {.value = +[](const void*) { return 42; }}, - .value = +[](void*) { return 42; }, - .set_5fvalue = +[](void*, int) {}}; - - EXPECT_EQ(vtable.const_view.value(nullptr), 42); - EXPECT_EQ(vtable.value(nullptr), 42); - vtable.set_5fvalue(nullptr, 1); + constexpr std::meta::info const_value_entry = find_data_member( + ^^ConstViewVtableFixture, vtable_slot_name(^^Fixture::value)); + constexpr std::meta::info value_entry = + find_data_member(^^ViewVtableFixture, vtable_slot_name(^^Fixture::value)); + constexpr std::meta::info set_value_entry = find_data_member( + ^^ViewVtableFixture, vtable_slot_name(^^Fixture::set_value)); + + view_vtable vtable{}; + vtable.const_view.[:const_value_entry:] = +[](const void*) { return 42; }; + vtable.[:value_entry:] = +[](void*) { return 42; }; + vtable.[:set_value_entry:] = +[](void*, int) {}; + + EXPECT_EQ(vtable.const_view.[:const_value_entry:](nullptr), 42); + EXPECT_EQ(vtable.[:value_entry:](nullptr), 42); + vtable.[:set_value_entry:](nullptr, 1); } } // namespace diff --git a/protocol_reflection_smoke_test.cc b/protocol_reflection_smoke_test.cc index 54e0cac..c76d8e6 100644 --- a/protocol_reflection_smoke_test.cc +++ b/protocol_reflection_smoke_test.cc @@ -26,6 +26,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include #include "interface_A.h" +#include "interface_C.h" #include "protocol.h" namespace { @@ -91,4 +92,24 @@ TEST(ProtocolReflectionSmoke, ANameIsActuallyNoexcept) { static_assert(noexcept(a.name())); } +// Interface C's compute() overloads share one name but need one distinct +// vtable entry each and one merged forwarder: proves the overload grouping +// in protocol_reflection.hxx (protocol_member_wrapper_combinator) actually +// resolves through real duck-typed dispatch, not just that the concept in +// conformance_test.cc accepts it. +struct CLike { + 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; } +}; + +TEST(ProtocolReflectionSmoke, DispatchesEachOverloadOfCToTheMatchingCandidate) { + xyz::protocol c(std::in_place_type); + EXPECT_EQ(c.compute(5), 10); + EXPECT_EQ(c.compute(2.0), 6.0); + EXPECT_EQ(c.compute(std::string("ab")), "abab"); +} + } // namespace