#include <cstddef>
#include <functional>
#include <iostream>
// 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;
}
// just for the sake of the example: p points to at least a sequence of 3 T
// probably not the best implem, but compiles without conversion warning with
// SFloat and float.
template <typename T>
void load(T* p) {
std::cout << "starting load\n";
p[0] = static_cast<T>(3.14);
p[1] = static_cast<T>(31.4);
p[2] = static_cast<T>(314.);
std::cout << "ending load\n";
}
// type-punning reusable buffer
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
// 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?
std::function<void(Buffer*)> Destructors = [](Buffer*) {};
// buffer address
unsigned char* p = nullptr;
// aligned buffer address
unsigned char* paligned = nullptr;
// number of stored elements
size_t n = 0;
// buffer size in bytes
size_t s = 0;
// computes the smallest positive offset in bytes that must be applied to p
// in order to have alignment a a is supposed to be a valid alignment
std::size_t OffsetForAlignement(unsigned char const* const ptr,
std::size_t a) {
std::size_t res = reinterpret_cast<std::size_t>(ptr) % a;
if (res) {
return a - res;
} else {
return 0;
}
}
// allocates a char buffer large enough for N object of type T and
// default-construct them
// N must be > 0
template <typename T>
T* DefaultAllocate(const std::size_t N) {
// Destroy previously stored objects, supposedly ends lifetime of the
// array object that contains them
Destructors(this);
std::size_t RequiredSize = sizeof(T) * N + alignof(T);
if (s < RequiredSize) {
std::cout << "Requiring " << RequiredSize << " bytes of storage\n";
// @2 creating a storage of RequiredSize bytes
// what would be the C++17+ way of do that? std::aligned_alloc?
p = reinterpret_cast<unsigned char*>(std::realloc(p, RequiredSize));
s = RequiredSize;
// here should do something for the case where p is nullptr
paligned = p + OffsetForAlignement(p, alignof(T));
}
// @3 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*
// T *tmp = new (p) T[N];
// @4 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*
unsigned char* pos = paligned;
T* tmp = reinterpret_cast<T*>(paligned);
for (std::size_t i = 0; i < N; ++i) {
new (pos) T();
pos += sizeof(T);
}
// update nb of objects
n = N;
// create destructors functor
// @5 supposedly ends the lifetime of the array object and of the
// contained objects
Destructors = [](Buffer* pobj) {
T* ToDestruct = reinterpret_cast<T*>(pobj->p);
// Delete elements in reverse order of creation
while (pobj->n > 0) {
--(pobj->n);
// should be std::Destroy(ToDestruct[n]) 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->n)].~T();
}
// @6 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() {
// Ending objects lifetime
Destructors(this);
// Releasing storage
std::free(p);
}
};
int main() {
constexpr std::size_t N0 = 7;
constexpr std::size_t N1 = 3;
Buffer B;
std::cout << "Test on SomeClass\n";
SFloat* ps = B.DefaultAllocate<SFloat>(N0);
ps[0] = 3.14;
*(ps + 1) = 31.4;
ps[2] = 314.;
std::cout << ps[0] << '\n';
std::cout << ps[1] << '\n';
std::cout << *(ps + 2) << '\n';
std::cout << "Test on float\n";
// reallocating, possibly using existing storage, for a different type and
// size
float* pf = B.DefaultAllocate<float>(N1);
pf[0] = 3.14f;
*(pf + 1) = 31.4f;
pf[2] = 314.f;
std::cout << pf[0] << '\n';
std::cout << pf[1] << '\n';
std::cout << *(pf + 2) << '\n';
return 0;
}
Live demo
I would appreciate a review on this Buffer class, in C++14. Yet I'm also interested in upgrade to C++17 and beyond.
I've placed // @# numbered comment in the code for focus on specific implementation issues/questions.
[EDIT] replacing std::function by a simple pointer to function. Yet I don't understand why the pointer does not get dangling when leaving the DefaultAllocate function.