Is this undefined behavior in C++?
Asked Answered
T

2

6

This example is copied from cppreference.

struct Y { int z; };
alignas(Y) std::byte s[sizeof(Y)];
Y* q = new(&s) Y{2};
const int f = reinterpret_cast<Y*>(&s)->z; // Class member access is undefined
                                           // behavior: reinterpret_cast<Y*>(&s)
                                           // has value "pointer to s" and does
                                           // not point to a Y object
const int g = q->z; // OK
const int h = std::launder(reinterpret_cast<Y*>(&s))->z; // OK

I wonder if adding operations like s[0] = std::byte{0} after statements above is undefined behavior? It seems that it doesn't disobey strict aliasing rule, for std::byte can be an "AliasedType" for any type according to cppreference, which means it's legal to view any object as array of bytes.

Notice that I add c++20 label because they may only be well-defined after C++20.

Tilla answered 5/12, 2022 at 9:12 Comment(9)
It is legal to view anything as an array of bytes but It is not legal to view an array of bytes as anything else.Bleareyed
"std::byte is a similar type to any type" AFAICT cppreference does not make any such claim. It is simply not true.Bleareyed
@n.m. I've changed my wording, sorry for that.Tilla
Yes, this behaviour is well-defined precisely because std::byte is an "AliasedType".Harker
@JMuzhen in the firtst reinterpret_cast std::byte is DynamicType and not AliasedTypeBleareyed
I would argue that the statement, "does not point to a Y object" is wrong, because it does point to a Y object ... one created (and constructed) by the placement new.Mycah
It 8s not about what can be AliasedType (anything can be), it is about what is AliasedType in a specific expression.Bleareyed
@AdrianMole the statement is correct. The pointer does not point to a Y object. A true statement can be made, e.g. "there is a Y object at the same address as the byte array object pointed to by the pointer". There is a subtle but important difference between the two. It is precisely because of this difference std::launder is needed.Bleareyed
I wonder if adding operations like s[0] = std::byte{0} after statements above is undefined behavior? You mean after new(&s) Y{2}? Yes, it is undefined behavior because it would be an access to an out-of-lifetime object.Stempien
M
2

I have updated my answer thanks to @LanguageLawyer's correction.


I wonder if adding operations like s[0] = std::byte{0} after statements above is undefined behavior?

I believe this is an undefined behavior, but the reason has nothing to do with strict aliasing rules. Strict aliasing rules state that if the program accesses an object of type D through a glvalue of type T but D and T are not "similar" and T is not one of those special character types, the program has undefined behaviors. In your case, you're accessing a std::byte object through a glvalue of type std::byte, which is perfectly fine under strict aliasing rules.

The real problem here is that s also provides storage for another Y object. Once s provides storage for the Y object (after the new expression), the lifetime of the array elements is terminated because their storage is reused:

[basic.life]/1.2:

The lifetime of an object o of type T ends when:

  • if T is a non-class type, the object is destroyed, or
  • if T is a class type, the destructor call starts, or
  • the storage which the object occupies is released, or is reused by an object that is not nested within o ([intro.object]).

So if you access s[0] after the new expression, you are accessing an out-of-lifetime object, which is an undefined behavior.

Minnesota answered 10/1, 2023 at 7:39 Comment(8)
Note that the std::byte[] is still alive because providing storage does not terminate the lifetime of the array object But does terminate the lifetime of its elementsStempien
@LanguageLawyer Can you give a reference to the corresponding standard text? I didn't expect an array to be alive while its elements are dead.Minnesota
Can you give a reference to the corresponding standard text? It is in your answer: «the storage which the object occupies ... is reused»Stempien
@LanguageLawyer Oh I understand. So the idea is that the array also provides storage for its elements, and the elements are terminated once the storage is reused.Minnesota
So the idea is that the array also provides storage for its elements Ehm, no, «provides storage» does not apply hereStempien
@LanguageLawyer OK. So the elements are terminated simply because their associated storage is reused by the new object.Minnesota
So if I hope to modify some bytes in the object *q, I need to code like std::byte* viewAsBytes = reinterpret_cast<std::byte*>(q); viewAsBytes[1] = std::byte{0x10};, but cannot use the original variable s, right?Tilla
@Tilla Exactly.Minnesota
B
0

It seems that it doesn't disobey strict aliasing rule, for std::byte can be an "AliasedType" for any type

While std::byte can be AliasedType for anything, it isn't in this fragment. Y is the AliasedType, for we are trying to access an object1 trough a glvalue of type Y2.

it's legal to view any object as array of bytes

It is indeed legal to view any object as an array of bytes. This reinterpret_cast, however, is trying to do exactly the opposite: view an array of bytes as some other type. This isn't and never was legal.

--
1In this case, s.
2In this case, *reinterpret_cast<Y*>(&s).

Bleareyed answered 5/12, 2022 at 11:47 Comment(1)
In the days before the Standard, such constructs were legal and 100% reliable on many compilers, if the bit patterns of the underlying types were compatible, and for many such constructs there was never any reason why quality compilers shouldn't support them. Note further that the C++ Standard does not define any category of conformance for C++ programs but only implementations. Unfortunately, the Stadnard can't add official support for such constructs because the most logical syntax would be the same syntax as was used before the Standard, and which should always have been supported.Haffner

© 2022 - 2024 — McMap. All rights reserved.