/* SEVERAL-BUILDER.hpp - builder for a limited fixed collection of elements Copyright (C) Lumiera.org 2024, Hermann Vosseler This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /** @file several-builder.hpp ** Builder to create and populate instances of the lib::Several container. ** For mere usage, inclusion of several.hpp should be sufficient, since the ** container front-end is generic and intends to hide most details of allocation ** and element placement. It is an array-like container, but may hold subclass ** elements, while exposing only a reference to the interface type. ** ** # Implementation data layout ** ** The front-end container lib::Several is actually just a smart-ptr referring ** to the actual data storage, which resides within an _array bucket._ Typically ** the latter is placed into memory managed by a custom allocator, most notably ** lib::AllocationCluster. However, by default, the ArrayBucket will be placed ** into heap memory. All further meta information is also maintained alongside ** this data allocation, including a _deleter function_ to invoke all element ** destructors and de-allocate the bucket itself. Neither the type of the ** actual elements, nor the type of the allocator is revealed. ** ** Since the actual data elements can (optionally) be of a different type than ** the exposed interface type \a I, additional storage and spacing is required ** in the element array. The field ArrayBucket::spread defines this spacing ** and thus the offset used for subscript access. ** ** @todo this is a first implementation solution from 6/2025 — and was deemed ** _roughly adequate_ at that time, yet should be revalidated once more ** observations pertaining real-world usage are available... ** @see several-builder-test.cpp ** */ #ifndef LIB_SEVERAL_BUILDER_H #define LIB_SEVERAL_BUILDER_H #include "lib/error.hpp" #include "lib/several.hpp" #include "include/limits.hpp" #include "lib/iter-explorer.hpp" #include "lib/format-string.hpp" #include "lib/util.hpp" #include #include #include #include #include namespace lib { namespace err = lumiera::error; using std::vector; using std::forward; using std::move; using std::byte; namespace {// Allocation management policies /** number of storage slots to open initially; * starting with an over-allocation similar to `std::vector` */ const uint INITIAL_ELM_CNT = 10; using util::max; using util::min; using util::_Fmt; using std::is_nothrow_move_constructible_v; using std::is_trivially_move_constructible_v; using std::is_trivially_destructible_v; using std::has_virtual_destructor_v; using std::is_trivially_copyable_v; using std::is_copy_constructible_v; using std::is_object_v; using std::is_volatile_v; using std::is_const_v; using std::is_same_v; using lib::meta::is_Subclass; /** * Helper to determine the »spread« required to hold * elements of type \a TY in memory _with proper alignment._ * @warning assumes that the start of the buffer is also suitably aligned, * which _may not be the case_ for **over-aligned objects** with * `alignof(TY) > alignof(void*)` */ template size_t inline constexpr reqSiz() { size_t quant = alignof(TY); size_t siz = max (sizeof(TY), quant); size_t req = (siz/quant) * quant; if (req < siz) req += quant; return req; } template class ALO> class ElementFactory : private ALO { using Allo = ALO; using AlloT = std::allocator_traits; using Bucket = ArrayBucket; Allo& baseAllocator() { return *this; } template auto adaptAllocator() { using XAllo = typename AlloT::template rebind_alloc; if constexpr (std::is_constructible_v) return XAllo{baseAllocator()}; else return XAllo{}; } public: ElementFactory (Allo allo = Allo{}) : Allo{std::move (allo)} { } Bucket* create (size_t cnt, size_t spread) { size_t storageBytes = Bucket::requiredStorage (cnt, spread); std::byte* loc = AlloT::allocate (baseAllocator(), storageBytes); Bucket* bucket = reinterpret_cast (loc); using BucketAlloT = typename AlloT::template rebind_traits; auto bucketAllo = adaptAllocator(); try { BucketAlloT::construct (bucketAllo, bucket, cnt*spread, spread); } catch(...) { AlloT::deallocate (baseAllocator(), loc, storageBytes); throw; } return bucket; }; template E& createAt (Bucket* bucket, size_t idx, ARGS&& ...args) { REQUIRE (bucket); using ElmAlloT = typename AlloT::template rebind_traits; auto elmAllo = adaptAllocator(); E* loc = reinterpret_cast (& bucket->subscript (idx)); ElmAlloT::construct (elmAllo, loc, forward (args)...); ENSURE (loc); return *loc; }; template void destroy (ArrayBucket* bucket) { REQUIRE (bucket); if (not is_trivially_destructible_v) { size_t cnt = bucket->cnt; using ElmAlloT = typename AlloT::template rebind_traits; auto elmAllo = adaptAllocator(); for (size_t idx=0; idx (& bucket->subscript (idx)); ElmAlloT::destroy (elmAllo, elm); } } size_t storageBytes = Bucket::requiredStorage (bucket->buffSiz); std::byte* loc = reinterpret_cast (bucket); AlloT::deallocate (baseAllocator(), loc, storageBytes); }; }; template class ALO> struct AllocationPolicy : ElementFactory { using Fac = ElementFactory; using Bucket = ArrayBucket; using Fac::Fac; // pass-through ctor const bool isDisposable{false}; ///< memory must be explicitly deallocated bool canExpand(size_t){ return false; } Bucket* realloc (Bucket* data, size_t cnt, size_t spread) { Bucket* newBucket = Fac::create (cnt, spread); if (data) try { newBucket->deleter = data->deleter; size_t elms = min (cnt, data->cnt); for (size_t idx=0; idxdestroy(); } catch(...) { newBucket->destroy(); } return newBucket; } void moveElem (size_t idx, Bucket* src, Bucket* tar) { if constexpr (is_trivially_copyable_v) { void* oldPos = & src->subscript(idx); void* newPos = & tar->subscript(idx); size_t amount = min (src->spread, tar->spread); std::memmove (newPos, oldPos, amount); } else if constexpr (is_nothrow_move_constructible_v or is_copy_constructible_v) { E& oldElm = reinterpret_cast (src->subscript (idx)); Fac::template createAt (tar, idx ,std::move_if_noexcept (oldElm)); } else { NOTREACHED("realloc immovable type (neither trivially nor typed movable)"); // this alternative code section is very important, because it allows // to instantiate this code even for »noncopyable« types, assuming that // sufficient storage is reserved beforehand, and thus copying is irrelevant. // For context: the std::vector impl. from libStdC++ is lacking this option. } tar->cnt = idx+1; // mark fill continuously for proper clean-up after exception } }; template using HeapOwn = AllocationPolicy; }//(End)implementation details /** * Wrap a vector holding objects of a subtype and * provide array-like access using the interface type. */ template ,class E =I ///< a subclass element element type (relevant when not trivially movable and destructible) ,class POL =HeapOwn ///< Allocator policy > class SeveralBuilder : private Several , util::MoveOnly , POL { using Coll = Several; using Bucket = ArrayBucket; using Deleter = typename Bucket::Deleter; public: SeveralBuilder() = default; /** start Several build using a custom allocator */ template>> SeveralBuilder (ARGS&& ...alloInit) : Several{} , POL{forward (alloInit)...} { } /* ===== Builder API ===== */ SeveralBuilder&& reserve (size_t cntElm) { adjustStorage (cntElm, reqSiz()); return move(*this); } template SeveralBuilder&& append (VAL&& val, VALS&& ...vals) { emplace (forward (val)); if constexpr (0 < sizeof...(VALS)) return append (forward (vals)...); else return move(*this); } template SeveralBuilder&& appendAll (IT&& data) { explore(data).foreach ([this](auto it){ emplaceCopy(it); }); return move(*this); } template SeveralBuilder&& appendAll (std::initializer_list ili) { using Val = typename meta::Strip::TypeReferred; for (Val const& x : ili) emplaceNewElm (x); return move(*this); } template SeveralBuilder&& fillElm (size_t cntNew, ARGS&& ...args) { for ( ; 0 (forward (args)...); return move(*this); } template SeveralBuilder&& emplace (ARGS&& ...args) { using Val = typename meta::Strip::TypeReferred; emplaceNewElm (forward (args)...); return move(*this); } /** * Terminal Builder: complete and lock the collection contents. * @note the SeveralBuilder is sliced away, effectively * returning only the pointer to the ArrayBucket. */ Several build() { return move (*this); } size_t size() const { return Coll::size(); } bool empty() const { return Coll::empty();} private: template void emplaceCopy (IT& dataSrc) { using Val = typename IT::value_type; emplaceNewElm (*dataSrc); } template void emplaceNewElm (ARGS&& ...args) { static_assert (is_object_v and not (is_const_v or is_volatile_v)); // mark when target type is not trivially movable probeMoveCapability(); // ensure sufficient element capacity or the ability to adapt element spread if (Coll::spread() < reqSiz() and not (Coll::empty() or canWildMove())) throw err::Invalid{_Fmt{"Unable to place element of type %s (size=%d)" "into Several-container for element size %d."} % util::typeStr() % reqSiz() % Coll::spread()}; // ensure sufficient storage or verify the ability to re-allocate if (not (Coll::empty() or Coll::hasReserve(reqSiz()) or POL::canExpand(reqSiz()) or canDynGrow())) throw err::Invalid{_Fmt{"Several-container is unable to accommodate further element of type %s; " "storage reserve (%s bytes) exhausted and unable to move elements " "of mixed unknown detail type, which are not trivially movable." } % util::typeStr() % Coll::storageBuffSiz()}; size_t elmSiz = reqSiz(); size_t newPos = Coll::size(); size_t newCnt = Coll::empty()? INITIAL_ELM_CNT : newPos+1; adjustStorage (newCnt, max (elmSiz, Coll::spread())); ENSURE (Coll::data_); ensureDeleter(); POL::template createAt (Coll::data_, newPos, forward (args)...); Coll::data_->cnt = newPos+1; } /** ensure clean-up can be handled properly. * @throw err::Invalid when \a TY requires a different style * of deleter than was established for this instance */ template void ensureDeleter() { Deleter deleterFunctor = selectDestructor(); if (Coll::data_->deleter) return; Coll::data_->deleter = deleterFunctor; } void adjustStorage (size_t cnt, size_t spread) { size_t demand{cnt*spread}; size_t buffSiz{Coll::storageBuffSiz()}; if (demand == buffSiz) return; if (demand > buffSiz) {// grow into exponentially expanded new allocation size_t safetyLim = LUMIERA_MAX_ORDINAL_NUMBER * Coll::spread(); size_t expandAlloc = min (safetyLim ,max (2*buffSiz, demand)); if (expandAlloc < demand) throw err::State{_Fmt{"Storage expansion for Several-collection " "exceeds safety limit of %d bytes"} % safetyLim ,LERR_(SAFETY_LIMIT)}; // allocate new storage block... size_t newCnt = expandAlloc / spread; if (newCnt * spread < expandAlloc) ++newCnt; Coll::data_ = POL::realloc (Coll::data_, newCnt,spread); } ENSURE (Coll::data_); if (canWildMove() and spread != Coll::spread()) adjustSpread (spread); } void fitStorage() { if (not Coll::data) return; if (not canDynGrow()) throw err::Invalid{"Unable to shrink storage for Several-collection, " "since at least one element can not be moved."}; Coll::data_ = POL::realloc (Coll::data_, Coll::size(), Coll::spread()); } /** move existing data to accommodate spread */ void adjustSpread (size_t newSpread) { REQUIRE (Coll::data_); REQUIRE (newSpread * Coll::size() <= Coll::storageBuffSiz()); size_t oldSpread = Coll::spread(); if (newSpread > oldSpread) // need to spread out for (size_t i=Coll::size()-1; 0spread = newSpread; } void shiftStorage (size_t idx, size_t oldSpread, size_t newSpread) { REQUIRE (idx); REQUIRE (oldSpread); REQUIRE (newSpread); REQUIRE (Coll::data_); byte* oldPos = Coll::data_->storage; byte* newPos = oldPos; oldPos += idx * oldSpread; newPos += idx * newSpread; std::memmove (newPos, oldPos, util::min (oldSpread,newSpread)); } /* ==== Logic do decide about possible element handling ==== */ enum DestructionMethod{ UNKNOWN , TRIVIAL , ELEMENT , VIRTUAL }; static Literal render (DestructionMethod m) { switch (m) { case TRIVIAL: return "trivial"; case ELEMENT: return "fixed-element-type"; case VIRTUAL: return "virtual-baseclass"; default: throw err::Logic{"unknown DestructionMethod"}; } } DestructionMethod destructor{UNKNOWN}; bool lock_move{false}; /** * Select a suitable method for invoking the element destructors * and build a λ-object to be stored as deleter function alongside * with the data; this includes a _copy_ of the embedded allocator, * which in many cases is a monostate empty base class. * @note this collection is _primed_ by the first element added, * causing to lock into one of the possible destructor schemes; * the reason is, we do not retain the information of the individual * element types and thus we must employ one coherent scheme for all. */ template Deleter selectDestructor() { typename POL::Fac& factory(*this); if (is_Subclass() and has_virtual_destructor_v) { __ensureMark (VIRTUAL); return [factory](ArrayBucket* bucket){ unConst(factory).template destroy (bucket); }; } if (is_trivially_destructible_v) { __ensureMark (TRIVIAL); return [factory](ArrayBucket* bucket){ unConst(factory).template destroy (bucket); }; } if (is_same_v and is_Subclass()) { __ensureMark (ELEMENT); return [factory](ArrayBucket* bucket){ unConst(factory).template destroy (bucket); }; } throw err::Invalid{_Fmt{"Unsupported kind of destructor for element type %s."} % util::typeStr()}; } template void __ensureMark (DestructionMethod requiredKind) { if (destructor != UNKNOWN and destructor != requiredKind) throw err::Invalid{_Fmt{"Unable to handle (%s-)destructor for element type %s, " "since this container has been primed to use %s-destructors."} % render(requiredKind) % util::typeStr() % render(destructor)}; destructor = requiredKind; } /** mark that we're about to accept an otherwise unknown type, * which can not be trivially moved. This irrevocably disables * relocations by low-level `memove` for this container instance */ template void probeMoveCapability() { if (not (is_same_v or is_trivially_copyable_v)) lock_move = true; } bool canWildMove() { return is_trivially_copyable_v and not lock_move; } bool canDynGrow() { return not lock_move; } }; template SeveralBuilder makeSeveral (std::initializer_list ili) { return SeveralBuilder{} .reserve (ili.size()) .appendAll (ili); } template SeveralBuilder makeSeveral() { return SeveralBuilder{}; } } // namespace lib #endif /*LIB_SEVERAL_BUILDER_H*/