From aefea967d5f49fd714d0cd3d6083014d2c610a29 Mon Sep 17 00:00:00 2001 From: robot-piglet Date: Wed, 4 Dec 2024 12:40:18 +0300 Subject: Intermediate changes commit_hash:cbf5ab65e5ba1b2d286f974a6b6b463ed152b381 --- library/cpp/yt/memory/type_erasure.h | 418 ++++++++++++ library/cpp/yt/memory/type_erasure_detail.h | 710 +++++++++++++++++++++ .../cpp/yt/memory/unittests/type_erasure_ut.cpp | 333 ++++++++++ library/cpp/yt/memory/unittests/ya.make | 1 + 4 files changed, 1462 insertions(+) create mode 100644 library/cpp/yt/memory/type_erasure.h create mode 100644 library/cpp/yt/memory/type_erasure_detail.h create mode 100644 library/cpp/yt/memory/unittests/type_erasure_ut.cpp (limited to 'library/cpp/yt') diff --git a/library/cpp/yt/memory/type_erasure.h b/library/cpp/yt/memory/type_erasure.h new file mode 100644 index 00000000000..92c4eb34c54 --- /dev/null +++ b/library/cpp/yt/memory/type_erasure.h @@ -0,0 +1,418 @@ +#pragma once + +#include "type_erasure_detail.h" + +namespace NYT { + +//////////////////////////////////////////////////////////////////////////////// + +namespace NDetail { + +//////////////////////////////////////////////////////////////////////////////// + +template +struct TTypeErasableTraits; + +template +struct TTypeErasableTraits> +{ + using TReplaced = TFromThis; + + static constexpr bool Value = NoExcept + ? requires (TReplaced t, TArgs... args) { + { Cpo(std::forward(t), std::forward(args)...) } noexcept -> std::same_as; + } + : requires (TReplaced t, TArgs... args) { + { Cpo(std::forward(t), std::forward(args)...) } -> std::same_as; + }; +}; + +template +concept CAnyRefErasable = (TTypeErasableTraits, TCpos>::Value && ...); + +template +concept CAnyObjectErasable = + CAnyRefErasable && + std::movable> && + (!EnableCopy || std::copyable>); + +//////////////////////////////////////////////////////////////////////////////// + +struct TBadAnyCast + : public std::exception +{ }; + +//////////////////////////////////////////////////////////////////////////////// + +// AnyRef. + +template +class TAnyRef + : private TAnyFragment, TCpos>... +{ +public: + using TStorage = TNonOwningStorage; + + TAnyRef() = default; + + template + requires + CAnyRefErasable, TCpos...> && + (!std::same_as, TAnyRef>) + TAnyRef(TConcrete& concRef) noexcept + { + using TDecayed = std::remove_cvref_t; + + Storage_.Set(const_cast(reinterpret_cast(std::addressof(concRef)))); + Holder_ = THolder::template Create(); + } + + // Copy/Move are trivial copy. Dtor is also trivial. + + const auto& GetStorage() const & + { + return Storage_; + } + + auto& GetStorage() & + { + return Storage_; + } + + auto&& GetStorage() && + { + return std::move(Storage_); + } + + const auto& GetVTable() const + { + return Holder_.GetVTable(); + } + + template + requires CAnyRefErasable + T& AnyCast() & + { + const auto& vtable = GetVTable(); + if (!vtable.IsValid() || !vtable.template IsCurrentlyStored()) { + throw TBadAnyCast{}; + } + + return Storage_.template As(); + } + + template + requires CAnyRefErasable + const T& AnyCast() const & + { + const auto& vtable = Holder_.GetVTable(); + if (!vtable.IsValid() || !vtable.template IsCurrentlyStored()) { + throw TBadAnyCast{}; + } + + return Storage_.template As(); + } + +private: + using TVTable = TVTable; + using THolder = TVTableHolder; + + TStorage Storage_ = {}; + THolder Holder_ = {}; +}; + +//////////////////////////////////////////////////////////////////////////////// + +template +struct TAnyRefPublicHelper +{ + using T = TAnyRef<(sizeof...(TCpos) >= 3), TCpos...>; +}; + +template <> +struct TAnyRefPublicHelper<> +{ + using T = TAnyRef>; +}; + +//////////////////////////////////////////////////////////////////////////////// + +template +struct TIsAnyRef + : public std::false_type +{ }; + +template +struct TIsAnyRef> + : public std::true_type +{ }; + +//////////////////////////////////////////////////////////////////////////////// + +template +struct TRebindVTable +{ + using TVTable = TVTable< + TStorage, + TTagInvokeTag, + TTagInvokeTag>, + TTagInvokeTag>, + TCpos...>; +}; + +template +struct TRebindVTable +{ + using TVTable = TVTable< + TStorage, + TTagInvokeTag, + TTagInvokeTag>, + TCpos...>; +}; + +//////////////////////////////////////////////////////////////////////////////// + +template +class TAnyObject + : private TAnyFragment< + TAnyObject< + UseStaticVTable, + EnableCopy, + SmallObjectSize, + SmallObjectAlign, + TCpos...>, + TCpos>... +{ +public: + using TStorage = TOwningStorage; + + TAnyObject() = default; + + template + requires + (!std::same_as, TAnyObject>) && + std::constructible_from, TConcrete> && + CAnyObjectErasable, EnableCopy, TCpos...> && + (!TIsAnyRef>::value) + TAnyObject(TConcrete&& concrete) + { + Set(std::forward(concrete)); + } + + template + requires + (!std::same_as, TAnyObject>) && + std::constructible_from, TArgs...> && + (!std::is_const_v) && + CAnyObjectErasable, EnableCopy, TCpos...> && + (!TIsAnyRef>::value) + TAnyObject(std::in_place_type_t, TArgs&&... args) + { + Set(std::forward(args)...); + } + + TAnyObject(TAnyObject&& other) + : Holder_(other.Holder_) + { + if (IsValid()) { + auto* mover = GetVTable().template GetFunctor>(); + mover(std::move(other).GetStorage(), GetStorage()); + + other.Holder_.Reset(); + } + } + + TAnyObject& operator=(TAnyObject&& other) + { + if (this == &other) { + return *this; + } + + Reset(); + + Holder_ = other.Holder_; + + if (IsValid()) { + auto* mover = GetVTable().template GetFunctor>(); + mover(std::move(other).GetStorage(), GetStorage()); + + other.Holder_.Reset(); + } + + return *this; + } + + TAnyObject(const TAnyObject& other) requires EnableCopy + : Holder_(other.Holder_) + { + if (IsValid()) { + auto* copier = GetVTable().template GetFunctor>(); + copier(other.GetStorage(), GetStorage()); + } + } + + TAnyObject& operator=(const TAnyObject& other) requires EnableCopy + { + if (this == &other) { + return *this; + } + + Reset(); + + Holder_ = other.Holder_; + + if (IsValid()) { + auto* copier = GetVTable().template GetFunctor>(); + copier(other.GetStorage(), GetStorage()); + } + + return *this; + } + + constexpr explicit operator bool() const + { + return IsValid(); + } + + ~TAnyObject() + { + Reset(); + } + + Y_FORCE_INLINE const auto& GetStorage() const & + { + return Storage_; + } + + Y_FORCE_INLINE auto& GetStorage() & + { + return Storage_; + } + + Y_FORCE_INLINE auto&& GetStorage() && + { + return std::move(Storage_); + } + + Y_FORCE_INLINE const auto& GetVTable() const + { + return Holder_.GetVTable(); + } + + template + Y_FORCE_INLINE T& AnyCast() & + { + using TDecayed = std::remove_cvref_t; + using TWrapped = TOwningWrapper; + + const auto& vtable = GetVTable(); + if (!vtable.IsValid() || !vtable.template IsCurrentlyStored()) { + throw TBadAnyCast{}; + } + + return Storage_.template As().Unwrap(); + } + + template + Y_FORCE_INLINE const T& AnyCast() const & + { + using TDecayed = std::remove_cvref_t; + using TWrapped = TOwningWrapper; + + const auto& vtable = GetVTable(); + if (!vtable.IsValid() || !vtable.template IsCurrentlyStored()) { + throw TBadAnyCast{}; + } + + return Storage_.template As().Unwrap(); + } + + Y_FORCE_INLINE bool IsValid() const + { + return Holder_.IsValid(); + } + +private: + using TVTable = typename TRebindVTable::TVTable; + using THolder = TVTableHolder; + using TTraits = std::allocator_traits>; + + TStorage Storage_ = {}; + THolder Holder_ = {}; + + static inline std::allocator Allocator = {}; + + Y_FORCE_INLINE void Reset() noexcept + { + const auto& vtable = Holder_.GetVTable(); + if (Holder_.IsValid()) { + auto* deleter = vtable.template GetFunctor(); + deleter(Storage_); + + Holder_.Reset(); + } + } + + template + Y_FORCE_INLINE void Set(TArgs&&... args) + { + using TDecayed = std::remove_cvref_t; + using TWrapped = TOwningWrapper; + + Reset(); + + Holder_ = THolder::template Create(); + + if constexpr (TStorage::template IsStatic) { + Storage_.Set(); + } else { + Storage_.Set(TTraits::allocate(Allocator, sizeof(TWrapped))); + } + + TTraits::template construct( + Allocator, + &Storage_.template As(), + std::forward(args)...); + } +}; + +} // namespace NDetail + +//////////////////////////////////////////////////////////////////////////////// + +// A simple placeholder for overloaded cpo usage. +using NDetail::TErasedThis; + +//////////////////////////////////////////////////////////////////////////////// + +// A wrapper class which carries all the necessary information for type erasure. +template +using TOverload = NDetail::TOverloadedCpo; + +//////////////////////////////////////////////////////////////////////////////// + +// A non-owning reference. +template +using TAnyRef = typename NDetail::TAnyRefPublicHelper::T; + +//////////////////////////////////////////////////////////////////////////////// + +template +using TAnyObject = NDetail::TAnyObject< + (sizeof...(TCpos) >= 3), + /*EnableCopy*/ true, + /*SmallObjectSize*/ sizeof(void*) * 2, + /*SmallObjectAlign*/ alignof(void*) * 2, + TCpos...>; + +template +using TAnyUnique = NDetail::TAnyObject< + (sizeof...(TCpos) >= 3), + /*EnableCopy*/ false, + /*SmallObjectSize*/ sizeof(void*) * 2, + /*SmallObjectAlign*/ alignof(void*) * 2, + TCpos...>; + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT diff --git a/library/cpp/yt/memory/type_erasure_detail.h b/library/cpp/yt/memory/type_erasure_detail.h new file mode 100644 index 00000000000..5e158d3a814 --- /dev/null +++ b/library/cpp/yt/memory/type_erasure_detail.h @@ -0,0 +1,710 @@ +#pragma once + +#include + +#include + +#include + +#include +#include +#include + +namespace NYT::NDetail { + +//////////////////////////////////////////////////////////////////////////////// + +// NB(arkady-e1ppa): Next section of code is about most general type erasure storage. +// There are two kinds: One which simply holds a pointer to data stored elsewhere +// and the one which holds the data on its own. The latter can store it inline +// on the byte array or on heap depending on the object size (small object optimization). +// Classes below feature bare-bone implementations of said storages which do not +// control when and which object are being placed inside the storage for they do not +// have access to the virtual table (ctors/dtors) of the type. Said operations are handled +// by type erasure containers themselves. +//////////////////////////////////////////////////////////////////////////////// + +// NB(arkady-e1ppa): This class is used to check +// whether some class has a templated method which +// accepts any type parameter. +struct TConceptWitness +{ }; + +//////////////////////////////////////////////////////////////////////////////// + +// Semantic requirement: +// 1) Pointer provided by |Ptr| call +// must be pointing to the beginning of some object. +// 2) IsStatic must be a constexpr variable. +// NB(arkady-e1ppa): value of bool template in Ptr +// is whether currently stored object is static or not. +template +concept CPointerProvider = requires (T* t, const T* ct) { + { t->template Ptr() } -> std::same_as; + { ct->template Ptr() } -> std::same_as; + { t->template Ptr() } -> std::same_as; + { ct->template Ptr() } -> std::same_as; + { T::template IsStatic } -> std::same_as; +}; + +//////////////////////////////////////////////////////////////////////////////// + +// CRTP base to implement cast methods for each +// storage. +template +struct TStorageCasterBase +{ + template + Y_FORCE_INLINE void* GetPtr() const noexcept + { + return static_cast(this)->template Ptr(); + } + + template + Y_FORCE_INLINE TDecayedConcrete& As() & noexcept + { + return this->template ToRef(); + } + + template + Y_FORCE_INLINE const TDecayedConcrete& As() const & noexcept + { + return this->template ToRef(); + } + + template + Y_FORCE_INLINE TDecayedConcrete&& As() && noexcept + { + return std::move(this->template ToRef()); + } + + template + Y_FORCE_INLINE decltype(auto) ToRef() const noexcept + { + static_assert(std::derived_from, "Must inherit from TStorageCasterBase"); + static_assert(CPointerProvider, "Class must define Ptr method. See CPointerProvider"); + static_assert( + std::same_as< + TDecayedConcrete, + std::remove_cvref_t>, + "Submitted type must not contain const, reference or volatile qualifiers"); + + if constexpr (TDerived::template IsStatic) { + return *std::launder(reinterpret_cast(this->template GetPtr())); + } else { + return *static_cast(this->template GetPtr()); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +template +concept CStorage = requires (const T& cref, T& ref, void* ptr) { + { cref.template As() } -> std::same_as; + { ref.template As() } -> std::same_as; + { std::move(ref).template As() } -> std::same_as; + { ref.Set(ptr) } -> std::same_as; + + // Semantic: Set() should reset the instance making it empty. + { ref.Set() } -> std::same_as; +} && std::default_initializable; + +//////////////////////////////////////////////////////////////////////////////// + +class TNonOwningStorage + : public TStorageCasterBase +{ +public: + template + Y_FORCE_INLINE void* Ptr() const noexcept + { + return const_cast(Data_); + } + + template + static constexpr bool IsStatic = false; + + Y_FORCE_INLINE void Set(void* ptr) noexcept + { + Data_ = ptr; + } + + Y_FORCE_INLINE void Set() noexcept + { + Data_ = nullptr; + } + +private: + void* Data_ = nullptr; +}; + +static_assert(CPointerProvider); +static_assert(CStorage); + +//////////////////////////////////////////////////////////////////////////////// + +template +class TOwningStorage + : public TStorageCasterBase> +{ +private: + static constexpr size_t RealSize = sizeof(void*) > InlineSize + ? sizeof(void*) + : InlineSize; + + static constexpr size_t RealAlign = alignof(void*) > InlineAlign + ? alignof(void*) + : InlineAlign; + +public: + template + static constexpr bool IsStatic = + (sizeof(T) <= RealSize) && + (alignof(T) <= RealAlign); + + Y_FORCE_INLINE void Set(void* ptr) noexcept + { + std::construct_at(reinterpret_cast(Data_), ptr); + } + + Y_FORCE_INLINE void Set() noexcept + { + std::construct_at(Data_); + } + + template + Y_FORCE_INLINE void* Ptr() const noexcept + { + auto* mutableThis = const_cast(this); + + if constexpr (IsStatic) { + return &mutableThis->Data_; + } else { + return *std::launder(reinterpret_cast(&mutableThis->Data_)); + } + } + +private: + alignas(RealAlign) std::byte Data_[RealSize] = {}; +}; + +static_assert(CPointerProvider>); +static_assert(CStorage>); +static_assert(CPointerProvider>); +static_assert(CStorage>); + +//////////////////////////////////////////////////////////////////////////////// + +// NB(arkady-e1ppa): This part covers details of overloaded CPOs parcing and placeholder types. +// Relevant to public interface parts are exposed in type_erasure.h +//////////////////////////////////////////////////////////////////////////////// + +// Type erasable "function" = (function name <=> cpo) + (fixed signature). +template +struct TOverloadedCpo; + +template +struct TOverloadedCpo +{ }; + +//////////////////////////////////////////////////////////////////////////////// + +// Each implementation of erasable method has +// its own "this" in signature. Since in the erased methods +// we do not have any specific type, we use placeholder. +struct TErasedThis +{ }; + +//////////////////////////////////////////////////////////////////////////////// + +template +struct TFromThisImpl +{ + using TT = U; +}; + +template +struct TFromThisImpl +{ + using TT = const T&; +}; + +template +struct TFromThisImpl +{ + using TT = T&; +}; + +template +struct TFromThisImpl +{ + using TT = T&&; +}; + +template +struct TFromThisImpl +{ + using TT = T; +}; + +template +using TFromThis = typename TFromThisImpl::TT; + +//////////////////////////////////////////////////////////////////////////////// + +template +class TVTableEntry; + +template +class TVTableEntry> +{ +private: + using TReplaced = TFromThis; + using TSignature = TRet(TReplaced, TArgs...) noexcept(NoExcept); + using TFunction = TRet(*)(TReplaced, TArgs...) noexcept(NoExcept); + +public: + TVTableEntry() = default; + + Y_FORCE_INLINE TFunction Get() const noexcept + { + return Function_; + } + + template + Y_FORCE_INLINE static TVTableEntry Create() noexcept + { + TVTableEntry entry = {}; + + entry.Function_ = &TVTableEntry::StaticInvoke; + + return entry; + } + + Y_FORCE_INLINE void Reset() noexcept + { + Function_ = nullptr; + } + + Y_FORCE_INLINE bool IsValid() const noexcept + { + return Function_ != nullptr; + } + + // NB(arkady-e1ppa): This method may or may not work correctly + // for dynamically-linked libraries. + template + Y_FORCE_INLINE bool IsCurrentlyStored() const noexcept + { + return Function_ == &TVTableEntry::StaticInvoke; + } + +private: + TFunction Function_ = nullptr; + + template + static TRet StaticInvoke(TReplaced storage, TArgs... args) noexcept(NoExcept) + { + return Cpo(std::forward(storage).template As(), std::forward(args)...); + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +// Add vtable holder for static/local versions. +template +class TVTable; + +// NB(arkady-e1ppa): We do not support empty vtables since there is no way +// to check validity nor type of the object for such vtables. +template +class TVTable + : private TVTableEntry + , private TVTableEntry... +{ +public: + TVTable() = default; + + template + Y_FORCE_INLINE static TVTable Create() noexcept + { + return TVTable{TCtorTag{}}; + } + + template + Y_FORCE_INLINE auto GetFunctor() const noexcept + { + return TVTableEntry>::Get(); + } + + Y_FORCE_INLINE void Reset() noexcept + { + TVTableEntry::Reset(); + } + + Y_FORCE_INLINE bool IsValid() const noexcept + { + return TVTableEntry::IsValid(); + } + + template + Y_FORCE_INLINE bool IsCurrentlyStored() const noexcept + { + return TVTableEntry::template IsCurrentlyStored(); + } + +private: + template + struct TCtorTag + { }; + + template + explicit TVTable(TCtorTag) noexcept + : TVTableEntry{TVTableEntry::template Create()} + , TVTableEntry{TVTableEntry::template Create()}... + { } +}; + +//////////////////////////////////////////////////////////////////////////////// + +template +concept CVTableFor = + CStorage && + requires (const T& t) { + [] (TVTable) { } (t); + }; + +//////////////////////////////////////////////////////////////////////////////// + +template TTable, bool IsStatic> +class TVTableHolder +{ +public: + TVTableHolder() = default; + + bool IsValid() const + { + return Table_ != nullptr && Table_->IsValid(); + } + + template + static TVTableHolder Create() + { + static TTable staticTable = TTable::template Create(); + + TVTableHolder holder = {}; + holder.Table_ = std::addressof(staticTable); + return holder; + } + + const TTable& GetVTable() const + { + if (!IsValid()) { + return EmptyTable; + } + + return *Table_; + } + + void Reset() + { + Table_ = nullptr; + } + +private: + TTable* Table_ = nullptr; + + static inline TTable EmptyTable = {}; +}; + +template TTable> +class TVTableHolder +{ +public: + TVTableHolder() = default; + + bool IsValid() const + { + return Table_.IsValid(); + } + + template + static TVTableHolder Create() + { + TVTableHolder holder = {}; + holder.Table_ = TTable::template Create(); + return holder; + } + + const TTable& GetVTable() const + { + return Table_; + } + + void Reset() + { + Table_.Reset(); + } + +private: + TTable Table_ = {}; +}; + +//////////////////////////////////////////////////////////////////////////////// + +// NB(arkady-e1ppa): Below are Cpos which replicate function of default language features +// such as copy/move ctors, dtor. More can be added later if needed. +// But they are likely to be an overkill. +// General formula is that we have a cpo for default feature and then +// a wrapper of an object which serves the purpose of an adaptor between +// native c++ expression and CPO expression of the same behavior. +//////////////////////////////////////////////////////////////////////////////// + +struct TDeleter + : public TTagInvokeCpoBase +{ }; + +inline constexpr TOverloadedCpo Deleter = {}; + +//////////////////////////////////////////////////////////////////////////////// + +// NB(arkady-e1ppa): We do not use custom allocators with wacky policies +// and therefore don't adopt reallocating move ctor. +template +struct TMover + : public TTagInvokeCpoBase> +{ }; + +template +inline constexpr TOverloadedCpo{}, void(TErasedThis&&, TStorage&)> Mover = {}; + +//////////////////////////////////////////////////////////////////////////////// + +template +struct TCopier + : public TTagInvokeCpoBase> +{ }; + +template +inline constexpr TOverloadedCpo{}, void(const TErasedThis&, TStorage&)> Copier = {}; + +//////////////////////////////////////////////////////////////////////////////// + +struct TNoopCpo +{ + template + constexpr void operator() (TArgs&&... /*args*/) const + { + YT_ABORT(); + } +}; + +inline constexpr TOverloadedCpo NoopCpo = {}; + +//////////////////////////////////////////////////////////////////////////////// + +template +concept CWrapperOf = requires (W& ref, const W& cref) { + { ref.Unwrap() } -> std::same_as; + { std::move(ref).Unwrap() } -> std::same_as; + { cref.Unwrap() } -> std::same_as; +}; + +//////////////////////////////////////////////////////////////////////////////// + +// Wraps value, adding TagInvoke overload for dtor and move/copy ctors. +template +class TOwningWrapperBase +{ +public: + // NB(arkady-e1ppa): Wrapper is stored not only the object itself. + static constexpr bool IsStatic = TStorage::template IsStatic; + + using TTraits = std::allocator_traits>; + static inline std::allocator Allocator = {}; + + template + requires std::constructible_from + explicit TOwningWrapperBase(TArgs&&... args) + noexcept(std::is_nothrow_constructible_v) + : Concrete_(std::forward(args)...) + { } + + Y_FORCE_INLINE void Delete() & noexcept + { + TTraits::template destroy(Allocator, AsFinal()); + + if constexpr (!IsStatic) { + TTraits::deallocate(Allocator, reinterpret_cast(AsFinal()), sizeof(TDerived)); + } + } + + Y_FORCE_INLINE void Move(TStorage& to) && + requires std::movable + { + if constexpr (IsStatic) { + to.Set(); + + TTraits::template construct( + Allocator, + &to.template As(), + std::move(*AsFinal())); + } else { + to.Set(static_cast(AsFinal())); + } + } + + Y_FORCE_INLINE void Copy(TStorage& to) const & + requires (EnableCopy && std::copyable) + { + if constexpr (IsStatic) { + to.Set(); + } else { + to.Set(TTraits::allocate(Allocator, sizeof(TDerived))); + } + + TTraits::template construct( + Allocator, + &to.template As(), + *AsFinal()); + } + + friend Y_FORCE_INLINE void TagInvoke(TDeleter, TDerived& this_) noexcept + { + static_assert(std::derived_from, "Must derived from TOwningWrapperBase"); + this_.Delete(); + } + + friend Y_FORCE_INLINE void TagInvoke(TMover, TDerived&& this_, TStorage& to) + noexcept(std::is_nothrow_move_constructible_v) + { + std::move(this_).Move(to); + } + + friend Y_FORCE_INLINE void TagInvoke(TCopier, const TDerived& this_, TStorage& to) + noexcept(std::is_nothrow_copy_constructible_v) + { + this_.Copy(to); + } + + // CWrapperOf + Y_FORCE_INLINE TConcrete& Unwrap() & + { + return Concrete_; + } + + Y_FORCE_INLINE const TConcrete& Unwrap() const & + { + return Concrete_; + } + + Y_FORCE_INLINE TConcrete&& Unwrap() && + { + return std::move(Concrete_); + } + +private: + TConcrete Concrete_; + + Y_FORCE_INLINE TDerived* AsFinal() const + { + return static_cast(const_cast(this)); + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +template +struct TUnwrappingTagInvokeBase; + +// NB(arkady-e1ppa): Technically, we could accept TErasedThis as any argument, but +// there is barely any profit from doing so yet code becomes much more cumbersome +// both here and around TAny(Object|Ref). +template +struct TUnwrappingTagInvokeBase> +{ + using TReplaced = TFromThis; + + friend Y_FORCE_INLINE TRet TagInvoke(TTagInvokeTag, TReplaced wrapper, TArgs... args) noexcept(NoExcept) + { + return Cpo(std::forward(wrapper).Unwrap(), std::forward(args)...); + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +template +class TOwningWrapper final + : public TOwningWrapperBase< + TOwningWrapper< + TConcrete, + TStorage, + EnableCopy, + TCpos...>, + TConcrete, + TStorage, + EnableCopy> + , public TUnwrappingTagInvokeBase< + TOwningWrapper< + TConcrete, + TStorage, + EnableCopy, + TCpos...>, + TCpos>... +{ + using TBase = TOwningWrapperBase< + TOwningWrapper< + TConcrete, + TStorage, + EnableCopy, + TCpos...>, + TConcrete, + TStorage, + EnableCopy>; + + using TBase::TBase; +}; + +//////////////////////////////////////////////////////////////////////////////// + +// Every "Any-like" object must be able to provide access to storage +// and a vtable which matches the storage provided. +template +concept CSomeAnyObject = requires (T& ref, const T& cref) { + typename T::TStorage; + { ref.GetStorage() } -> std::same_as; + { std::move(ref).GetStorage() } -> std::same_as; + { cref.GetStorage() } -> std::same_as; + + { cref.GetVTable() } -> CVTableFor; +}; + +//////////////////////////////////////////////////////////////////////////////// + +template +struct TAnyFragment; + +template +struct TAnyFragment> +{ + using TReplaced = TFromThis; + using TVTableTag = TOverloadedCpo; + + friend Y_FORCE_INLINE TRet TagInvoke(TTagInvokeTag, TReplaced any, TArgs... args) noexcept(NoExcept) + { + static_assert(CSomeAnyObject); + + auto&& vtable = any.GetVTable(); + + YT_VERIFY(vtable.IsValid()); + + auto* functor = vtable.template GetFunctor(); + + return functor(std::forward(any).GetStorage(), std::forward(args)...); + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NDetail diff --git a/library/cpp/yt/memory/unittests/type_erasure_ut.cpp b/library/cpp/yt/memory/unittests/type_erasure_ut.cpp new file mode 100644 index 00000000000..e6e917d9756 --- /dev/null +++ b/library/cpp/yt/memory/unittests/type_erasure_ut.cpp @@ -0,0 +1,333 @@ +#include + +#include + +namespace NYT { +namespace { + +//////////////////////////////////////////////////////////////////////////////// + +struct TTestCpo1 + : public TTagInvokeCpoBase +{ }; + +inline constexpr TTestCpo1 TestCpo = {}; + +struct TCustomized +{ + int Value = 42; + + friend int TagInvoke(TTagInvokeTag, const TCustomized& this_) + { + return this_.Value + 1; + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +// 2 Cpos won't trigger static vtable +static_assert( + sizeof( + TAnyRef< + TOverload< + TestCpo, + void(TErasedThis&)>, + TOverload< + TestCpo, + void(TErasedThis&&)>>) + == 8 + 2 * 8); + +// 3 Cpos trigger static vtable +static_assert( + sizeof( + TAnyRef< + TOverload< + TestCpo, + void(TErasedThis&)>, + TOverload< + TestCpo, + void(TErasedThis&&)>, + TOverload< + TestCpo, + void(TErasedThis, int)>>) + == 8 + 8); + +//////////////////////////////////////////////////////////////////////////////// + +TEST(TAnyRefTest, JustWorks) +{ + using TAnyRef = TAnyRef>; + TCustomized concrete{.Value = 42}; + + TAnyRef any{concrete}; + EXPECT_EQ(TestCpo(any), 43); + + auto copy = any; + EXPECT_EQ(TestCpo(copy), 43); + + auto movedOut = std::move(any); + EXPECT_EQ(TestCpo(movedOut), 43); +} + +TEST(TAnyRefTest, EmptyRef) +{ + TCustomized concrete{.Value = 11}; + + TAnyRef<> any{concrete}; + + static_assert(!std::invocable, decltype(any)>); + const auto& conc = any.AnyCast(); + EXPECT_EQ(conc.Value, 11); +} + +//////////////////////////////////////////////////////////////////////////////// + +struct TNoCopy +{ + TNoCopy() = default; + + TNoCopy(const TNoCopy&) = delete; + + TNoCopy(TNoCopy&&) + { } + + TNoCopy& operator=(TNoCopy&&) + { + return *this; + } + + int Val = 123; +}; + +static_assert(std::movable); + +struct TCustomized2 +{ + int Value = 1; + static inline int DtorCount = 0; + + TCustomized2() = default; + + TCustomized2(const TCustomized2&) + { } + + TCustomized2& operator=(const TCustomized2&) + { + return *this; + } + + TCustomized2(TCustomized2&& other) + : Value(other.Value) + { + other.Value = -1; + } + + TCustomized2& operator=(TCustomized2&& other) + { + if (this == &other) { + return *this; + } + + Value = std::exchange(other.Value, -1); + return *this; + } + + ~TCustomized2() + { + ++DtorCount; + } + + friend const TNoCopy& TagInvoke(TTagInvokeTag, TCustomized2&) + { + static TNoCopy noCp; + noCp.Val = 11; + return noCp; + } + + friend int TagInvoke(TTagInvokeTag, TCustomized2&& this_) + { + auto v = std::move(this_); + + return 1212; + } + + friend int TagInvoke(TTagInvokeTag, TCustomized2&, int) + { + return 42; + } +}; + +static_assert(std::copyable); + +//////////////////////////////////////////////////////////////////////////////// + +TEST(TAnyRefTest, CvRefCorrectness) +{ + using TAnyRef = TAnyRef< + TOverload, + TOverload>; + + TCustomized2 cust = {}; + + cust.DtorCount = 0; + EXPECT_EQ(cust.Value, 1); + + { + TAnyRef ref{cust}; + + EXPECT_EQ(TestCpo(ref).Val, 11); + } + + EXPECT_EQ(cust.DtorCount, 0); + EXPECT_EQ(cust.Value, 1); + + { + TAnyRef ref(cust); + + EXPECT_EQ(TestCpo(std::move(ref)), 1212); + } + + EXPECT_EQ(cust.DtorCount, 1); + EXPECT_EQ(cust.Value, -1); + + cust.Value = 1; + TAnyRef any{cust}; + { + TAnyRef copy{any}; + TAnyRef movedOut{std::move(any)}; + EXPECT_EQ(TestCpo(copy).Val, 11); + EXPECT_EQ(TestCpo(movedOut).Val, 11); + } + + EXPECT_EQ(cust.DtorCount, 1); + EXPECT_EQ(cust.Value, 1); +} + +TEST(TAnyRefTest, StaticVTableForAnyRef) +{ + using TAnyRef = TAnyRef< + TOverload, + TOverload, + TOverload + >; + + TCustomized2 cst = {}; + cst.Value = 1111; + cst.DtorCount = 0; + + TAnyRef any{cst}; + { + TAnyRef copy{any}; + TAnyRef movedOut{std::move(any)}; + EXPECT_EQ(TestCpo(copy).Val, 11); + EXPECT_EQ(TestCpo(movedOut).Val, 11); + } + EXPECT_EQ(cst.Value, 1111); + EXPECT_EQ(cst.DtorCount, 0); +} + +//////////////////////////////////////////////////////////////////////////////// + +TEST(TAnyObjectTest, JustWorks) +{ + TCustomized concrete{.Value = 42}; + + TAnyObject> any{concrete}; + EXPECT_EQ(TestCpo(any), 43); + + auto copy = any; + EXPECT_EQ(TestCpo(copy), 43); + + auto movedOut = std::move(any); + EXPECT_EQ(TestCpo(movedOut), 43); +} + +TEST(TAnyObjectTest, EmptyAny) +{ + TCustomized concrete{.Value = 11}; + + TAnyObject<> any{concrete}; + + static_assert(!std::invocable, decltype(any)>); + const auto& conc = any.AnyCast(); + EXPECT_EQ(conc.Value, 11); +} + +TEST(TAnyObjectTest, CvRefCorrectness) +{ + using TAnyObject = TAnyObject< + TOverload, + TOverload>; + + TCustomized2 cust = {}; + + cust.DtorCount = 0; + EXPECT_EQ(cust.Value, 1); + + { + TAnyObject any{cust}; + + EXPECT_EQ(TestCpo(any).Val, 11); + } + + // Any object itself. + EXPECT_EQ(cust.DtorCount, 1); + EXPECT_EQ(cust.Value, 1); + + { + TAnyObject any(cust); + + EXPECT_EQ(TestCpo(std::move(any)), 1212); + } + + // Second any object + moved out object. + EXPECT_EQ(cust.DtorCount, 3); + EXPECT_EQ(cust.Value, 1); + + TAnyObject any{cust}; + { + TAnyObject copy{any}; + TAnyObject movedOut{std::move(any)}; + EXPECT_EQ(TestCpo(copy).Val, 11); + EXPECT_EQ(TestCpo(movedOut).Val, 11); + } + + EXPECT_EQ(cust.DtorCount, 5); + EXPECT_THROW(any.AnyCast(), NDetail::TBadAnyCast); +} + +TEST(TAnyObjectTest, StaticVTableForAnyRef) +{ + using TAnyObject = TAnyObject< + TOverload, + TOverload, + TOverload + >; + + TCustomized2 cst = {}; + cst.Value = 1111; + cst.DtorCount = 0; + + TAnyObject any{cst}; + { + TAnyObject copy{any}; + TAnyObject movedOut{std::move(any)}; + EXPECT_EQ(TestCpo(copy).Val, 11); + EXPECT_EQ(TestCpo(movedOut).Val, 11); + } + EXPECT_EQ(cst.Value, 1111); + EXPECT_EQ(cst.DtorCount, 2); + EXPECT_FALSE(any.IsValid()); +} + +//////////////////////////////////////////////////////////////////////////////// + +TEST(TAnyObjectTest, AnyMoveOnly) +{ + TAnyUnique<> any{std::in_place_type}; +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace +} // namespace NYT diff --git a/library/cpp/yt/memory/unittests/ya.make b/library/cpp/yt/memory/unittests/ya.make index 1a61a130baf..708586d651d 100644 --- a/library/cpp/yt/memory/unittests/ya.make +++ b/library/cpp/yt/memory/unittests/ya.make @@ -15,6 +15,7 @@ SRCS( weak_ptr_ut.cpp ref_ut.cpp range_protobuf_repeated_field_ut.cpp + type_erasure_ut.cpp ) IF (NOT OS_WINDOWS) -- cgit v1.3