Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
95 changes: 73 additions & 22 deletions protocol_reflection.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, Allocator> 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<T, Allocator> 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<T,...> 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<T,...>
// 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_

Expand Down Expand Up @@ -269,20 +266,34 @@ class protocol : public reflection_detail::protocol_bases<T, Allocator> {
const vtable* vtable_;
[[no_unique_address]] Allocator alloc_;

using allocator_traits = std::allocator_traits<Allocator>;

public:
template <class U, class... Ts>
explicit constexpr protocol(std::in_place_type_t<U>, Ts&&... ts)
explicit constexpr protocol(std::allocator_arg_t, const Allocator& alloc,
std::in_place_type_t<U>, Ts&&... ts)
requires std::same_as<std::remove_cvref_t<U>, U> &&
not_protocol_or_view<U> && std::constructible_from<U, Ts&&...> &&
std::copy_constructible<U> &&
std::default_initializable<Allocator> &&
reflection_detail::reflection_protocol_concept<U, T>
: alloc_() {
: alloc_(alloc) {
p_ = create_storage<U>(std::forward<Ts>(ts)...);
vtable_ = &vtable_impl<U>::vtable_;
}

constexpr protocol(const protocol& other) : alloc_(other.alloc_) {
template <class U, class... Ts>
explicit constexpr protocol(std::in_place_type_t<U>, Ts&&... ts)
requires std::same_as<std::remove_cvref_t<U>, U> &&
not_protocol_or_view<U> && std::constructible_from<U, Ts&&...> &&
std::copy_constructible<U> &&
std::default_initializable<Allocator> &&
reflection_detail::reflection_protocol_concept<U, T>
: protocol(std::allocator_arg_t{}, Allocator{}, std::in_place_type<U>,
std::forward<Ts>(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_;
Expand All @@ -292,11 +303,42 @@ class protocol : public reflection_detail::protocol_bases<T, Allocator> {
}
}

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() {
Expand All @@ -305,20 +347,29 @@ class protocol : public reflection_detail::protocol_bases<T, Allocator> {
}
}

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 {
Expand Down
159 changes: 159 additions & 0 deletions protocol_reflection_smoke_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <gtest/gtest.h>

#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
Expand All @@ -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 {

Expand Down Expand Up @@ -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_; }
Expand Down Expand Up @@ -138,4 +145,156 @@ TEST(ProtocolReflectionSmoke, DispatchesAllThreeMembersOfB) {
EXPECT_EQ(b.get_results(), (std::vector<int>{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<xyz::A, xyz::TrackingAllocator<std::byte>> a(
std::allocator_arg,
xyz::TrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
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<xyz::A, xyz::TrackingAllocator<std::byte>> a(
std::allocator_arg,
xyz::TrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
xyz::protocol<xyz::A, xyz::TrackingAllocator<std::byte>> 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<xyz::A, xyz::TrackingAllocator<std::byte>> a(
std::allocator_arg,
xyz::TrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
xyz::protocol<xyz::A, xyz::TrackingAllocator<std::byte>> 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<xyz::A, xyz::TrackingAllocator<std::byte>> a(
std::allocator_arg,
xyz::TrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
xyz::protocol<xyz::A, xyz::TrackingAllocator<std::byte>> aa(
std::allocator_arg,
xyz::TrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
aa = std::move(a);
}
EXPECT_EQ(alloc_counter, 2u);
EXPECT_EQ(dealloc_counter, 2u);
}

template <typename T>
struct NonEqualTrackingAllocator : xyz::TrackingAllocator<T> {
using xyz::TrackingAllocator<T>::TrackingAllocator;
using propagate_on_container_move_assignment = std::true_type;

template <typename Other>
struct rebind {
using other = NonEqualTrackingAllocator<Other>;
};

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<xyz::A, NonEqualTrackingAllocator<std::byte>> a(
std::allocator_arg,
NonEqualTrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
xyz::protocol<xyz::A, NonEqualTrackingAllocator<std::byte>> aa(
std::allocator_arg,
NonEqualTrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
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 <typename T>
struct POCSTrackingAllocator : xyz::TrackingAllocator<T> {
using xyz::TrackingAllocator<T>::TrackingAllocator;
using propagate_on_container_swap = std::true_type;

template <typename Other>
struct rebind {
using other = POCSTrackingAllocator<Other>;
};
};

TEST(ProtocolReflectionSmoke, MemberSwapWhenAllocatorsDontCompareEqual) {
unsigned alloc_counter = 0;
unsigned dealloc_counter = 0;
xyz::protocol<xyz::A, POCSTrackingAllocator<std::byte>> p(
std::allocator_arg,
POCSTrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
xyz::protocol<xyz::A, POCSTrackingAllocator<std::byte>> pp(
std::allocator_arg,
POCSTrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>, "pp");
p.swap(pp);
EXPECT_EQ(p.name(), "pp");
EXPECT_EQ(pp.name(), "ALike");
}

TEST(ProtocolReflectionSmoke, AllocatorExtendedCopyFromValueless) {
xyz::protocol<xyz::A> p(std::in_place_type<ALike>);
xyz::protocol<xyz::A> pp(std::move(p));
xyz::protocol<xyz::A> ppp(std::allocator_arg, std::allocator<std::byte>(), p);
EXPECT_TRUE(ppp.valueless_after_move());
}

TEST(ProtocolReflectionSmoke, AllocatorExtendedMoveFromValueless) {
xyz::protocol<xyz::A> p(std::in_place_type<ALike>);
xyz::protocol<xyz::A> pp(std::move(p));
xyz::protocol<xyz::A> ppp(std::allocator_arg, std::allocator<std::byte>(),
std::move(p));
EXPECT_TRUE(ppp.valueless_after_move());
}

} // namespace
Loading