There is already a lot of posts about strict aliasing rule and type-punning but I couldn't find an explanation that I could understand regarding array of objects. My goal is to have a memory pool non-template class that is used to store arrays of objects. Basically I need to know the actual type only at access time: it can be seen as a non template vector whose iterators would be template. The design I thought of rises several questions so I will try to split them into several SO questions.
My question (which is the second one, see below) is can the placement new (lines 45 and 55) and the corresponding destructor loop (in Deallocate()
) be omitted in other case than arithmetic types?.
#include <cassert>
#include <iostream>
#include <type_traits>
// type that support initialisation from a single double value
using test_t = float;
// just for the sake of the example: p points to at least a sequence of 3 test_t
void load(test_t* p) {
std::cout << "starting load\n";
p[0] = static_cast<test_t>(3.14);
p[1] = static_cast<test_t>(31.4);
p[2] = static_cast<test_t>(314.);
std::cout << "ending load\n";
}
// type-punning buffer
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
// buffer address
char* p = nullptr;
// number of stored elements
size_t n = 0;
// buffer size in bytes
size_t s = 0;
// allocates a char buffer large enough for N object of type T and
// default-construct them
// calling it on a previously allocated buffer without adequate call to
// Deallocate is UB
template <typename T>
T* DefaultAllocate(const size_t N) {
size_t RequiredSize =
sizeof(std::aligned_storage_t<sizeof(T), alignof(T)>) * N;
n = N;
T* tmp;
if (s < RequiredSize) {
if (p) {
delete[] p;
}
s = RequiredSize;
std::cout << "Requiring " << RequiredSize << " bytes of storage\n";
p = new char[s];
// placement array default construction
tmp = new (p) T[N];
// T* tmp = reinterpret_cast<T*>(p);
// // optional for arithmetic types and also for trivially
// destructible
// // types when we don't care about default values
// for (size_t i = 0; i < n; ++i) {
// new (tmp + i) T();
// }
} else {
// placement array default construction
tmp = new (p) T[N];
// T* tmp = reinterpret_cast<T*>(p);
// // optional for arithmetic types and also for trivially
// destructible
// // types when we don't care about default values
// for (size_t i = 0; i < n; ++i) {
// new (tmp + i) T();
// }
}
return tmp;
}
// deallocate objects in buffer but not the buffer itself
template <typename T>
void Deallocate() {
T* tmp = reinterpret_cast<T*>(p);
// Delete elements in reverse order of creation
// optional for default destructible types
for (size_t i = 0; i < n; ++i) {
tmp[n - 1 - i].~T();
}
n = 0;
}
~Buffer() {
if (p) {
delete[] p;
}
}
};
int main() {
constexpr std::size_t N = 3;
Buffer B;
test_t* fb = B.DefaultAllocate<test_t>(N);
load(fb);
std::cout << fb[0] << '\n';
std::cout << fb[1] << '\n';
std::cout << fb[2] << '\n';
std::cout << alignof(test_t) << '\t' << sizeof(test_t) << '\n';
B.Deallocate<test_t>();
return 0;
}
For the sake of clarity, for arithmetic types, is it safe to remove altogether the placement new (replacing it by a mere reinterpret_cast<T*>(p)
) and the destructor loop?
Are there other kind of types for which it would also be possible?
NB: I'm using C++14 but I'm interested also in how it would be done in more recent standard versions.
link to question 1
link to question 3
[EDIT] this answer to question 3 shows that my C++14 snippet above might not be properly aligned: here is a proposed better version inspired from the referenced answer.
see also question 1 for some additional material.
can the placement
new
(lines 45 and 55) and the corresponding destructor loop (inDeallocate()
) be omitted in other case than arithmetic types?
No. The lifetime of an object starts with the constructor and ends with the destructor. Without those explicit calls, you'll just have memory, no objects.