From 65c7a77f64a52881981ebee84eb31610fe6e7db9 Mon Sep 17 00:00:00 2001 From: kelbon <58717435+kelbon@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:15:24 +0100 Subject: [PATCH] reuse memory in more situations (#72) * reuse memory in case of non propagatable alloc and in emplace / emplace operator= * disable throwing functions (any_cast) is exceptions disabled --- include/anyany/anyany.hpp | 343 +++++++++++++-------- include/anyany/noexport/anyany_details.hpp | 20 +- tests/test_anyany.cpp | 69 ++++- 3 files changed, 291 insertions(+), 141 deletions(-) diff --git a/include/anyany/anyany.hpp b/include/anyany/anyany.hpp index 78910b0..845974b 100644 --- a/include/anyany/anyany.hpp +++ b/include/anyany/anyany.hpp @@ -17,7 +17,6 @@ #include // assert #include // for any_cast(std::exception) #include // std::exchange -#include #include "type_descriptor.hpp" @@ -25,6 +24,20 @@ #include "noexport/file_begin.hpp" +// this attribute not supported correctly on clang (windows) and on msvc +// its literaly hell +// and EVEN with [[msvc::no_unique_address]] msvc does not work! +// https://github.com/microsoft/STL/issues/4411 +#ifdef __has_cpp_attribute +#if __has_cpp_attribute(no_unique_address) +#define ANYANY_NO_UNIQUE_ADDRESS [[no_unique_address]] +#elif __has_cpp_attribute(msvc::no_unique_address) +#define ANYANY_NO_UNIQUE_ADDRESS msvc::no_unique_address +#else +#define ANYANY_NO_UNIQUE_ADDRESS +#endif +#endif + namespace aa { // May be specialized for your Method or even for Any with some properties. @@ -56,17 +69,14 @@ constexpr inline bool is_const_method_v = method_traits::is_const; // pseudomethod is just a value, which is stored in vtable template -concept pseudomethod = std::is_empty_v && std::default_initializable && - requires { - typename T::value_type; - // T{}.template do_value(), - // where 'do_value' is a - // consteval function template and X - // - some type for which 'do_value' - // exist(not substitution failure) - } && - (!std::is_reference_v && - !std::is_function_v); +concept pseudomethod = std::is_empty_v && std::default_initializable && requires { + typename T::value_type; + // T{}.template do_value(), + // where 'do_value' is a + // consteval function template and X + // - some type for which 'do_value' + // exist(not substitution failure) +} && (!std::is_reference_v && !std::is_function_v); template concept regular_method = std::is_empty_v && std::default_initializable && @@ -262,10 +272,10 @@ constexpr inline auto default_any_soos = 64 - 3 * sizeof(void*); using default_allocator = std::allocator; -// enables copy/copy assgin/move/move assign for any_with -// enables 'materialize' for references -template -struct copy_with { +namespace noexport { + +template +struct copy_method { private: template static AA_CONSTEVAL_CPP20 auto* select_copy_fn() { @@ -302,13 +312,13 @@ struct copy_with { } }; -using copy = copy_with<>; +} // namespace noexport namespace noexport { auto get_any_copy_method(type_list<>) -> void; template -auto get_any_copy_method(type_list, Tail...>) -> copy_with; +auto get_any_copy_method(type_list, Tail...>) -> copy_method; template auto get_any_copy_method(type_list) { return get_any_copy_method(type_list{}); @@ -323,6 +333,13 @@ constexpr inline bool has_move = } // namespace noexport +// enables copy/copy assgin/move/move assign for any_with +// enables 'materialize' for references +template +using copy_with = noexport::copy_method, noexport::soo_buffer_size(SooS)>; + +using copy = copy_with<>; + // enables std::hash specialization for polymorphic value and reference struct hash { using signature_type = size_t(const erased_self_t&); @@ -849,14 +866,8 @@ struct ref : construct_interface<::aa::stateful::ref, Methods...> { private: void* value_ptr; -#if __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunknown-attributes" -#endif - [[no_unique_address]] vtable vtable_value; -#if __clang__ -#pragma clang diagnostic pop -#endif + ANYANY_NO_UNIQUE_ADDRESS vtable vtable_value; + friend struct ::aa::mate; template friend struct cref; @@ -922,14 +933,8 @@ struct cref : construct_interface<::aa::stateful::cref, Methods...> private: const void* value_ptr; -#if __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunknown-attributes" -#endif - [[no_unique_address]] vtable vtable_value; -#if __clang__ -#pragma clang diagnostic pop -#endif + ANYANY_NO_UNIQUE_ADDRESS vtable vtable_value; + friend struct ::aa::mate; constexpr cref() noexcept = default; @@ -1095,43 +1100,26 @@ struct unreachable_allocator { } }; +// dont use it directly, instead use any_with / basic_any_with // SooS == Small Object Optimization Size -// strong exception guarantee for all constructors and assignments, +// strong exception guarantee for constructors and copy/move assignments // emplace - *this is empty if exception thrown -// for alloc not all fancy pointers supported and construct / destroy not throught alloc // if SooS == 0, value is always allocated, is_stable_pointers() == true template struct basic_any : construct_interface, Methods...> { using aa_polymorphic_tag = int; private: - static AA_CONSTEVAL_CPP20 size_t soo_buffer_size() noexcept { - // needs atleast sizeof(std::size_t) in buffer(SooS) - // to store allocated size for passing it into deallocate(ptr, n) - static_assert(SooS == 0 || SooS >= sizeof(size_t), - "basic_any requires SooS to be >= sizeof(size_t) or 0"); - return SooS == 0 ? sizeof(size_t) : SooS; - } + static_assert(SooS == 0 || SooS >= sizeof(size_t)); const vtable* vtable_ptr = nullptr; void* value_ptr = data; union { - alignas(std::max_align_t) std::byte data[soo_buffer_size()]; + // if SooS == 0, then union degenerates to just `size_t` + alignas(SooS == 0 ? alignof(size_t) : alignof(std::max_align_t)) std::byte data[SooS]; size_t size_allocated; // stored when value allocated }; -#if __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunknown-attributes" -#endif - [[no_unique_address]] Alloc alloc; -#if __clang__ -#pragma clang diagnostic pop -#endif - - // invariant of basic_any - it is always in one of those states: - // empty - has_value() == false, memory_allocated == false - // small - has_value() == true, memory_allocated == false, MOVE IS NOEXCEPT - // big - has_value() == true, memory_allocated == true, + ANYANY_NO_UNIQUE_ADDRESS Alloc alloc; // guarantees that small is nothrow movable(for noexcept move ctor/assign) template @@ -1139,24 +1127,24 @@ struct basic_any : construct_interface, Metho // precondition: has_value() == false template void emplace_in_empty(Args&&... args) { + static_assert(std::is_same_v>); if constexpr (any_is_small_for && std::is_void_v) { - alloc_traits::construct(alloc, reinterpret_cast(&data[0]), std::forward(args)...); + construct_value(std::forward(args)...); } else { constexpr size_t allocation_size = sizeof(T); value_ptr = alloc.allocate(allocation_size); size_allocated = allocation_size; if constexpr (std::is_nothrow_constructible_v) { - alloc_traits::construct(alloc, reinterpret_cast(value_ptr), std::forward(args)...); + construct_value(std::forward(args)...); } else { scope_failure free_memory{[&] { alloc.deallocate(reinterpret_cast(value_ptr), allocation_size); value_ptr = data; }}; - alloc_traits::construct(alloc, reinterpret_cast(value_ptr), std::forward(args)...); + construct_value(std::forward(args)...); free_memory.no_longer_needed(); } } - vtable_ptr = addr_vtable_for; } using alloc_traits = std::allocator_traits; @@ -1202,8 +1190,7 @@ struct basic_any : construct_interface, Metho constexpr basic_any() = default; AA_IF_HAS_CPP20(constexpr) ~basic_any() { - if (has_value()) - destroy_value(); + reset(); } // basic_any copy/move stuff @@ -1231,20 +1218,34 @@ struct basic_any : construct_interface, Metho } basic_any& operator=(basic_any&& other) noexcept(movable_alloc()) ANYANY_LIFETIMEBOUND AA_IF_HAS_CPP20(requires(has_move)) { - // nocheck about this == &other - // because after move assign other by C++ standard in unspecified(valid) state - reset(); + if (this == std::addressof(other)) [[unlikely]] + return *this; if constexpr (alloc_traits::is_always_equal::value) { + reset(); move_value_from(other); } else if constexpr (alloc_traits::propagate_on_container_move_assignment::value) { + reset(); if (alloc != other.alloc) alloc = std::move(other.alloc); move_value_from(other); } else { // not propagatable alloc - if (alloc != other.alloc) + destroy_value(); + if (alloc != other.alloc) { + if (other.has_value() && other.memory_allocated() && memory_allocated() && + size_allocated >= other.size_allocated) { + // reuse allocated memory + // if exception thrown here, *this is empty + other.get_move_fn()(other.value_ptr, value_ptr); + vtable_ptr = std::exchange(other.vtable_ptr, nullptr); + return *this; + } + if (memory_allocated()) + deallocate_memory(); move_value_from(other); - else + } else { + reset(); move_value_from(other); + } } return *this; } @@ -1261,68 +1262,61 @@ struct basic_any : construct_interface, Metho *this = std::move(value); return *this; } - // has strong exception guarantee - // you can use .emplace() without exception guarantees - // and without any requirements - template , exist_for> && - (Self::has_move || (std::is_nothrow_constructible_v, V&&> && - any_is_small_for>))), - int> = 0> - basic_any& operator=(V&& val) ANYANY_LIFETIMEBOUND { - if constexpr (std::is_nothrow_constructible_v, V&&> && - any_is_small_for>) { - reset(); - emplace_in_empty>(std::forward(val)); - } else { - *this = basic_any{std::forward(val)}; - } - return *this; + // * if exception thrown, *this is empty + // for exception guarantee use move assign lik e'any = any_t(val)' + // * disables implicit creating any in any, like any = other_any, + // if you really need this, use .emplace + // * returns reference to emplaced value (not *this!) + // for cases like any1 = any2 = 5; (any1 = int is better then any1 = any) + template , exist_for>, int> = 0> + std::decay_t& operator=(V&& val) ANYANY_LIFETIMEBOUND { + return emplace(std::forward(val)); } // making from any other type - // postconditions : has_value() == true, *this is empty if exception thrown + // postcondition: has_value() == true, *this is empty if exception thrown template ::value, int> = 0> std::decay_t& emplace(Args&&... args) noexcept( - std::is_nothrow_constructible_v, Args&&...>&& any_is_small_for>) - ANYANY_LIFETIMEBOUND { - reset(); - emplace_in_empty>(std::forward(args)...); - return *reinterpret_cast*>(value_ptr); + std::is_nothrow_constructible_v, Args&&...> && + any_is_small_for>) ANYANY_LIFETIMEBOUND { + // decay T, so less function instantiated (emplace same fn) + return emplace_decayed>(std::forward(args)...); } template ::value, int> = 0> std::decay_t& emplace(std::initializer_list list, Args&&... args) noexcept( - std::is_nothrow_constructible_v, std::initializer_list, Args&&...>&& - any_is_small_for>) ANYANY_LIFETIMEBOUND { - reset(); - emplace_in_empty>(list, std::forward(args)...); - return *reinterpret_cast*>(value_ptr); + std::is_nothrow_constructible_v, std::initializer_list, Args&&...> && + any_is_small_for>) ANYANY_LIFETIMEBOUND { + return emplace, std::initializer_list, Args...>(std::move(list), + std::forward(args)...); } template ::value, int> = 0> - basic_any(std::in_place_type_t, Args&&... args) noexcept( - std::is_nothrow_constructible_v, Args&&...>&& any_is_small_for>) { + basic_any(std::in_place_type_t, + Args&&... args) noexcept(std::is_nothrow_constructible_v, Args&&...> && + any_is_small_for>) { emplace_in_empty>(std::forward(args)...); } template ::value, int> = 0> basic_any(std::in_place_type_t, std::initializer_list list, Args&&... args) noexcept( - std::is_nothrow_constructible_v, std::initializer_list, Args&&...>&& - any_is_small_for>) { + std::is_nothrow_constructible_v, std::initializer_list, Args&&...> && + any_is_small_for>) { emplace_in_empty>(list, std::forward(args)...); } template , exist_for>, int> = 0> - basic_any(T&& value) noexcept( - std::is_nothrow_constructible_v, T&&>&& any_is_small_for>) + basic_any(T&& value) noexcept(std::is_nothrow_constructible_v, T&&> && + any_is_small_for>) : basic_any(std::in_place_type>, std::forward(value)) { } constexpr basic_any(std::allocator_arg_t, Alloc alloc) noexcept : alloc(std::move(alloc)) { } template ::value, int> = 0> - basic_any(std::allocator_arg_t, Alloc alloc, T&& value) noexcept( - std::is_nothrow_constructible_v, T&&>&& any_is_small_for>) + basic_any(std::allocator_arg_t, Alloc alloc, + T&& value) noexcept(std::is_nothrow_constructible_v, T&&> && + any_is_small_for>) : alloc(std::move(alloc)) { emplace_in_empty>(std::forward(value)); } @@ -1340,9 +1334,10 @@ struct basic_any : construct_interface, Metho if constexpr (!noexport::copy_requires_alloc()) { value_ptr = invoke>(other).copy_fn(mate::get_value_ptr(other), value_ptr); } else { - value_ptr = invoke>(other).copy_fn(mate::get_value_ptr(other), value_ptr, alloc); + value_ptr = invoke>(other).copy_fn(mate::get_value_ptr(other), value_ptr, + std::addressof(alloc)); } - vtable_ptr = subtable_ptr(mate::get_vtable_ptr(other)); + vtable_ptr = subtable_ptr(other.vtable_ptr); } template && @@ -1355,33 +1350,58 @@ struct basic_any : construct_interface, Metho // force allocate versions template ::value, int> = 0> - basic_any(force_stable_pointers_t, std::in_place_type_t, Args&&... args) noexcept( - std::is_nothrow_constructible_v, Args&&...>&& any_is_small_for>) { + basic_any(force_stable_pointers_t, std::in_place_type_t, + Args&&... args) noexcept(std::is_nothrow_constructible_v, Args&&...> && + any_is_small_for>) { emplace_in_empty, force_stable_pointers_t>(std::forward(args)...); } template ::value, int> = 0> basic_any(force_stable_pointers_t, std::in_place_type_t, std::initializer_list list, Args&&... args) noexcept(std::is_nothrow_constructible_v, - std::initializer_list, Args&&...>&& - any_is_small_for>) { + std::initializer_list, Args&&...> && + any_is_small_for>) { emplace_in_empty, force_stable_pointers_t>(list, std::forward(args)...); } template , exist_for>, int> = 0> - basic_any(force_stable_pointers_t, T&& value) noexcept( - std::is_nothrow_constructible_v, T&&>&& any_is_small_for>) + basic_any(force_stable_pointers_t, + T&& value) noexcept(std::is_nothrow_constructible_v, T&&> && + any_is_small_for>) : basic_any(force_stable_pointers, std::in_place_type>, std::forward(value)) { } - // postconditions : has_value() == false + // postcondition : !has_value() + // also deallocates memory void reset() noexcept { + destroy_value(); + if (memory_allocated()) + deallocate_memory(); + } + // postcondition : !has_value() + // do not deallocates memory + void destroy_value() noexcept { if (!has_value()) return; - destroy_value(); + invoke (*this)(value_ptr); vtable_ptr = nullptr; } + // postcondition: capacity() >= bytes + void replace_with_bytes(size_t bytes) { + destroy_value(); + if (capacity() >= bytes) + return; + auto* new_place = alloc.allocate(bytes); + if (memory_allocated()) + deallocate_memory(); + value_ptr = new_place; + size_allocated = bytes; + } + constexpr size_t capacity() const noexcept { + return memory_allocated() ? size_allocated : SooS; + } + // observe constexpr explicit operator bool() const noexcept { @@ -1404,7 +1424,7 @@ struct basic_any : construct_interface, Metho constexpr size_t sizeof_now() const noexcept { if (!has_value()) return 0; - return memory_allocated() ? allocated_size() : SooS; + return memory_allocated() ? size_allocated : SooS; } private: @@ -1414,9 +1434,37 @@ struct basic_any : construct_interface, Metho else return invoke>(*this).move_fn; } - // precodition - has_value() == false + + // postcondition: has_value() == true, *this is empty if exception thrown + template ::value, int> = 0> + T& emplace_decayed(Args&&... args) noexcept(std::is_nothrow_constructible_v && + any_is_small_for) { + static_assert(std::is_same_v>); + destroy_value(); + if (!memory_allocated()) + goto do_emplace_in_empty; + if (size_allocated < sizeof(T)) { + deallocate_memory(); + do_emplace_in_empty: + emplace_in_empty(std::forward(args)...); + } else { + construct_value(std::forward(args)...); + } + return *reinterpret_cast(value_ptr); + } + + // postcondition: has_value() + template + constexpr void construct_value(Args&&... args) noexcept(std::is_nothrow_constructible_v) { + static_assert(std::is_same_v>); + alloc_traits::construct(alloc, static_cast(value_ptr), std::forward(args)...); + vtable_ptr = addr_vtable_for; + } + + // precodition: !has_value() && !memory_allocated() template void move_value_from(basic_any& other) noexcept(MemoryMaybeReused) { + assert(!has_value() && !memory_allocated()); if (!other.has_value()) return; if constexpr (SooS == 0) { @@ -1450,18 +1498,12 @@ struct basic_any : construct_interface, Metho constexpr bool memory_allocated() const noexcept { return value_ptr != data; } - constexpr size_t allocated_size() const noexcept { - assert(has_value() && memory_allocated()); - // when allocates stores in size in unused buffer - return size_allocated; - } - // precondition - has_value() == true - void destroy_value() noexcept { - invoke (*this)(value_ptr); - if (memory_allocated()) { - alloc_traits::deallocate(alloc, reinterpret_cast(value_ptr), allocated_size()); - value_ptr = data; - } + + // preconditions: !has_value() && memory_allocated() + void deallocate_memory() noexcept { + assert(!has_value() && memory_allocated()); + alloc_traits::deallocate(alloc, reinterpret_cast(value_ptr), size_allocated); + value_ptr = data; } }; @@ -1486,7 +1528,7 @@ auto materialize(const_poly_ref ref, Alloc alloc = Alloc{}) #define AA_DECLARE_MATERIALIZE(TEMPLATE, TRANSFORM) \ template \ AA_ALWAYS_INLINE auto materialize(const TEMPLATE& value, Alloc alloc = Alloc{}) \ - ->decltype(materialize(TRANSFORM, std::move(alloc))) { \ + -> decltype(materialize(TRANSFORM, std::move(alloc))) { \ return materialize(TRANSFORM, std::move(alloc)); \ } AA_DECLARE_MATERIALIZE(poly_ref, const_poly_ref(value)) @@ -1517,7 +1559,7 @@ struct any_cast_fn { return reinterpret_cast(Traits{}.to_address(val)); } }; - +// TODO disable // may be throwed when casting not Any* / poly_ptr struct bad_cast : std::exception { const char* what() const noexcept override { @@ -1557,9 +1599,9 @@ struct any_cast_fn { const X* operator()(const U* ptr) const noexcept { return any_cast_impl(static_cast(ptr)); } - +#if __cpp_exceptions template ::value, int> = 0> - decltype(auto) operator()(U&& any) const { + decltype(auto) operator()(U && any) const { auto* ptr = (*this)(std::addressof(any)); if (!ptr) throw aa::bad_cast{}; @@ -1576,6 +1618,13 @@ struct any_cast_fn { else return noexport::remove_cvref_t(*ptr); // copy value } +#else + template ::value, int> = 0> + decltype(auto) operator()(U && any) const { + static_assert( + ![] {}, "exceptions disabled, use nothrow version"); + } +#endif template const X* operator()(const_poly_ptr p) const noexcept { @@ -1589,6 +1638,7 @@ struct any_cast_fn { return nullptr; return reinterpret_cast(p.raw()); } +#if __cpp_exceptions template std::conditional_t, noexport::remove_cvref_t, std::conditional_t, T, std::remove_cv_t>> @@ -1598,6 +1648,16 @@ struct any_cast_fn { throw aa::bad_cast{}; return *ptr; } +#else + template + std::conditional_t, noexport::remove_cvref_t, + std::conditional_t, T, std::remove_cv_t>> + operator()(poly_ref p) const { + static_assert( + ![] {}, "exceptions disabled, use nothrow version"); + } +#endif +#if __cpp_exceptions // clang-format off template std::conditional_t, const X&, std::remove_cv_t> @@ -1608,6 +1668,15 @@ struct any_cast_fn { throw aa::bad_cast{}; return *ptr; } +#else + template + std::conditional_t, const X&, std::remove_cv_t> operator()( + const_poly_ref p) const { + static_assert( + ![] {}, "exceptions disabled, use nothrow version"); + } +#endif + template decltype(auto) operator()(const stateful::ref& r) const { return (*this)(r.get_view()); @@ -1642,6 +1711,10 @@ auto flatten_into_basic_any(type_list) { return insert_into_basic_any(flatten_types_t{}); } +template +using flatten_into_basic_any_t = + typename decltype(flatten_into_basic_any(type_list{}))::type; + template