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;
}