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 first one, see below) is does the following code breaks the strict-aliasing-rule (is it UB?) and, if not, why (I suspect it to be correct but strict-aliasing-rule discussions made me cautious).
#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;
}
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 2
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.
subsidiary question: why does gcc issues a warning in godbolt version? I nether required inlining of default constructor?
I leave it also below for the record.
The interesting part, IMHO, is the use of std::function with lambda to type-erased the destructors and deleters.
My question regarding the correctness of this code is still valid:
- is there an UB?
- if the strict aliasing rule is not violated, why?
- btw: what, in the standard, describes the strict aliasing rule?
#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
std::function<void()> Destructors = [] {};
// freeing functor storage
// required to call the correct buffer deleter
std::function<void()> FreeBuf = [] {};
// 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) {
// Destroy previously stored objects
Destructors();
size_t RequiredSize =
sizeof(std::aligned_storage_t<sizeof(T), alignof(T)>) * N;
if ((s < RequiredSize) ||
(reinterpret_cast<std::size_t>(p) % alignof(T) != 0)) {
// not enough room or misaligned
FreeBuf();
s = RequiredSize;
std::cout << "Requiring " << RequiredSize << " bytes of storage\n";
// using new and std::aligned_storage_t to provide correct alignment
p = reinterpret_cast<char*>(
new std::aligned_storage_t<sizeof(T), alignof(T)>[N]);
// create/update free functor
FreeBuf = [this] {
std::aligned_storage_t<sizeof(T), alignof(T)>* ToFree =
reinterpret_cast<
std::aligned_storage_t<sizeof(T), alignof(T)>*>(p);
delete[] ToFree;
};
}
// placement array default construction
T* tmp = new (p) T[N];
// update nb of objects
n = N;
// create destructors functor
Destructors = [this] {
T* ToDestruct = reinterpret_cast<T*>(p);
// Delete elements in reverse order of creation
while (n > 0) {
--n;
ToDestruct[n].~T();
}
};
return tmp;
}
// deallocate objects in buffer but not the buffer itself
template <typename T>
void Deallocate() {
Destructors();
}
~Buffer() {
Destructors();
FreeBuf();
}
};
int main() {
constexpr std::size_t N0 = 7;
constexpr std::size_t N1 = 3;
Buffer B;
std::cout << "Test on Float\n";
SFloat* Fb = B.DefaultAllocate<SFloat>(N0);
load(Fb);
std::cout << Fb[0] << '\n';
std::cout << Fb[1] << '\n';
std::cout << Fb[2] << '\n';
std::cout << alignof(SFloat) << '\t' << sizeof(SFloat) << '\n';
std::cout << "Test on float\n";
// reallocating, possibly using existing storage
float* fb = B.DefaultAllocate<float>(N1);
load(fb);
std::cout << fb[0] << '\n';
std::cout << fb[1] << '\n';
std::cout << fb[2] << '\n';
std::cout << alignof(float) << '\t' << sizeof(float) << '\n';
return 0;
}