My goal is to have a memory pool non-template class that is used to store arrays of objects. The same memory pool object must be reusable for a different array (difference size, different type and/or alignment).
I already posted a series of questions on SO about this subject:
- Correctly using type-punning and erasure for array of objects
- type-punning and strict aliasing rule for array of objects
- type-punning: omitting placement new and destructors
- type-punning with std::aligned_alloc for array of objects
- What exactly does the standard array form placement new expression?
I tried to sum-up all the discussions in these questions into a C++14 code. The "interesting" part is the Buffer class
:
#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.
N.B. my concern in SO was that these type-punning techniques, though well defined for single objects, seemed to be ill-defined when speaking of arrays.
Note: I previously had a std::function
which I replaced by a simple pointer to function. Yet I don't understand why the pointer does not become dangling when leaving the DefaultAllocate
function.
<memory>
? Is it just because the interface is much more limited in C++14 than it is in C++17? \$\endgroup\$std::realloc
followed by a placement new? Yet, as mentioned in my SO question, I'm not sure if this code is legit, for instance, with respect to strict aliasing rule and validity of pointer arithmetic on the obtained typed array pointer. \$\endgroup\$std::uninitialized_copy()
and family might be useful for the implementation. I haven't looked deeply enough to work out for myself whether they might save work - it was a question rather than a criticism! \$\endgroup\$