Here is a follow up on Reusable storage for array of objects, taking into account the provided answers.
The following code should be compliant at least with gcc
, clang
, msvc
and for C++14 and beyond.
#include <cstddef> #include <cstdint> #include <functional> #include <iostream> #include <new> #include <type_traits> template <typename T> constexpr T* Launder(T* Ptr) { #if __cplusplus < 201703L // Launder in C++<17 return Ptr; #else // Launder in C++>=17 return std::launder(Ptr); #endif } // Object constructible from a double // forcing alignment struct alignas(16) SFloat { float val = 0.f; SFloat() { std::cout << "Constructing a SFloat with default value\n"; }; SFloat(const double v) : val(static_cast<float>(v)) { std::cout << "Constructing a SFloat from " << v << '\n'; }; SFloat& operator=(SFloat&& f) { val = f.val; std::cout << "Move-assigning from a SFloat " << f.val << '\n'; return *this; } ~SFloat() { std::cout << "Destructing a SFloat holding " << val << '\n'; } }; // Serialization of Float objects std::ostream& operator<<(std::ostream& o, SFloat const& f) { return o << f.val; } // type-punning reusable buffer // holds a non-typed buffer (actually a char*) that can be used to store any // types, according to user needs class Buffer { private: // destructing functor storage // required to call the correct object destructors // using std::function to store a copy of the lambda used // @1 is there a way to avoid std::function? a mere function pointer could // be used but it does not compile with msvc/C++14 // @2 std::function does not support noexcept properly how to do better? std::function<void(Buffer*)> Destructors = [](Buffer*) noexcept {}; // buffer address unsigned char* Storage = nullptr; // aligned buffer address unsigned char* AlignedStorage = nullptr; // number of stored elements std::size_t CountOfStoredObjects = 0; // buffer size in bytes std::size_t StorageSizeInBytes = 0; // computes the smallest positive offset in bytes that must be applied to // Storage in order to have alignment a Alignment is supposed to be a valid // alignment std::size_t OffsetForAlignment(unsigned char const* const Ptr, std::size_t Alignment) noexcept { std::size_t Res = static_cast<std::size_t>( reinterpret_cast<std::uintptr_t>(Ptr) % Alignment); if (Res) { return Alignment - Res; } else { return 0; } } public: // allocates a char buffer large enough for Counts object of type T and // default-construct them template <typename T> T* DefaultAllocate(const std::size_t Count) { // Destroy previously stored objects, supposedly ends lifetime of the // array object that contains them Destructors(this); std::size_t RequiredSize = sizeof(T) * Count + alignof(T); if (StorageSizeInBytes < RequiredSize) { std::cout << "Requiring " << RequiredSize << " bytes of storage\n"; // @3 creating a storage of RequiredSize bytes // what would be the C++17+ way of do that? std::aligned_alloc? Storage = reinterpret_cast<unsigned char*>( std::realloc(Storage, RequiredSize)); if (Storage == nullptr) { throw(std::bad_alloc()); } StorageSizeInBytes = RequiredSize; // here should do something for the case where Storage is nullptr } AlignedStorage = Storage + OffsetForAlignment(Storage, alignof(T)); // @4 Form1 placement array new default construction: ill-defined in // C++14? // expecting starting an array object lifetime and the lifetime of // contained objects // expecting pointer arithmetic to be valid on tmp T* // Is the use of Launder correct? T* tmp = Launder(new (Storage) T[Count]); // @5 Form2 individually creating packed object in storage // expecting starting an array object lifetime and the lifetime of // contained objects // expecting pointer arithmetic to be valid on tmp T* // Is the use of Launder correct? // unsigned char* pos = AlignedStorage; // T* tmp = Launder(reinterpret_cast<T*>(AlignedStorage)); // for (std::size_t i = 0; i < Count; ++i) { // new (pos) T(); // pos += sizeof(T); // } // update nb of objects CountOfStoredObjects = Count; // create destructors functor // @6 supposedly ends the lifetime of the array object and of the // contained objects Destructors = [](Buffer* pobj) noexcept { T* ToDestruct = reinterpret_cast<T*>(pobj->AlignedStorage); // Delete elements in reverse order of creation while (pobj->CountOfStoredObjects > 0) { --(pobj->CountOfStoredObjects); // should be std::Destroy(ToDestruct[CountOfStoredObjects]) in // C++17 I should provide my own implementation in C++14 in // order to distinguish between fundamental types and other ones // @ how to formally en the lifetime of a fundamental objects? // merely rewrite on its memory location? ToDestruct[(pobj->CountOfStoredObjects)].~T(); } // @7 How to formally end the array object lifetime? }; return tmp; } // deallocate objects in buffer but not the buffer itself // actually useless // template <typename T> // void Deallocate() { // Destructors(this); // } ~Buffer() noexcept { // Ending objects lifetime // @8 Actually not throwing and compilers are not complaining // should I wrap it anyway into a try-catch block? Destructors(this); // Releasing storage std::free(Storage); } }; int main() { #if (__cplusplus >= 201703L) std::cout << "C++17\n"; #else std::cout << "C++14\n"; #endif constexpr std::size_t N0 = 7; constexpr std::size_t N1 = 3; Buffer B; std::cout << "Test on SomeClass\n"; SFloat* PSFloat = B.DefaultAllocate<SFloat>(N0); PSFloat[0] = 3.14; *(PSFloat + 1) = 31.4; PSFloat[2] = 314.; std::cout << PSFloat[0] << '\n'; std::cout << PSFloat[1] << '\n'; std::cout << *(PSFloat + 2) << '\n'; std::cout << "Test on float\n"; // reallocating, possibly using existing storage, for a different type and // size float* PFloat = B.DefaultAllocate<float>(N1); PFloat[0] = 3.14f; *(PFloat + 1) = 31.4f; PFloat[2] = 314.f; std::cout << PFloat[0] << '\n'; std::cout << PFloat[1] << '\n'; std::cout << *(PFloat + 2) << '\n'; return 0; }