Is it undefined behaviour to access two objects of type T declared next to each other using T[]?

Viewed 164

I recently watched a CppCon talk by Miro Kenjp on "Non-conforming C++: the Secrets the Committee Is Hiding From You".

At 28:30 (https://youtu.be/IAdLwUXRUvg?t=1710) He states that accessing doubles next to each other was UB and I don't quite understand why. Can someone explain why is this the case?

And if this is the case then surely its UB to access other things this way for example:

int* twoInts = malloc(sizeof(int) * 2);
int secondInt = twoInts[1]; //Undefined behaviour?
2 Answers

I want to start with a quote from the presentation:

You are not programming against the CPU, you are programming against the abstract machine

You say:

He states that accessing doubles next to each other was UB

But your quote is incomplete. He specifies this very crucial fact:

... unless the objects are part of an array

malloc is a red herring (and a bag of another set of problems). His code uses new[] so malloc is just poisoning the well here.

The specific problem he mentions on his slides is that the double objects created on the buffer are created by std::uninitialized_default_construct_n and this method doesn't create an array of doubles, but instead creates multiple objects that are consecutive in memory. He asserts that in the C++ standard (the abstract machine you are programming against) you can't treat objects as part of an array unless you actually created the array of objects.

The point the author tries to make is that the C++ standard is flawed and there is no strictly conforming way to create a flexible array (pre C++20).


For reference here is the code (reproduced after image):

struct header
{
    int size;
    byte* buffer;
    thing some;
};
constexpr size_t x = ...;

byte* buffer = new byte[x + n * sizeof(double)];
header* p = new (buffer) header{n, buffer};
uninitialized_default_construct_n(
    reinterpret_cast<double*>(buffer + x), n);
double* data = reinterpret_cast<double*>(p->buffer + x);

data[0] = data[1] + data[2]; // <-- problem here
                             // because we never created an array of doubles

There is no guarantee that the variables will be allocated next to each other.

  1. The memory area used by malloc may be a different region than the memory area for temporary, local, variables.

  2. Local variables may not be stored in memory. They could be stored in registers.

  3. Accessing array elements outside the declared range is undefined behavior.
    The array could be allocated at the end of memory, so accessing outside the array has no memory assigned to it.

  4. Local variables may be defined in another memory segment, e.g. stack., that is not the same as memory for dynamic allocation.

Here's an example.
An embedded system has On-Chip Memory and memory outside the "System on the Chip" (SOC). The On-Chip memory is faster, but there is less of it. The architects assign this memory to the stack. The memory outside the SOC is slower, but there is more of it, so it is assigned to the dynamic memory.

Another example:
The operating system supports virtual memory. Memory is paged onto the hard drive as needed. The operating system assigns a small amount of memory to your program. The small amount of memory will be assigned to the stack and the virtual memory will be assigned to dynamic memory.

Not all platforms are made of contiguous memory. Memory locations may also be assigned to hardware devices.

Related