diff --git a/src/utility/small_unique_ptr.hpp b/src/utility/small_unique_ptr.hpp new file mode 100644 index 00000000..ea07ff5d --- /dev/null +++ b/src/utility/small_unique_ptr.hpp @@ -0,0 +1,319 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#ifndef GA_UTILITY_SMALL_UNIQUE_PTR_HPP +#define GA_UTILITY_SMALL_UNIQUE_PTR_HPP + +#include "type_traits.hpp" +#include "utility.hpp" +#include +#include +#include +#include +#include + +namespace gapp::detail +{ + template + inline constexpr size_t buffer_size_v = 64 - sizeof(T*) - sizeof(void(*)()); + + // should be inside small_unique_ptr + template + inline constexpr bool always_heap_allocated_v = (sizeof(T) > buffer_size_v) || (!std::is_nothrow_move_constructible_v && !std::is_abstract_v); + + + template> + class small_ptr_storage + { + public: + constexpr small_ptr_storage() noexcept {} + constexpr ~small_ptr_storage() noexcept {} + + small_ptr_storage(const small_ptr_storage&) = delete; + small_ptr_storage(small_ptr_storage&&) = delete; + + small_ptr_storage& operator=(const small_ptr_storage&) = delete; + small_ptr_storage& operator=(small_ptr_storage&&) = delete; + + constexpr T* buffer() noexcept + { + if constexpr (!std::is_abstract_v) return std::addressof(buffer_); + else return reinterpret_cast(std::addressof(buffer_)); + } + + constexpr const T* buffer() const noexcept + { + if constexpr (!std::is_abstract_v) return std::addressof(buffer_); + else return reinterpret_cast(std::addressof(buffer_)); + } + + template + constexpr void move_buffer_to(small_ptr_storage& dst) noexcept + { + move_(buffer(), dst.buffer()); + dst.move_ = move_; + } + + using buffer_t = std::conditional_t, unsigned char[sizeof(T)], T>; + using move_t = void(*)(void* src, void* dst) noexcept; + + move_t move_ = nullptr; + union + { + unsigned char padding_[buffer_size_v]; + alignas(T) buffer_t buffer_; + }; + }; + + template + class small_ptr_storage + {}; + + + template // TODO: move CACHE_LINE_SIZE constant from small_vector to utility and use that here too + class small_unique_ptr : private small_ptr_storage> + { + public: + static_assert(!std::is_array_v && !std::is_void_v); + + using element_type = T; + using pointer = T*; + using reference = T&; + + constexpr small_unique_ptr() noexcept = default; + constexpr small_unique_ptr(std::nullptr_t) noexcept {} + + constexpr small_unique_ptr(small_unique_ptr&& other) noexcept : + small_unique_ptr(std::move(other), detail::empty_t{}) + {} + + template + constexpr small_unique_ptr(small_unique_ptr&& other, detail::empty_t = {}) noexcept + { + static_assert(!detail::is_proper_base_of_v || std::has_virtual_destructor_v); + + if (!other.is_local() || std::is_constant_evaluated()) + { + data_ = std::exchange(other.data_, nullptr); + return; + } + //if constexpr (!always_heap_allocated_v && !std::has_virtual_destructor_v) + //{ + // std::construct_at(this->buffer(), std::move(*other.buffer())); + // data_ = this->buffer(); + // other.reset(); + //} + if constexpr (!always_heap_allocated_v) // other.is_local() + { + other.move_buffer_to(*this); + data_ = reinterpret_cast(reinterpret_cast(this->buffer()) + offsetof_base(other)); + other.reset(); + } + } + + constexpr small_unique_ptr& operator=(small_unique_ptr&& other) noexcept + { + if (std::addressof(other) == this) [[unlikely]] return *this; + + return operator=(std::move(other)); + } + + template + constexpr small_unique_ptr& operator=(small_unique_ptr&& other) noexcept + { + static_assert(!detail::is_proper_base_of_v || std::has_virtual_destructor_v); + + if (!other.is_local() || std::is_constant_evaluated()) + { + reset(std::exchange(other.data_, nullptr)); + return *this; + } + if constexpr (!always_heap_allocated_v) // other.is_local() + { + const ptrdiff_t old_offset = offsetof_this(); + reset(); + other.move_buffer_to(*this); + data_ = reinterpret_cast(reinterpret_cast(this->buffer()) + offsetof_base(other) + old_offset); + other.reset(); + } + return *this; + } + + constexpr small_unique_ptr& operator=(std::nullptr_t) noexcept + { + reset(); + return *this; + } + + constexpr ~small_unique_ptr() noexcept + { + reset(); + } + + constexpr void reset(pointer new_data = pointer{}) noexcept + { + is_local() ? std::destroy_at(data_) : delete data_; + data_ = new_data; + if constexpr (!always_heap_allocated_v) this->move_ = nullptr; // maybe add reset() method to buffer? + } + + constexpr void swap(small_unique_ptr& other) noexcept requires(always_heap_allocated_v) + { + std::swap(data_, other.data_); + } + + constexpr void swap(small_unique_ptr& other) noexcept requires(!always_heap_allocated_v) + { + if (is_local() && other.is_local()) + { + T* new_this_data = reinterpret_cast(reinterpret_cast(this->buffer()) + other.offsetof_this()); + T* new_other_data = reinterpret_cast(reinterpret_cast(other.buffer()) + offsetof_this()); + + small_ptr_storage temp; + + other.move_buffer_to(temp); + std::destroy_at(other.buffer()); + this->move_buffer_to(other); + std::destroy_at(this->buffer()); + temp.move_buffer_to(*this); + std::destroy_at(temp.buffer()); + + this->data_ = new_this_data; + other.data_ = new_other_data; + } + else if (!is_local() && !other.is_local()) + { + std::swap(data_, other.data_); + } + else if (!is_local() && other.is_local()) + { + T* new_data = reinterpret_cast(reinterpret_cast(this->buffer()) + other.offsetof_this()); + other.move_buffer_to(*this); + other.reset(std::exchange(data_, new_data)); + } + else /* if (is_local() && !other.is_local()) */ + { + T* new_data = reinterpret_cast(reinterpret_cast(other.buffer()) + offsetof_this()); + this->move_buffer_to(other); + reset(std::exchange(other.data_, new_data)); + } + } + + [[nodiscard]] + constexpr pointer get() const noexcept + { + return data_; + } + + [[nodiscard]] + constexpr explicit operator bool() const noexcept + { + return bool(data_); + } + + [[nodiscard]] + constexpr reference operator*() const noexcept(detail::is_nothrow_dereferenceable_v) + { + GAPP_ASSERT(data_); + return *data_; + } + + [[nodiscard]] + constexpr pointer operator->() const noexcept + { + GAPP_ASSERT(data_); + return data_; + } + + constexpr bool operator==(std::nullptr_t) const noexcept + { + return data_ == pointer{ nullptr }; + } + + constexpr std::strong_ordering operator<=>(std::nullptr_t) const noexcept + { + return data_ <=> pointer{ nullptr }; + } + + template + constexpr bool operator==(const small_unique_ptr& rhs) const noexcept + { + return data_ == rhs.data_; + } + + template + constexpr std::strong_ordering operator<=>(const small_unique_ptr& rhs) const noexcept + { + return data_ <=> rhs.data_; + } + + private: + pointer data_ = nullptr; + + constexpr bool is_local() const noexcept requires(!always_heap_allocated_v) { return this->move_ != nullptr; } + constexpr bool is_local() const noexcept requires(always_heap_allocated_v) { return false; } + + template + constexpr ptrdiff_t offsetof_base(const small_unique_ptr& other) const noexcept + { + if constexpr (!detail::is_proper_base_of_v) return 0; + else + { + const auto derived_ptr = reinterpret_cast(other.get()); + const auto base_ptr = reinterpret_cast(static_cast(other.get())); + + return base_ptr - derived_ptr; + } + } + + constexpr ptrdiff_t offsetof_this() const noexcept + { + if (!is_local() || !data_) return 0; + + const auto derived_ptr = reinterpret_cast(this->buffer()); + const auto base_ptr = reinterpret_cast(data_); + + return base_ptr - derived_ptr; + } + + template + friend class small_unique_ptr; + + template + friend constexpr small_unique_ptr make_unique_small(Args&&...); + }; + + template + constexpr void swap(small_unique_ptr& lhs, small_unique_ptr& rhs) noexcept + { + lhs.swap(rhs); + } + + + template + void move_buffer(void* src, void* dst) noexcept + { + std::construct_at(static_cast(dst), std::move(*static_cast(src))); + } + + template + [[nodiscard]] constexpr small_unique_ptr make_unique_small(Args&&... args) + { + small_unique_ptr ptr; + + if constexpr (always_heap_allocated_v || std::is_constant_evaluated()) + { + ptr.data_ = new T(std::forward(args)...); + } + else + { + std::construct_at(ptr.buffer(), std::forward(args)...); + ptr.data_ = ptr.buffer(); + ptr.move_ = move_buffer; + } + + return ptr; + } + +} // namespace gapp::detail + +#endif // !GA_UTILITY_SMALL_UNIQUE_PTR_HPP \ No newline at end of file diff --git a/src/utility/type_traits.hpp b/src/utility/type_traits.hpp index 44eba355..6e382b7e 100644 --- a/src/utility/type_traits.hpp +++ b/src/utility/type_traits.hpp @@ -92,6 +92,17 @@ namespace gapp::detail + template + struct is_proper_base_of : + std::conjunction, std::remove_cv>, + std::negation, std::remove_cv>>> + {}; + + template + inline constexpr bool is_proper_base_of_v = is_proper_base_of::value; + + + template class BaseTempl> struct is_derived_from_spec_of { @@ -161,6 +172,13 @@ namespace gapp::detail using promoted_t = typename promoted::type; + + template + struct is_nothrow_dereferenceable : std::bool_constant())> {}; + + template + inline constexpr bool is_nothrow_dereferenceable_v = is_nothrow_dereferenceable::value; + } // namespace gapp::detail #endif // !GA_UTILITY_TYPE_TRAITS_HPP \ No newline at end of file diff --git a/test/unit/small_unique_ptr.cpp b/test/unit/small_unique_ptr.cpp new file mode 100644 index 00000000..c640c43d --- /dev/null +++ b/test/unit/small_unique_ptr.cpp @@ -0,0 +1,278 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#include +#include "utility/small_unique_ptr.hpp" +#include + +using namespace gapp::detail; + +struct Base +{ + constexpr virtual int value() const { return 0; } + constexpr virtual void value(int) {} + constexpr virtual int padding() const { return 0; } + constexpr virtual ~Base() = default; +}; + +template +struct Derived : Base +{ +public: + constexpr Derived() = default; + constexpr Derived(int n) noexcept : value_(n) {} + constexpr ~Derived() override = default; + constexpr int value() const override { return value_; } + constexpr void value(int n) override { value_ = n; } + constexpr int padding() const override { return Padding; } +private: + unsigned char padding_[Padding] = {}; + int value_ = Padding; +}; + +using SmallDerived = Derived<32>; +using LargeDerived = Derived<64>; + + +TEST_CASE("ptr_size", "[small_unique_ptr]") +{ + STATIC_REQUIRE(sizeof(small_unique_ptr) == 64); // could be exactly sizeof(int) for non-polymorphic types + STATIC_REQUIRE(sizeof(small_unique_ptr) == 64); + STATIC_REQUIRE(sizeof(small_unique_ptr) == sizeof(LargeDerived*)); +} + +TEST_CASE("construction", "[small_unique_ptr]") +{ + STATIC_REQUIRE( std::invoke([]{ (void) small_unique_ptr(nullptr); return true; }) ); + STATIC_REQUIRE( std::invoke([]{ (void) small_unique_ptr(); return true; }) ); + STATIC_REQUIRE( std::invoke([]{ (void) small_unique_ptr(); return true; }) ); + + STATIC_REQUIRE( std::invoke([]{ (void) make_unique_small(3.0); return true; }) ); + STATIC_REQUIRE( std::invoke([]{ (void) make_unique_small(); return true; }) ); + STATIC_REQUIRE( std::invoke([]{ (void) make_unique_small(); return true; }) ); + STATIC_REQUIRE( std::invoke([]{ (void) make_unique_small(); return true; }) ); + + REQUIRE_NOTHROW(make_unique_small()); + REQUIRE_NOTHROW(make_unique_small()); + REQUIRE_NOTHROW(make_unique_small()); +} + +TEST_CASE("comparisons", "[small_unique_ptr]") +{ + STATIC_REQUIRE(small_unique_ptr(nullptr) == nullptr); + STATIC_REQUIRE(small_unique_ptr(nullptr) <= nullptr); + + STATIC_REQUIRE(make_unique_small() != nullptr); + STATIC_REQUIRE(make_unique_small() != nullptr); + + STATIC_REQUIRE(make_unique_small() != make_unique_small()); + STATIC_REQUIRE(make_unique_small() != make_unique_small()); +} + +TEST_CASE("bool_conversion", "[small_unique_ptr]") +{ + STATIC_REQUIRE(!small_unique_ptr()); + STATIC_REQUIRE(!small_unique_ptr(nullptr)); + + STATIC_REQUIRE(make_unique_small()); + STATIC_REQUIRE(make_unique_small()); + + REQUIRE(make_unique_small()); + REQUIRE(make_unique_small()); +} + +TEST_CASE("get", "[small_unique_ptr]") +{ + STATIC_REQUIRE(small_unique_ptr(nullptr).get() == nullptr); + STATIC_REQUIRE(small_unique_ptr().get() == nullptr); + + STATIC_REQUIRE(make_unique_small().get() != nullptr); + STATIC_REQUIRE(make_unique_small().get() != nullptr); + + REQUIRE(make_unique_small().get() != nullptr); + REQUIRE(make_unique_small().get() != nullptr); +} + +TEST_CASE("dereference", "[small_unique_ptr]") +{ + STATIC_REQUIRE((*make_unique_small()).padding() == 32); + STATIC_REQUIRE((*make_unique_small()).padding() == 64); + + STATIC_REQUIRE(make_unique_small()->padding() == 32); + STATIC_REQUIRE(make_unique_small()->padding() == 64); + + const auto p1 = make_unique_small(); + const auto p2 = make_unique_small(); + + REQUIRE((*p1).value() == 32); + REQUIRE((*p2).value() == 64); + + REQUIRE(p1->value() == 32); + REQUIRE(p2->value() == 64); +} + +TEST_CASE("move_construct_plain", "[small_unique_ptr]") +{ + STATIC_REQUIRE(32 == std::invoke([] { small_unique_ptr p = make_unique_small(); return p->padding(); })); + STATIC_REQUIRE(64 == std::invoke([] { small_unique_ptr p = make_unique_small(); return p->padding(); })); + + small_unique_ptr base1 = make_unique_small(); + small_unique_ptr base2 = make_unique_small(); + + REQUIRE(base1->value() == 32); + REQUIRE(base2->value() == 64); + + small_unique_ptr cbase = std::move(base1); + + REQUIRE(cbase->value() == 32); +} + +TEST_CASE("move_assignment_plain", "[small_unique_ptr]") +{ + STATIC_REQUIRE(32 == std::invoke([] { small_unique_ptr p; p = make_unique_small(); return p->padding(); })); + STATIC_REQUIRE(64 == std::invoke([] { small_unique_ptr p; p = make_unique_small(); return p->padding(); })); + + small_unique_ptr base; + + base = make_unique_small(); + REQUIRE(base->value() == 32); + + base = make_unique_small(); + REQUIRE(base->value() == 64); + + base = nullptr; + REQUIRE(!base); +} + +TEST_CASE("swap_large", "[small_unique_ptr]") +{ + small_unique_ptr p1 = nullptr; + small_unique_ptr p2 = make_unique_small(); + + using std::swap; + swap(p1, p2); + + REQUIRE(p2 == nullptr); + REQUIRE(p1->value() == 64); +} + +TEST_CASE("swap_mixed", "[small_unique_ptr]") +{ + using std::swap; + + small_unique_ptr p1 = make_unique_small(); + small_unique_ptr p2 = make_unique_small(); + + REQUIRE(p1->value() == 32); + REQUIRE(p2->value() == 64); + + swap(p1, p2); + + REQUIRE(p1->value() == 64); + REQUIRE(p2->value() == 32); + + swap(p1, p2); + + REQUIRE(p1->value() == 32); + REQUIRE(p2->value() == 64); +} + +TEST_CASE("swap_small", "[small_unique_ptr]") +{ + small_unique_ptr p1 = make_unique_small(1); + small_unique_ptr p2 = make_unique_small(2); + + using std::swap; + swap(p1, p2); + + REQUIRE(p1->value() == 2); + REQUIRE(p2->value() == 1); + + swap(p1, p2); + + REQUIRE(p1->value() == 1); + REQUIRE(p2->value() == 2); +} + +TEST_CASE("swap_constexpr", "[small_unique_ptr]") +{ + using std::swap; + + STATIC_REQUIRE(32 == std::invoke([] + { + small_unique_ptr p1 = make_unique_small(); + small_unique_ptr p2 = make_unique_small(); + + swap(p1, p2); + + return p2->padding(); + })); + + STATIC_REQUIRE(32 == std::invoke([] + { + small_unique_ptr p1 = make_unique_small(); + small_unique_ptr p2 = make_unique_small(); + + swap(p1, p2); + + return p2->padding(); + })); + + STATIC_REQUIRE(64 == std::invoke([] + { + small_unique_ptr p1 = make_unique_small(); + small_unique_ptr p2 = make_unique_small(); + + swap(p1, p2); + + return p2->padding(); + })); +} + + +struct VBase +{ + int n = -1; + virtual int value() const = 0; + virtual ~VBase() = default; +}; + +struct A : virtual VBase { int a = 0; int value() const override { return a; } }; +struct B : virtual VBase { int b = 1; int value() const override { return b; } }; +struct C : virtual VBase { int c = 2; int value() const override { return c; } }; +struct D : virtual VBase { int d = 3; int value() const override { return d; } }; +struct E : A, B { int e = 4; int value() const override { return e; } }; +struct F : C, D { int f = 5; int value() const override { return f; } }; +struct G : E, F { int g = 6; int value() const override { return g; } }; + + +TEST_CASE("move_construct_multi", "[small_unique_ptr]") +{ + small_unique_ptr leaf = make_unique_small(); + REQUIRE(leaf->value() == 6); + + small_unique_ptr derived = std::move(leaf); + REQUIRE(derived->value() == 6); + + small_unique_ptr base = std::move(derived); + REQUIRE(base->value() == 6); + + small_unique_ptr cbase = std::move(base); + REQUIRE(cbase->value() == 6); +} + +TEST_CASE("move_assignment_multi", "[small_unique_ptr]") +{ + small_unique_ptr p1; + + p1 = make_unique_small(); + REQUIRE(p1->value() == 1); + + p1 = make_unique_small(); + REQUIRE(p1->value() == 6); + + p1 = make_unique_small(); + REQUIRE(p1->value() == 4); + + p1 = make_unique_small(); + REQUIRE(p1->value() == 1); +}