user9723177
user9723177

Reputation:

How to create objects in C++ with dynamic alignment requirements?

What is the correct way in C++ to allocate and use a buffer with dynamically-specified alignment? The use case that I had in mind was Vulkan dynamic uniform buffers (see this previous question which discusses the required process in the abstract), for which a constraint on alignment is given via the minUniformBufferOffsetAlignment property on VkPhysicalDeviceLimits, which is not known at compile time.

I initially thought I might be able to use operator new(std::align_val_t) doing something like

Foo* buffer = new(std::align_val_t{alignment}) Foo[n];

but that doesn't compile (at least on MSVC).

I've also watched Timur Doumler's CppCon presentation "Type punning in modern C++" which notes that using reinterpret_cast on the result of something like std::aligned_alloc leads to undefined behaviour.

So far I've come up with the following:

std::size_t n = getNumberOfElements(); // possibly not known at compile time
std::size_t alignment = getRequiredAlignment(); // not known at compile time

makeSureMultiplicationDoesNotOverflow(sizeof(Foo), n); // details irrelevant

void* storage = std::aligned_alloc(sizeof(Foo) * n, alignment); // _aligned_malloc on MSVC
if (!storage) { std::terminate(); }
Foo* buffer = new(storage) Foo[n];

// do stuff with buffer

for(std::size_t i = 0; i < n; ++i) { buffer[i].~Foo(); }
std::free(storage); // _aligned_free on MSVC

Have I missed something here that's going to cause undefined behaviour?

EDIT: I noticed that the above does not apply the alignment to any objects other than the first one, so that's definitely an oops...

(Obviously in a real application this should be encapsulated into a class to provide RAII, but leaving that out for now so as not to bloat the example code.)

Upvotes: 3

Views: 689

Answers (2)

Evg
Evg

Reputation: 26292

You can do the following:

auto storage = static_cast<Foo*>(std::aligned_alloc(n * sizeof(Foo), alignment));
std::uninitialized_default_construct_n(storage, n);
auto ptr = std::launder(storage);

// use ptr to refer to Foo objects

std::destroy_n(storage, n);
free(storage);

Casting a pointer returned by std::aligned_alloc to Foo* is not undefined behavior. It would be UB if you tried to dereference it right after std::aligned_alloc before std::uninitialized_default_construct_n created Foo objects.

Edit.

The code above is technically undefined behavior. But it seems that in C++ there is no 100% standard-conforming way to do such an allocation without UB. From the practical point to view this code is reliable and safe. std::launder(storage) should probably be used to access Foo objects through storage pointer.

See this question for details and discussions.

Upvotes: 4

walnut
walnut

Reputation: 22152

You already have an answer with a better alternative, but I just want to add that your code has undefined behavior.

The array form of new, even the placement-new form that you are using, is allowed to use an unspecified amount of memory from your allocation to store metadata for the runtime (e.g. the size of the allocation for delete[] to know how many elements it needs to destroy).

Therefore you have no way of guaranteeing that the allocated memory size is sufficient or that the actual pointer to the array returned by the placement-new is properly aligned.

As far as I can tell from the current draft this seems to change in C++20, making this particular use of the array placement-new well-behaved, but it is not required to be so in C++17 or earlier.

Upvotes: 1

Related Questions