Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding implicit lifetime creation and aliasing in C++

I am trying to wrap my head around the implicit lifetime and aliasing rules in C++.

The standard says:

Some operations are described as implicitly creating objects within a specified region of storage. For each operation that is specified as implicitly creating objects, that operation implicitly creates and starts the lifetime of zero or more objects of implicit-lifetime types in its specified region of storage if doing so would result in the program having defined behavior.

And:

An operation that begins the lifetime of an array of unsigned char or std::byte implicitly creates objects within the region of storage occupied by the array.

As well as providing an example that states:

// The call to std::malloc implicitly creates an object of type X

Does that mean that the following is legal and correct?

constexpr size_t N = 10;
constexpr size_t S = sizeof(uint32_t);
std::vector<std::byte> buffer;
buffer.resize(N * S);

for (size_t i = 0; i < N * S; i += S)
  *reinterpret_cast<uint32_t*>(&buffer.data()[i + 2 * S]) = 42;

uint32_t x;
for (size_t i = 0; i < S; ++i)
  *(reinterpret_cast<std::byte*>(&x) + i) = buffer[i];
assert(x == 42);

If not, what am I missing? And is there a way to make it legal using the C++23 subset that LLVM 17 (Clang and libc++) supports?

Note: Even though I tagged the question as "language lawyer", I myself am not one, so I would very much appreciate an explanation in as "simple" terms as possible.

like image 900
Alex O Avatar asked Oct 27 '25 13:10

Alex O


1 Answers

*reinterpret_cast<uint32_t*>(&buffer.data()[i + 2 * S]) = 42;

This is undefined behavior because you forgot std::launder. Without std::launder, you are accessing a std::byte through a glvalue of type uint32_t, which is UB because std::byte is not type-accessible through uin32_t. It doesn't matter whether a uint32_t exists in those bytes; reinterpret_cast without laundering doesn't give you a pointer to it.

As for whether implicit object creation takes place there: std::vector<std::byte> has to maintain an array of bytes internally ([vector.data] implies this), but not necessarily one where implicit objects for you are created. If std::vector simply allocated some bytes (with std::allocator, operator new, this is guaranteed) and gave you a pointer, then you could obviously use the implicitly created objects there, but it might do a lot more.

For example, it could do placement-new for each individual byte when setting them to zero, which would end the lifetime of any implicit uint32_t in the same place. You're at best relying on implementation details of std::vector with this code.

If not, what am I missing? And is there a way to make it legal using the C++23 subset that LLVM 17 (Clang and libc++) supports?

Yes, but ideally, don't use std::vector if you need storage for implicitly created objects. Use something like std::unique_ptr<std::byte[]>:

// obtain uninitialized, dynamically allocated byte[]
// objects are implicitly created inside (see [intro.object])
std::unique_ptr<std::byte[]> buffer
  = std::make_unique_for_overwrite<std::byte[]>(N * S);

for (size_t i = 0; i < N * S; i += S) {
  // obtain a pointer to the byte where the uin32_t is stored
  std::byte* byte = buffer.get() + i * S;
  // obtain a pointer to the uint32_t
  uint32_t* uint = std::launder(reinterpret_cast<uint32_t*>(byte));
  // overwrite its value with 42
  *uint = 42;
}

This code is still highly questionable because you could have just allocated a uint32_t[] in the first place. If all objects in your buffer have the same type, you could also simplify this code by doing:

uint32_t* integers = std::launder(reinterpret_cast<uint32_t*>(buffer.get()));
for (size_t i = 0; i < N; ++i) { // or use std::fill
  integers[i] = 42;
}

Note on alignment

Both the std::vector case and the std::unique_ptr case are "fine in practice" in terms of alignment. [basic.stc.dynamic.allocation] explains that for operator new:

the storage is aligned for any object that does not have new-extended alignment

i.e. you get the minimum guaranteed alignment of __STDCPP_DEFAULT_NEW_ALIGNMENT__, and this is going to be at least alignof(void*) and maybe alignof(max_align_t) in any sane implementation.

like image 142
Jan Schultke Avatar answered Oct 29 '25 04:10

Jan Schultke



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!