Is it undefined behavior to `reinterpret_cast` a `T*` to `T(*)[N]`?
Asked Answered
P

2

28

Consider the following scenario:

std::array<int, 8> a;
auto p = reinterpret_cast<int(*)[8]>(a.data());
(*p)[0] = 42;

Is this undefined behavior? I think it is.

  • a.data() returns a int*, which is not the same as int(*)[8]

  • The type aliasing rules on cppreference seem to suggest that the reinterpret_cast is not valid

  • As a programmer, I know that the memory location pointed by a.data() is an array of 8 int objects

Is there any rule I am missing that makes this reinterpret_cast valid?

Paleogeography answered 25/1, 2018 at 13:28 Comment(3)
std::array is required to be a contiguous container. Thus, the question boils down to whether an int * p such that [p; p+N) is known to be a valid pointer range can be casted to int[N].Bibbs
#47924603Piet
Misleading title. It's never UB to do a pointer cast. The potential problem would be accessing memory via dereferencing the result of the castSquelch
S
15

An array object and its first element are not pointer-interconvertible*, so the result of the reinterpret_cast is a pointer of type "pointer to array of 8 int" whose value is "pointer to a[0]"1.In other words, despite the type, it does not actually point to any array object.

The code then applies the array-to-pointer conversion to the lvalue that resulted from dereferencing such a pointer (as a part of the indexing expression (*p)[0])2. That conversion's behavior is only specified when the lvalue actually refers to an array object3. Since the lvalue in this case does not, the behavior is undefined by omission4.


*If the question is "why is an array object and its first element not pointer-interconvertible?", it has already been asked: Pointer interconvertibility vs having the same address.

1See [expr.reinterpret.cast]/7, [conv.ptr]/2, [expr.static.cast]/13 and [basic.compound]/4.

2See [basic.lval]/6, [expr.sub] and [expr.add].

3[conv.array]: "The result is a pointer to the first element of the array."

4[defns.undefined]: undefined behavior is "behavior for which this document imposes no requirements", including "when this document omits any explicit definition of behavior".

Saharan answered 25/1, 2018 at 15:10 Comment(10)
Are you saying that std::array<int, 8> doesn't contain any int[8] subobject, or are you saying the pointer doesn't point to that subobject? In either case, I think that would mean that a.data() + 3 is also invalid, and we know that's supposed to be valid.Knickers
@hvd I'm saying that the pointer points to a subobject of that int[8] subobject; i.e., it's still pointing to an element of the array, not the array itself. You cannot obtain a pointer to the array from a pointer to an element with reinterpret_cast.Saharan
Okay, when I get back on my computer I'll check to see if I can find anything to counter that, and if not, delete my answer. Meanwhile, your logic would suggest that still, std::launder is enough to make it valid. Do you agree?Knickers
@hvd Only if you meet the no-extra-reachability requirement, which will depend on the array at issue.Saharan
In the OP's case, that should not be an issue, as far as I can tell. Every byte of the int[8] subobject was already accessible through data().Knickers
@hvd The issue is whether you can access beyond that subobject with the resulting pointer. E.g., if std::array<int, 8> is something like struct {int e[8]; char dummy;}; then a pointer to that int[8] is pointer-interconvertible with a pointer to the entire struct and so can reach dummy, which makes the launder undefined.Saharan
Following this line of thought, is it impossible to violate strict aliasing rules with reinterpret cast except with unions?Moonmoonbeam
How can you though, when the pointer is unchanged by the cast unless there is interconvertibility?Moonmoonbeam
@PasserBy int i; *reinterpret_cast<float*>(&i) = 1; attempts to modify i through an lvalue of type float that refers to i.Saharan
That char dummy; would pretty much require malice on the implementer's part, but true, the standard doesn't seem to prohibit it...Knickers
C
7

Yes the behaviour is undefined.

int* (the return type of a.data()) is a different type from int(*)[8], so you are breaking strict aliasing rules.

Naturally though (and this is more for the benefit of future readers),

int* p = a.data();

is perfectly valid, as is the ensuing expression p + n where the integral type n is between 0 and 8 inclusive.

Carbolize answered 25/1, 2018 at 13:29 Comment(6)
I am casting the .data() though, not the array itself.Paleogeography
@VittorioRomeo: Oops. Corrected, the answer still holds though.Carbolize
in 99.9999% of the cases I see where this might be useful. I would suggest using a gsl::span to solve the problem insteadExecutant
I think this answer still is misleading or has a typo: *"int* is a completely different type from std::array<int, N>*" - I am not casting to std::array<int, N>*, but to int(*)[8] instead.Paleogeography
@VittorioRomeo: One more edit - I'm inclined to tin this.Carbolize
I don't think it's a strict aliasing violation to use lvalue of type int to access an object of type int (which happens to be a member of an array)Squelch

© 2022 - 2024 — McMap. All rights reserved.