From aaffd2f4f869a66dca72fb3efd7921e9d4383123 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sat, 25 Jul 2026 18:05:55 +0000 Subject: [PATCH] Add allocator-awareness protocol gains the allocator-extended constructor set plus allocator-aware assignment and swap, matching the Python/libclang backend's protocol_A.h: equal allocators steal the source's pointer, unequal ones move-construct into freshly allocated storage. --- CMakeLists.txt | 1 + protocol_reflection.hxx | 95 +++++++++++++----- protocol_reflection_smoke_test.cc | 159 ++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a2fe5ef..4bea543 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -254,6 +254,7 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) interface_A.h interface_B.h interface_C.h + tracking_allocator.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 5ae319f..bf4ed14 100644 --- a/protocol_reflection.hxx +++ b/protocol_reflection.hxx @@ -17,17 +17,14 @@ 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 first end-to-end vertical slice of 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. +// 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. // -// Deliberately narrow: default-allocator construction (using the real -// Allocator template parameter, but none of the allocator-extended -// constructor overloads yet), copy/move/destroy, and dispatch, one -// interface at a time with no narrowing conversions between -// protocol specializations. xyz::protocol_view is out of scope -// here; it is added once dispatch is proven. +// 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. #ifndef XYZ_PROTOCOL_REFLECTION_HXX_ #define XYZ_PROTOCOL_REFLECTION_HXX_ @@ -269,20 +266,34 @@ class protocol : public reflection_detail::protocol_bases { const vtable* vtable_; [[no_unique_address]] Allocator alloc_; + using allocator_traits = std::allocator_traits; + public: template - explicit constexpr protocol(std::in_place_type_t, Ts&&... ts) + explicit constexpr protocol(std::allocator_arg_t, const Allocator& alloc, + std::in_place_type_t, Ts&&... ts) requires std::same_as, U> && not_protocol_or_view && std::constructible_from && std::copy_constructible && - std::default_initializable && reflection_detail::reflection_protocol_concept - : alloc_() { + : alloc_(alloc) { p_ = create_storage(std::forward(ts)...); vtable_ = &vtable_impl::vtable_; } - constexpr protocol(const protocol& other) : alloc_(other.alloc_) { + template + explicit constexpr protocol(std::in_place_type_t, Ts&&... ts) + requires std::same_as, U> && + not_protocol_or_view && std::constructible_from && + std::copy_constructible && + std::default_initializable && + reflection_detail::reflection_protocol_concept + : protocol(std::allocator_arg_t{}, Allocator{}, std::in_place_type, + std::forward(ts)...) {} + + constexpr protocol(std::allocator_arg_t, const Allocator& alloc, + const protocol& other) + : alloc_(alloc) { if (!other.valueless_after_move()) { p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); vtable_ = other.vtable_; @@ -292,11 +303,42 @@ class protocol : public reflection_detail::protocol_bases { } } - constexpr protocol(protocol&& other) noexcept : alloc_(other.alloc_) { - p_ = std::exchange(other.p_, nullptr); - vtable_ = std::exchange(other.vtable_, nullptr); + constexpr protocol(const protocol& other) + : protocol(std::allocator_arg_t{}, + allocator_traits::select_on_container_copy_construction( + other.alloc_), + other) {} + + constexpr protocol( + std::allocator_arg_t, const Allocator& alloc, + protocol&& other) noexcept(allocator_traits::is_always_equal::value) + : alloc_(alloc) { + if constexpr (allocator_traits::is_always_equal::value) { + p_ = std::exchange(other.p_, nullptr); + vtable_ = std::exchange(other.vtable_, nullptr); + } else { + if (alloc_ == other.alloc_) { + p_ = std::exchange(other.p_, nullptr); + vtable_ = std::exchange(other.vtable_, nullptr); + } else { + if (!other.valueless_after_move()) { + p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_); + vtable_ = other.vtable_; + other.vtable_->xyz_protocol_destroy(other.p_, other.alloc_); + other.p_ = nullptr; + other.vtable_ = nullptr; + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + } } + constexpr protocol(protocol&& other) noexcept( + allocator_traits::is_always_equal::value) + : protocol(std::allocator_arg_t{}, other.alloc_, std::move(other)) {} + constexpr bool valueless_after_move() const noexcept { return p_ == nullptr; } ~protocol() { @@ -305,20 +347,29 @@ class protocol : public reflection_detail::protocol_bases { } } - protocol& operator=(protocol other) noexcept { + protocol& operator=(protocol other) noexcept( + allocator_traits::is_always_equal::value) { std::swap(p_, other.p_); std::swap(vtable_, other.vtable_); - std::swap(alloc_, other.alloc_); + if constexpr (!allocator_traits::is_always_equal::value) { + std::swap(alloc_, other.alloc_); + } return *this; } - void swap(protocol& other) noexcept { + void swap(protocol& other) noexcept( + allocator_traits::is_always_equal::value) { std::swap(p_, other.p_); std::swap(vtable_, other.vtable_); - std::swap(alloc_, other.alloc_); + if constexpr (!allocator_traits::is_always_equal::value) { + std::swap(alloc_, other.alloc_); + } } - friend void swap(protocol& lhs, protocol& rhs) noexcept { lhs.swap(rhs); } + friend void swap(protocol& lhs, protocol& rhs) noexcept( + allocator_traits::is_always_equal::value) { + lhs.swap(rhs); + } }; namespace reflection_detail { diff --git a/protocol_reflection_smoke_test.cc b/protocol_reflection_smoke_test.cc index 2be0d14..ce80539 100644 --- a/protocol_reflection_smoke_test.cc +++ b/protocol_reflection_smoke_test.cc @@ -21,6 +21,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // interface, keeping this file's compile/link surface minimal. #include +#include +#include #include #include #include @@ -30,6 +32,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "interface_B.h" #include "interface_C.h" #include "protocol.h" +#include "tracking_allocator.h" namespace { @@ -77,6 +80,10 @@ struct ALike { std::string name_ = "ALike"; int count_ = 0; + ALike() = default; + + explicit ALike(std::string_view name) : name_(name) {} + std::string_view name() const noexcept { return name_; } int count() { return ++count_; } @@ -138,4 +145,156 @@ TEST(ProtocolReflectionSmoke, DispatchesAllThreeMembersOfB) { EXPECT_EQ(b.get_results(), (std::vector{5})); } +// Allocator-awareness: standalone equivalents of protocol_test.cc's own +// TrackingAllocator-based tests, proving the allocator-extended +// constructors, select_on_container_copy_construction, and the +// equal-vs-non-equal-allocator move/swap paths all really run through +// TrackingAllocator rather than silently falling back to Allocator{}. + +TEST(ProtocolReflectionSmoke, CountAllocationsForInPlaceConstruction) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> a( + std::allocator_arg, + xyz::TrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type); + EXPECT_EQ(alloc_counter, 1u); + EXPECT_EQ(dealloc_counter, 0u); + } + EXPECT_EQ(alloc_counter, 1u); + EXPECT_EQ(dealloc_counter, 1u); +} + +TEST(ProtocolReflectionSmoke, CountAllocationsForCopyConstruction) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> a( + std::allocator_arg, + xyz::TrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type); + xyz::protocol> aa(a); + EXPECT_EQ(alloc_counter, 2u); + } + EXPECT_EQ(dealloc_counter, 2u); +} + +TEST(ProtocolReflectionSmoke, CountAllocationsForMoveConstruction) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> a( + std::allocator_arg, + xyz::TrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type); + xyz::protocol> aa(std::move(a)); + } + EXPECT_EQ(alloc_counter, 1u); + EXPECT_EQ(dealloc_counter, 1u); +} + +TEST(ProtocolReflectionSmoke, CountAllocationsForMoveAssignment) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> a( + std::allocator_arg, + xyz::TrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type); + xyz::protocol> aa( + std::allocator_arg, + xyz::TrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type); + aa = std::move(a); + } + EXPECT_EQ(alloc_counter, 2u); + EXPECT_EQ(dealloc_counter, 2u); +} + +template +struct NonEqualTrackingAllocator : xyz::TrackingAllocator { + using xyz::TrackingAllocator::TrackingAllocator; + using propagate_on_container_move_assignment = std::true_type; + + template + struct rebind { + using other = NonEqualTrackingAllocator; + }; + + friend bool operator==(const NonEqualTrackingAllocator&, + const NonEqualTrackingAllocator&) noexcept { + return false; + } + + friend bool operator!=(const NonEqualTrackingAllocator&, + const NonEqualTrackingAllocator&) noexcept { + return true; + } +}; + +TEST(ProtocolReflectionSmoke, + CountAllocationsForMoveAssignmentWhenAllocatorsDontCompareEqual) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> a( + std::allocator_arg, + NonEqualTrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type); + xyz::protocol> aa( + std::allocator_arg, + NonEqualTrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type); + EXPECT_EQ(alloc_counter, 2u); + aa = std::move(a); // Copies (move-constructs into new storage): the + // allocators never compare equal, so the cheap + // pointer-steal path isn't available. + } + EXPECT_EQ(alloc_counter, 3u); + EXPECT_EQ(dealloc_counter, 3u); +} + +template +struct POCSTrackingAllocator : xyz::TrackingAllocator { + using xyz::TrackingAllocator::TrackingAllocator; + using propagate_on_container_swap = std::true_type; + + template + struct rebind { + using other = POCSTrackingAllocator; + }; +}; + +TEST(ProtocolReflectionSmoke, MemberSwapWhenAllocatorsDontCompareEqual) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + xyz::protocol> p( + std::allocator_arg, + POCSTrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type); + xyz::protocol> pp( + std::allocator_arg, + POCSTrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type, "pp"); + p.swap(pp); + EXPECT_EQ(p.name(), "pp"); + EXPECT_EQ(pp.name(), "ALike"); +} + +TEST(ProtocolReflectionSmoke, AllocatorExtendedCopyFromValueless) { + xyz::protocol p(std::in_place_type); + xyz::protocol pp(std::move(p)); + xyz::protocol ppp(std::allocator_arg, std::allocator(), p); + EXPECT_TRUE(ppp.valueless_after_move()); +} + +TEST(ProtocolReflectionSmoke, AllocatorExtendedMoveFromValueless) { + xyz::protocol p(std::in_place_type); + xyz::protocol pp(std::move(p)); + xyz::protocol ppp(std::allocator_arg, std::allocator(), + std::move(p)); + EXPECT_TRUE(ppp.valueless_after_move()); +} + } // namespace