From 0e0d4bbbd8224962a76f9723d990e3fe0cba979b Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 26 Jul 2026 08:25:05 +0000 Subject: [PATCH] Add protocol_view and protocol_view Non-owning views dispatching through a raw pointer/vtable pair instead of protocol's allocator-owned storage, reusing the same forwarder-combinator and thunk machinery; the const view exposes only const members, through a const void*. --- protocol_reflection.hxx | 555 ++++++++++++++++++++++++++++-- protocol_reflection_smoke_test.cc | 175 ++++++++++ 2 files changed, 694 insertions(+), 36 deletions(-) diff --git a/protocol_reflection.hxx b/protocol_reflection.hxx index 06e9433..b7914bd 100644 --- a/protocol_reflection.hxx +++ b/protocol_reflection.hxx @@ -17,17 +17,16 @@ 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. ==============================================================================*/ -// The C++26-reflection backend: a single xyz::protocol class -// template body that works for any interface satisfying -// reflection_protocol_concept, with no per-interface code generation step. -// -// Allocator-aware construction, copy/move/destroy/swap, and dispatch, one -// interface at a time. No narrowing conversions between protocol -// specializations yet. xyz::protocol_view is out of scope here; it is -// added once dispatch is proven. +// The C++26-reflection backend: single xyz::protocol and +// xyz::protocol_view/protocol_view class templates that work +// for any interface satisfying reflection_protocol_concept, with no +// per-interface code generation step. Allocator-aware construction, +// copy/move/destroy/swap, dispatch, and narrowing conversions between +// protocol/protocol_view specializations. #ifndef XYZ_PROTOCOL_REFLECTION_HXX_ #define XYZ_PROTOCOL_REFLECTION_HXX_ +#include #include #include #include @@ -60,6 +59,23 @@ consteval std::vector members_named_like( return result; } +// The first *const* member sharing `member`'s name: the const view's +// representative for a name-group, which can differ from +// members_named_like(...)[0] when a group mixes const and non-const +// overloads. Every const sibling recomputes this identically, the +// invariant protocol_const_view_bases_type relies on to agree on one +// representative per const name-group, the same way protocol_bases_type +// relies on members_named_like(...)[0] for the unfiltered case. +consteval std::meta::info first_const_member_named_like( + std::meta::info interface, std::meta::info member) { + for (std::meta::info sibling : members_named_like(interface, member)) { + if (std::meta::is_const(sibling)) { + return sibling; + } + } + throw std::meta::exception("no const sibling", ^^void); +} + // 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 @@ -141,41 +157,316 @@ consteval std::meta::info protocol_bases_type() { template using protocol_bases = typename[:protocol_bases_type():]; +// protocol_view/protocol_view's own forwarder +// machinery: the same shape as protocol_single_overload_wrapper/ +// protocol_member_wrapper_type/protocol_bases_type above, generalized to a +// different Owner shape. protocol_view has no Allocator, dispatches +// through vptr_/ptr_ instead of vtable_/p_, and (for the const view) +// exposes only const overloads. protocol_member_wrapper_combinator's +// merge-via-inheritance is Owner-agnostic, and reused as-is for both. + +// Mutable protocol_view: every member, dispatched through +// void*, exactly like protocol's own entries. +template +struct protocol_view_single_overload_wrapper { + R operator()(Ps... ps) const noexcept(std::meta::is_noexcept(Member)); +}; + +consteval std::meta::info protocol_view_member_wrapper_type( + std::meta::info interface, std::meta::info member) { + std::vector base_types; + for (std::meta::info sibling : members_named_like(interface, member)) { + std::vector args{ + interface, 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_view_single_overload_wrapper, args)); + } + return std::meta::substitute(^^protocol_member_wrapper_combinator, + base_types); +} + +template +using protocol_view_member_wrapper = + typename[:protocol_view_member_wrapper_type(^^Interface, Member):]; + +template +consteval std::meta::info protocol_view_bases_type() { + std::vector bases; + for (std::meta::info member : interface_member_functions(^^Interface)) { + std::meta::info representative = members_named_like(^^Interface, member)[0]; + if (representative != member) { + continue; + } + std::meta::info wrapper_type = + protocol_view_member_wrapper_type(^^Interface, member); + bases.push_back(std::meta::substitute( + ^^forwarder_base, + { + wrapper_type, std::meta::reflect_constant(representative)})); + } + return forwarders_type(bases); +} + +template +using protocol_view_bases = typename[:protocol_view_bases_type():]; + +// protocol_view: const members only, dispatched through +// const void*. +template +struct protocol_const_view_single_overload_wrapper { + R operator()(Ps... ps) const noexcept(std::meta::is_noexcept(Member)); +}; + +consteval std::meta::info protocol_const_view_member_wrapper_type( + std::meta::info interface, std::meta::info member) { + std::vector base_types; + for (std::meta::info sibling : members_named_like(interface, member)) { + if (!std::meta::is_const(sibling)) { + continue; + } + std::vector args{ + interface, 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_const_view_single_overload_wrapper, args)); + } + return std::meta::substitute(^^protocol_member_wrapper_combinator, + base_types); +} + +template +using protocol_const_view_member_wrapper = + typename[:protocol_const_view_member_wrapper_type(^^Interface, Member):]; + +template +consteval std::meta::info protocol_const_view_bases_type() { + std::vector bases; + for (std::meta::info member : interface_member_functions(^^Interface)) { + if (!std::meta::is_const(member)) { + continue; + } + std::meta::info representative = + first_const_member_named_like(^^Interface, member); + if (representative != member) { + continue; + } + std::meta::info wrapper_type = + protocol_const_view_member_wrapper_type(^^Interface, member); + bases.push_back(std::meta::substitute( + ^^forwarder_base, + { + wrapper_type, std::meta::reflect_constant(representative)})); + } + return forwarders_type(bases); +} + +template +using protocol_const_view_bases = + typename[:protocol_const_view_bases_type():]; + +// Populates a const_view_vtable/view_vtable +// (vtable_layout.hxx) for one implementation, dispatch entry by dispatch +// entry: the read-only (protocol_view) and read-write +// (protocol_view) counterparts of protocol's own vtable +// population above, using the same template-for + thunk.hxx/conformance.hxx +// pattern. +template +consteval const_view_vtable build_const_view_vtable() { + using ConstViewVtable = const_view_vtable; + ConstViewVtable vt{}; + template for (constexpr std::meta::info member : std::define_static_array( + interface_member_functions(^^Interface))) { + if constexpr (std::meta::is_const(member)) { + constexpr std::meta::info entry = + find_data_member(^^ConstViewVtable, vtable_slot_name(member)); + constexpr std::meta::info merged_type = candidate_overload_set_type( + resolve_implementation_candidates(member, ^^Implementation), + ^^const Implementation&); + using Thunk = erased_call_thunk< + Implementation, + typename[:merged_type:], typename + [:member_function_type( + member):], true, std::meta::is_noexcept(member)>; + vt.[:entry:] = &Thunk::call; + } + } + return vt; +} + +template +inline constexpr const_view_vtable const_view_vtable_for = + build_const_view_vtable(); + +template +consteval view_vtable build_view_vtable() { + using ViewVtable = view_vtable; + ViewVtable vt{}; + vt.const_view = const_view_vtable_for; + template for (constexpr std::meta::info member : std::define_static_array( + interface_member_functions(^^Interface))) { + constexpr std::meta::info entry = + find_data_member(^^ViewVtable, vtable_slot_name(member)); + constexpr std::meta::info merged_type = candidate_overload_set_type( + resolve_implementation_candidates(member, ^^Implementation), + ^^Implementation&); + using Thunk = erased_call_thunk< + Implementation, typename[:merged_type:], typename[:member_function_type( + member): + ], false, std::meta::is_noexcept(member)>; + vt.[:entry:] = &Thunk::call; + } + return vt; +} + +template +inline constexpr view_vtable view_vtable_for = + build_view_vtable(); + +// Copies every data member `to` has from the same-named member of `from`. +// The shared implementation behind every map_*_vtable_members overload +// below: each vtable shape (owning, const-view, view) differs only in +// which fields it has, not in how narrowing one to another works. +template +void copy_matching_by_name(const FromVtable* from, ToVtable* to) { + template for (constexpr std::meta::info to_member : + std::define_static_array(std::meta::nonstatic_data_members_of( + ^^ToVtable, std::meta::access_context::current()))) { + constexpr std::string_view name = std::meta::identifier_of(to_member); + constexpr std::meta::info from_member = + find_data_member(^^FromVtable, name); + to->[:to_member:] = from->[:from_member:]; + } +} + +// const_view_vtable and view_vtable (vtable_layout.hxx) +// are local classes, declared inside make_const_view_vtable_info and +// make_view_vtable_info in this namespace. +// +// A function-local class's namespace for ADL is the namespace of the +// function that declares it, not the enclosing namespace. That's different +// from protocol::vtable, a member class of protocol, whose +// associated namespace is xyz directly. +// +// protocol.h's get_const_vtable/get_vtable call these two unqualified and +// rely on ADL, so they must live here, in xyz::reflection_detail, to be +// found. map_owning_vtable_members has no such restriction and correctly +// lives in xyz instead. Moving these two into xyz produces "not declared in +// this scope, and no declarations were found by argument-dependent lookup." + +// const_view_vtable is flat, so narrowing it is exactly +// copy_matching_by_name. +template +void map_const_vtable_members(const FromVtable* from, ToVtable* to) { + copy_matching_by_name(from, to); +} + +// view_vtable additionally has a const_view sub-object: narrowed +// the same way, recursively, then every other (non-const_view) entry is +// narrowed like the owning vtable. +template +void map_vtable_members(const FromVtable* from, ToVtable* to) { + map_const_vtable_members(&from->const_view, &to->const_view); + template for (constexpr std::meta::info to_member : + std::define_static_array(std::meta::nonstatic_data_members_of( + ^^ToVtable, std::meta::access_context::current()))) { + constexpr std::string_view name = std::meta::identifier_of(to_member); + if constexpr (name != "const_view") { + constexpr std::meta::info from_member = + find_data_member(^^FromVtable, name); + to->[:to_member:] = from->[:from_member:]; + } + } +} + } // namespace reflection_detail -// protocol's own nested vtable already is the owning -// vtable used for narrowing; this trait just names it the way -// protocol.h's get_owning_vtable expects, matching how the Python/libclang -// backend's own generated protocol_owning_vtable_traits<::xyz::A, -// Allocator> is a bare alias to protocol<::xyz::A, Allocator>::vtable, not -// a separately-designed type. +// protocol's own nested vtable is already the owning +// vtable used for narrowing. This trait just names it the way protocol.h's +// get_owning_vtable expects, matching how the Python/libclang backend's +// generated protocol_owning_vtable_traits<::xyz::A, Allocator> is a bare +// alias to protocol<::xyz::A, Allocator>::vtable, not a separately-designed +// type. template struct protocol_owning_vtable_traits { using vtable = typename protocol::vtable; }; -// Narrows a source owning vtable to a target one by copying every entry -// the target has (by exact name -- including xyz_protocol_clone/_move/ -// _destroy, which every protocol's own vtable always declares) from the -// matching entry in the source. FromVtable/ToVtable are deduced directly -// from the pointer arguments, not from an Interface/Allocator template -// argument list: unlike the Python/libclang backend, where a per-interface -// generated overload hardcodes its own target type, this one generic -// definition works for any (FromInterface, ToInterface, Allocator), since -// it reflects on the two concrete vtable struct types themselves rather -// than needing to know which interfaces they came from. Found via ADL -// from get_owning_vtable (protocol.h): FromVtable/ToVtable are nested -// types of xyz::protocol<...>, so this must live in namespace xyz, not -// xyz::reflection_detail, to be visible there. +// protocol_view/protocol_view's narrowing counterpart to +// protocol_owning_vtable_traits above, used by protocol.h's +// get_const_vtable/get_vtable. +template +struct protocol_vtable_traits { + using const_vtable = reflection_detail::const_view_vtable; + using vtable = reflection_detail::view_vtable; +}; + +namespace reflection_detail { + +// Narrows a view_vtable* to a view_vtable* +// (protocol's own view_vt field), using the same cached-mapping +// mechanism get_vtable (protocol.h) uses. It's keyed directly off the +// concrete FromViewVtable/ToViewVtable types deduced from view_vt's own +// pointer type, since a vtable struct here carries no Interface type of its +// own to look protocol_vtable_traits up by. +template +const ToViewVtable* narrow_view_vtable_pointer(const FromViewVtable* source) { + static const char conversion_anchor = 0; + auto mapping_function = [](const void* from, void* to) { + map_vtable_members(static_cast(from), + static_cast(to)); + }; + return static_cast(get_mapped_vtable( + source, &conversion_anchor, sizeof(ToViewVtable), mapping_function)); +} + +} // namespace reflection_detail + +// Narrows a source owning vtable to a target one by copying every entry the +// target has, by exact name, from the matching entry in the source. That +// includes xyz_protocol_clone/_move/_destroy, which every protocol's own +// vtable declares. +// +// The exception is view_vt (protocol's own vtable field): it +// points to a different view_vtable per Interface, so it needs +// its own recursive narrowing instead of a same-type copy. +// +// FromVtable and ToVtable are deduced directly from the pointer arguments, +// not from an Interface/Allocator template argument list. The Python/ +// libclang backend instead generates one hardcoded overload per interface +// pair; this single generic definition works for any (FromInterface, +// ToInterface, Allocator) because it operates on the two concrete vtable +// struct types directly, without needing to know which interfaces they +// came from. +// +// get_owning_vtable (protocol.h) finds this function via ADL: FromVtable +// and ToVtable are nested types of xyz::protocol<...>, so this function +// must live in namespace xyz, not xyz::reflection_detail, to be visible +// there. template void map_owning_vtable_members(const FromVtable* from, ToVtable* to) { template for (constexpr std::meta::info to_member : std::define_static_array(std::meta::nonstatic_data_members_of( ^^ToVtable, std::meta::access_context::current()))) { constexpr std::string_view name = std::meta::identifier_of(to_member); - constexpr std::meta::info from_member = - reflection_detail::find_data_member(^^FromVtable, name); - to->[:to_member:] = from->[:from_member:]; + if constexpr (name == "view_vt") { + to->view_vt = reflection_detail::narrow_view_vtable_pointer< + std::remove_const_tview_vt)>>, + std::remove_const_tview_vt)>>>( + from->view_vt); + } else { + constexpr std::meta::info from_member = + reflection_detail::find_data_member(^^FromVtable, name); + to->[:to_member:] = from->[:from_member:]; + } } } @@ -185,15 +476,20 @@ class protocol : public reflection_detail::protocol_bases { friend struct reflection_detail::protocol_single_overload_wrapper; template friend class protocol; + friend class protocol_view; + friend class protocol_view; using clone_or_move_fn = void* (*)(void*, const Allocator&); using destroy_fn = void (*)(void*, const Allocator&); - - // protocol's own vtable: clone/move/destroy, plus one entry per - // interface member, all erased through void* (never const void*). - // The owning object itself provides both const and non-const access - // paths, so a single erased pointer kind suffices here, unlike - // vtable_layout.hxx's view_vtable/const_view_vtable split. + using view_vtable_ptr = const reflection_detail::view_vtable*; + + // protocol's own vtable: clone/move/destroy, a pointer to the + // allocator-independent shape vtable protocol_view narrows against + // (view_vt), plus one entry per interface member, all erased through + // void* (never const void*). The owning object itself provides both + // const and non-const access paths, so a single erased pointer kind + // suffices for its own entries, unlike vtable_layout.hxx's + // view_vtable/const_view_vtable split that view_vt points to. struct vtable_incomplete; consteval { std::vector specs = { @@ -206,6 +502,9 @@ class protocol : public reflection_detail::protocol_bases { std::meta::data_member_spec(^^destroy_fn, { .name = "xyz_protocol_destroy"}), + std::meta::data_member_spec(^^view_vtable_ptr, + { + .name = "view_vt"}), }; for (std::meta::info spec : reflection_detail::define_vtable_entries(^^T, ^^void*, false)) { @@ -262,6 +561,7 @@ class protocol : public reflection_detail::protocol_bases { vt.xyz_protocol_clone = xyz_protocol_clone; vt.xyz_protocol_move = xyz_protocol_move; vt.xyz_protocol_destroy = xyz_protocol_destroy; + vt.view_vt = &reflection_detail::view_vtable_for; 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( @@ -486,6 +786,141 @@ class protocol : public reflection_detail::protocol_bases { } }; +template +class protocol_view + : public reflection_detail::protocol_const_view_bases { + template + friend struct reflection_detail::protocol_const_view_single_overload_wrapper; + template + friend class protocol_view; + + const void* ptr_; + const reflection_detail::const_view_vtable* vptr_; + + constexpr protocol_view( + const void* ptr, + const reflection_detail::const_view_vtable* vptr) noexcept + : ptr_(ptr), vptr_(vptr) {} + + template + static const void* checked_ptr(const protocol& p) noexcept { + assert(!p.valueless_after_move()); + return p.p_; + } + + public: + template + requires reflection_detail::reflection_protocol_const_concept && + not_protocol_or_view + constexpr protocol_view(const U& obj) noexcept + : ptr_(std::addressof(obj)), + vptr_( + &reflection_detail::const_view_vtable_for>) { + } + + template + requires reflection_detail::reflection_protocol_const_concept && + not_protocol_or_view + protocol_view(const U&&) = delete; + + template + protocol_view(const protocol& p) noexcept + : ptr_(checked_ptr(p)), vptr_(&p.vtable_->view_vt->const_view) {} + + template + protocol_view(const protocol&&) = delete; + + template + protocol_view(protocol& p) noexcept + : ptr_(checked_ptr(p)), vptr_(&p.vtable_->view_vt->const_view) {} + + template + protocol_view(protocol&&) = delete; + + constexpr protocol_view(protocol_view other) noexcept; + + template + requires(!std::same_as) + protocol_view(const protocol_view& other) noexcept + : ptr_(other.ptr_), vptr_(get_const_vtable(other.vptr_)) {} + + template + requires(!std::same_as) + protocol_view(const protocol_view& other) noexcept + : ptr_(other.ptr_), + vptr_(get_const_vtable(&other.vptr_->const_view)) {} + + template + requires(!std::same_as) + protocol_view(const protocol& p) noexcept + : protocol_view(protocol_view(p)) {} + + template + requires(!std::same_as) + protocol_view(const protocol&&) = delete; + + template + requires(!std::same_as) + protocol_view(protocol& p) noexcept + : protocol_view(protocol_view(p)) {} + + template + requires(!std::same_as) + protocol_view(protocol&&) = delete; +}; + +template +class protocol_view : public reflection_detail::protocol_view_bases { + template + friend struct reflection_detail::protocol_view_single_overload_wrapper; + template + friend class protocol_view; + + void* ptr_; + const reflection_detail::view_vtable* vptr_; + + template + static void* checked_ptr(protocol& p) noexcept { + assert(!p.valueless_after_move()); + return p.p_; + } + + public: + template + requires reflection_detail::reflection_protocol_concept && + not_protocol_or_view + constexpr protocol_view(U& obj) noexcept + : ptr_(std::addressof(obj)), + vptr_(&reflection_detail::view_vtable_for>) {} + + template + requires reflection_detail::reflection_protocol_concept && + not_protocol_or_view + protocol_view(const U&&) = delete; + + template + protocol_view(protocol& p) noexcept + : ptr_(checked_ptr(p)), vptr_(p.vtable_->view_vt) {} + + template + protocol_view(protocol&&) = delete; + + template + requires(!std::same_as) + protocol_view(const protocol_view& other) noexcept + : ptr_(other.ptr_), vptr_(get_vtable(other.vptr_)) {} + + template + requires(!std::same_as) + protocol_view(protocol& p) noexcept + : protocol_view(protocol_view(p)) {} + + template + requires(!std::same_as) + protocol_view(protocol&&) = delete; +}; + namespace reflection_detail { template vtable_->[:entry:](owner->p_, std::forward(ps)...); } +template +R protocol_view_single_overload_wrapper::operator()(Ps... ps) const + noexcept(std::meta::is_noexcept(Member)) { + using Owner = protocol_view; + using Combined = protocol_view_member_wrapper; + constexpr std::meta::info representative = + members_named_like(^^Interface, Member)[0]; + using Base = forwarder_base; + using ViewVtable = view_vtable; + 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(^^ViewVtable, vtable_slot_name(Member)); + return owner->vptr_->[:entry:](owner->ptr_, std::forward(ps)...); +} + +template +R protocol_const_view_single_overload_wrapper::operator()(Ps... ps) const + noexcept(std::meta::is_noexcept(Member)) { + using Owner = protocol_view; + using Combined = protocol_const_view_member_wrapper; + // Unlike the owning/mutable-view wrappers, the representative here must + // be the first *const* sibling: members_named_like(...)[0] could be a + // non-const overload the const view never exposes at all. + constexpr std::meta::info representative = + first_const_member_named_like(^^Interface, Member); + using Base = forwarder_base; + using ConstViewVtable = const_view_vtable; + 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(^^ConstViewVtable, vtable_slot_name(Member)); + return owner->vptr_->[:entry:](owner->ptr_, std::forward(ps)...); +} + } // namespace reflection_detail +template +inline constexpr protocol_view::protocol_view( + protocol_view other) noexcept + : ptr_(other.ptr_), vptr_(&other.vptr_->const_view) {} + } // namespace xyz #endif // XYZ_PROTOCOL_REFLECTION_HXX_ diff --git a/protocol_reflection_smoke_test.cc b/protocol_reflection_smoke_test.cc index 0d85027..cbb8bf9 100644 --- a/protocol_reflection_smoke_test.cc +++ b/protocol_reflection_smoke_test.cc @@ -467,4 +467,179 @@ TEST(ProtocolReflectionSmoke, NarrowingConversionConcurrentStressing) { } } +// xyz::protocol_view/protocol_view: standalone equivalents of +// protocol_test.cc's own ProtocolViewTest suite. + +TEST(ProtocolReflectionSmoke, ViewFromMutableConcrete) { + ALike a("view_test"); + xyz::protocol_view view(a); + EXPECT_EQ(view.name(), "view_test"); + EXPECT_EQ(view.count(), 1); + EXPECT_EQ(a.count_, 1); // The view mutated `a` directly. +} + +TEST(ProtocolReflectionSmoke, ViewFromMutableProtocol) { + xyz::protocol a(std::in_place_type, "proto_view"); + xyz::protocol_view view(a); + EXPECT_EQ(view.name(), "proto_view"); + EXPECT_EQ(view.count(), 1); + EXPECT_EQ(a.count(), 2); // The view mutated `a` directly. +} + +TEST(ProtocolReflectionSmoke, ConstViewFromMutableConcrete) { + ALike a("view_test"); + xyz::protocol_view view(a); + EXPECT_EQ(view.name(), "view_test"); +} + +TEST(ProtocolReflectionSmoke, ConstViewFromMutableProtocol) { + xyz::protocol a(std::in_place_type, "proto_view"); + xyz::protocol_view view(a); + EXPECT_EQ(view.name(), "proto_view"); +} + +TEST(ProtocolReflectionSmoke, ConstViewFromMutViewConcrete) { + ALike a("view_test"); + xyz::protocol_view mut_view(a); + xyz::protocol_view const_view(mut_view); + EXPECT_EQ(const_view.name(), "view_test"); +} + +TEST(ProtocolReflectionSmoke, ConstViewFromConstProtocol) { + const xyz::protocol a(std::in_place_type, "proto_view"); + xyz::protocol_view view(a); + EXPECT_EQ(view.name(), "proto_view"); +} + +TEST(ProtocolReflectionSmoke, ViewConstnessRouting) { + BLike b; + xyz::protocol_view view(b); + EXPECT_FALSE(view.is_ready()); + view.process("view processing"); + EXPECT_TRUE(view.is_ready()); + auto results = view.get_results(); + ASSERT_EQ(results.size(), 1u); + EXPECT_EQ(results[0], 15); +} + +class CopyCounter { + public: + int* copies_; + + explicit CopyCounter(int* copies) : copies_(copies) {} + + CopyCounter(const CopyCounter& other) : copies_(other.copies_) { + if (copies_) (*copies_)++; + } + + CopyCounter& operator=(const CopyCounter& other) { + copies_ = other.copies_; + if (copies_) (*copies_)++; + return *this; + } + + CopyCounter(CopyCounter&&) = default; + CopyCounter& operator=(CopyCounter&&) = default; + + std::string_view name() const noexcept { return "CopyCounter"; } + + int count() { return 0; } +}; + +TEST(ProtocolReflectionSmoke, ViewCopiesAreShallow) { + int copies = 0; + CopyCounter c(&copies); + xyz::protocol_view view(c); + EXPECT_EQ(copies, 0); + + xyz::protocol_view view2 = view; + EXPECT_EQ(copies, 0); + + xyz::protocol_view view3(view2); + EXPECT_EQ(view3.count(), 0); + EXPECT_EQ(copies, 0); +} + +TEST(ProtocolReflectionSmoke, ViewMoveIsStandard) { + ALike a("move_test"); + xyz::protocol_view view(a); + xyz::protocol_view view2 = std::move(view); + + // Moved-from view is still valid: it's just a pointer copy. + EXPECT_EQ(view.name(), "move_test"); + EXPECT_EQ(view2.name(), "move_test"); +} + +TEST(ProtocolReflectionSmoke, PreventConstructionFromRValues) { + static_assert(!std::constructible_from, ALike>); + static_assert(!std::constructible_from, + xyz::protocol>); + static_assert( + !std::constructible_from, ALike>); + static_assert(!std::constructible_from, + xyz::protocol>); + static_assert(!std::constructible_from, + const xyz::protocol>); + static_assert(!std::constructible_from, + xyz::protocol>); + static_assert( + !std::constructible_from, + xyz::protocol>); +} + +// Narrowing conversions between views: A -> A_Subset, mirroring the owning +// narrowing tests above but through get_const_vtable/get_vtable instead of +// get_owning_vtable. + +TEST(ProtocolReflectionSmoke, NarrowingViewConversions) { + ALike a_obj; + xyz::protocol_view view_a(a_obj); + + xyz::protocol_view const_view_subset = view_a; + EXPECT_EQ(const_view_subset.name(), "ALike"); + + xyz::protocol_view view_subset = view_a; + EXPECT_EQ(view_subset.name(), "ALike"); +} + +TEST(ProtocolReflectionSmoke, ConstViewNarrowingFromConstView) { + ALike a_obj; + xyz::protocol_view const_a(a_obj); + xyz::protocol_view const_subset(const_a); + EXPECT_EQ(const_subset.name(), "ALike"); +} + +// The protocol_view counterpart to NarrowingConversionConcurrentStressing: +// proves get_mapped_vtable caching/locking under contention for +// protocol_view's own narrowing path too. +TEST(ProtocolReflectionSmoke, ViewNarrowingConversionConcurrentStressing) { + constexpr int kNumThreads = 20; + constexpr int kIterationsPerThread = 50; + std::vector threads; + std::atomic start_signal{false}; + + for (int i = 0; i < kNumThreads; ++i) { + threads.emplace_back([&start_signal]() { + while (!start_signal.load()) { + std::this_thread::yield(); + } + for (int iter = 0; iter < kIterationsPerThread; ++iter) { + ALike a_obj; + xyz::protocol_view view_a(a_obj); + + xyz::protocol_view const_view = view_a; + EXPECT_EQ(const_view.name(), "ALike"); + + xyz::protocol_view mut_view = view_a; + EXPECT_EQ(mut_view.name(), "ALike"); + } + }); + } + + start_signal.store(true); + for (auto& thread : threads) { + thread.join(); + } +} + } // namespace