Home > Blockchain >  std::launder reachability rules
std::launder reachability rules

Time:08-17

std::launder example has this block of code:

int x2[2][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0])); 
// Undefined behavior: x2[1] would be reachable through the resulting pointer to x2[0]
// but is not reachable from the source

What? Example from std::aligned_storage makes it seem to me like that's not the case:

std::aligned_storage_t<sizeof(T), alignof(T)> data[N];

// Access an object in aligned storage
const T& operator[](std::size_t pos) const 
{
    // Note: std::launder is needed after the change of object model in P0137R1
    return *std::launder(reinterpret_cast<const T*>(&data[pos]));
}

Here's where confusion comes in: &data[pos] is just &data[pos][0], because &p == &p[0] where p is an array.

How come this is okay if std::aligned_storage can hardly if at all be implemented in any other way but something like alignas(T) std::byte[sizeof (T)];? Does the array being in a struct somehow magically make it okay? Why?

Let's say I have

template <typename T>
using uninitialized = alignas(T) std::byte[sizeof (T)];

constexpr auto N = 5;
uninitialized<int> array[N];
for (std::size_t i = 0; i < N; i  ) {
    new (&array[i]) int(i);
}

Now what? Isn't any cast like

auto laundered_value_ptr = std::launder(reinterpret_cast<int*>(&array[i]));

identical to the first example? What about this:

auto laundered_array_ptr = std::launder(reinterpret_cast<int*>(array));
laundered_array_ptr[0] = 9;
laundered_array_ptr[2] = 76;

If I follow, it seems like there's no way to use this memory correctly, because using a std::byte(*)[sizeof (int)] means that basically anything around it is reachable, and following first example, everything written in this question is may aswell be UB.

I compiled these examples using g -g -O0 -Wextra -Wall -Wpedantic -std=c 20 -fsanitize=undefined -fsanitize=address and curiously had gotten not even a warning which leaves me completely stumped.

I doubt this matters at all, but here's the version of compiler that I am using.

g   (GCC) 12.1.1 20220730

CodePudding user response:

You wrote:

&data[pos] is just &data[pos][0], because &p == &p[0] where p is an array.

In fact, that's not true. If p is an array (call its type T[N]), the expression &p == &p[0] will not compile. The left-hand side has type T(*)[N], and the right-hand side has type T*. These cannot be compared directly, any more than you could compare an int* with a char*.

However, static_cast<void*>(&p) == static_cast<void*>(&p[0]) will be true.

The two pointers represent the same address, but they are not substitutable for each other.

&data[pos] is a pointer to one element of data, and all other elements of data are reachable through it. On the other hand, (supposing that std::aligned_storage_t is an alias for an array of unsigned char) &data[pos][0] points only to a single element of an element of data; it does not point to the entire element of data. So all other elements of data[pos] are reachable through it, but other elements of data are not.

Since all elements of data are reachable through &data[pos], it follows that all bytes in data are reachable through &data[pos]. After this pointer is converted to const T*, the other T's that are stored inside the other elements of data are also reachable from the resulting pointer, but the bytes of those T's were also reachable from the original pointer, so the preconditions of std::launder are met.

If we used &data[pos][0] as the argument to reinterpret_cast, only the bytes in the single element data[pos] would be reachable from that pointer, and the preconditions would not be met.

  • Related