Suppose I want to create a memory pool so that I can control the memory locality of a large number of small, polymorphism-free objects. Naively I would do std::array<T, MAX_SIZE>
, or even just T[MAX_SIZE]
and then use placement new. However, both of these insist on initializing all of the objects, which in turn:
- Forces the objects to have a default constructor
- Could hurt my program's startup time if these objects are expensive
- Could open a massive can of static initialization worms if my buffer is static
So what I really want is the buffer contents to stay uninitialized until placement new is called on each slot. The solution I've found is to declare the buffer as std::array<char, MAX_SIZE*sizeof(T)>
instead, but this throws away type safety completely and is super ugly. Is there a better way?
Also, why is C like this? I find it super bizarre that C is so pedantic about uninitialized objects while it cheerfully destroys my object invariants via object slicing, and has no problem leaving my enums uninitialized...
CodePudding user response:
You cannot use std::array<char, MAX_SIZE*sizeof(T)>
as a backing storage for arbitrary types as there is no guarantee that the array will be well-aligned to alignof(T)
.
The idiomatic way to allocate uninitialized space is to use std::aligned_storage_t
. For your case, it would be std::aligned_storage_t<sizeof(T) * MAX_SIZE, alignof(T)>
. Then you can placement new (or use the standard functions related to uninitialized memory) at the correct offsets into this array without causing undefined behavior.
If you don't care about heap allocations, you should use std::vector<T>
as NathanOliver suggests in the comments.
CodePudding user response:
Since you will ever only store a single type in your pool, instead of std::aligned_storage_t
, you could also use a union with a single member:
template <class T>
union uninit_holder {
uninit_holder() {} // Intentionally empty
~uninit_holder() {} // Ditto
T val;
};
And use a std::array
with it:
std::array<uninit_holder<T>, MAX_SIZE> arr;
When you want to create one, you can placement new into the object and call std::destroy_at
to destroy one when you no longer need it.
(Since this answer proposes a completely different approach, I added another answer instead of editing my original one.)