I've been trying to figure out how to access a mapped buffer from C++17 without invoking undefined behavior. For this example, I'll use a buffer returned by Vulkan's vkMapMemory
.
So, according to N4659 (the final C++17 working draft), section [intro.object] (emphasis added):
The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition (6.1), by a new-expression (8.3.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2).
These are, apparently, the only valid ways to create a C++ object. So let's say we get a void*
pointer to a mapped region of host-visible (and coherent) device memory (assuming, of course, that all the required arguments have valid values and the call succeeds, and the returned block of memory is of sufficient size and properly aligned):
void* ptr{};
vkMapMemory(device, memory, offset, size, flags, &ptr);
assert(ptr != nullptr);
Now, I wish to access this memory as a float
array. The obvious thing to do would be to static_cast
the pointer and go on my merry way as follows:
volatile float* float_array = static_cast<volatile float*>(ptr);
(The volatile
is included since this is mapped as coherent memory, and thus may be written by the GPU at any point). However, a float
array doesn't technically exist in that memory location, at least not in the sense of the quoted excerpt, and thus accessing the memory through such a pointer would be undefined behavior. Therefore, according to my understanding, I'm left with two options:
memcpy
the dataIt should always be possible to use a local buffer, cast it to std::byte*
and memcpy
the representation over to the mapped region. The GPU will interpret it as instructed in the shaders (in this case, as an array of 32-bit float
) and thus problem solved. However, this requires extra memory and extra copies, so I would prefer to avoid this.
new
the arrayIt appears that section [new.delete.placement] doesn't impose any restrictions on how the placement address is obtained (it need not be a safely-derived pointer regardless of the implementation's pointer safety). It should, therefore, be possible to create a valid float array via placement-new
as follows:
volatile float* float_array = new (ptr) volatile float[sizeInFloats];
The pointer float_array
should now be safe to access (within the bounds of the array, or one-past).
So, my questions are the following:
static_cast
indeed undefined behavior? new
usage well-defined? As a side note, I've never had an issue by simply casting the returned pointer, I'm just trying to figure out what the proper way to do this would be, according to the letter of the standard.
As per the Standard, everything involving hardware-mapped memory is undefined behavior since that concept does not exist for the abstract machine. You should refer to your implementation manual.
Even though hardware-mapped memory is undefined behavior by the Standard, we can imagine any sane implementation providing some obeys common rules. Some constructs are then more undefined behavior than others (whatever that means).
Is the simple
static_cast
indeed undefined behavior?volatile float* float_array = static_cast<volatile float*>(ptr);
Yes, this is undefined behavior and have been discussed many times on StackOverflow.
Is this placement-new usage well-defined?
volatile float* float_array = new (ptr) volatile float[N];
No, even though this looks well defined, this is implementation dependent. As it happens, operator ::new[]
is allowed to reserve some overhead1, 2, and you cannot know how much unless you check your toolchain documentation. As a consequence, ::new (dst) T[N]
requires an unknown amount of memory greater or equal to N*sizeof T
and any dst
you allocate might be too small, involving buffer overflow.
How to proceed, then?
A solution would be to manually build a sequence of floats:
auto p = static_cast<volatile float*>(ptr);
for (std::size_t n = 0 ; n < N; ++n) {
::new (p+n) volatile float;
}
Or equivalently, relying on the Standard Library:
#include <memory>
auto p = static_cast<volatile float*>(ptr);
std::uninitialized_default_construct(p, p+N);
This constructs contiguously N
uninitialized volatile float
objects at the memory pointed to by ptr
. This means you must initialize those before reading them; reading an uninitialized object is undefined behavior.
Is this technique applicable to similar situations, such as accessing memory-mapped hardware?
No, again this is really implementation-defined. We can only assume your implementation took reasonable choices, but you should check what its documentation says.