Is it legal to cast a pointer to array reference using static_cast in C++?
Asked Answered
O

2

20

I have a pointer T * pValues that I would like to view as a T (&values)[N]

In this SO answer https://mcmap.net/q/664452/-is-it-possible-to-reinterpret-pointer-as-dimensioned-array-reference, the proposed way of doing this is

T (&values)[N] = *static_cast<T(*)[N]>(static_cast<void*>(pValues));

The concern I have about this is. In his example, pValues is initialized in the following way

T theValues[N];
T * pValues = theValues;

My question is whether the cast construct is legal if pValues comes from any of the following constructs:

1:

T theValues[N + M]; // M > 0
T * pValues = theValues;

2:

T * pValues = new T[N + M]; // M >= 0
Osculate answered 23/1, 2014 at 19:3 Comment(2)
*static_cast<T(*)[N]>(static_cast<void*>(pValues)) seems to be abusing of language's features and rather an ugly workaround than something you would actually want to use...Ephrem
Your memory in both 1 and 2 would be dynamic - that is, the pointer is an actual pointer to the starting point of a block of memory, not an array that has been decayed to a pointer. Seems like it would be UB.Indelible
E
10

Short answer: You are right. The cast is safe only if pValues is of type T[N] and both of the cases you mention (different size, dynamically allocated array) will most likely lead to undefined behavior.


The nice thing about static_cast is that some additional checks are made in compile time so if it seems that you are doing something wrong, compiler will complain about it (compared to ugly C-style cast that allows you to do almost anything), e.g.:

struct A { int i; };
struct C { double d; };

int main() {
    A a;
    // C* c = (C*) &a; // possible to compile, but leads to undefined behavior
    C* c = static_cast<C*>(&a);
}

will give you: invalid static_cast from type ‘A*’ to type ‘C*’

In this case you cast to void*, which from the view of checks that can be made in compile time is legal for almost anything, and vice versa: void* can be cast back to almost anything as well, which makes the usage of static_cast completely useless at first place since these checks become useless.

For the previous example:

C* c = static_cast<C*>(static_cast<void*>(&a));

is no better than:

C* c = (C*) &a;

and will most likely lead to incorrect usage of this pointer and undefined behavior with it.


In other words:

A arr[N];
A (&ref)[N] = *static_cast<A(*)[N]>(&arr);

is safe and just fine. But once you start abusing static_cast<void*> there are no guarantees at all about what will actually happen because even stuff like:

C *pC = new C;
A (&ref2)[N] = *static_cast<A(*)[N]>(static_cast<void*>(&pC));

becomes possible.

Ephrem answered 23/1, 2014 at 19:24 Comment(3)
Okay, but this doesn't exactly answer my question as you are introducing two types (A and C), whereas I'm asking about the case where there is one (non-pointer non-reference) type T being involved.Osculate
IMHO static cast to void* should have been illegal by standard.Starkey
A (&ref)[N] = *static_cast<A(*)[N]>(&arr);: The static_cast doesn't do anything. &arr already has that type.Cormorant
C
2

Since C++17 at least the shown expression isn't safe, even if pValues is a pointer to the first element of the array and the array is of exactly matching type (including excat size), whether obtained from a variable declaration or a call to new. (If theses criteria are not satisfied it is UB regardless of the following.)

Arrays and their first element are not pointer-interconvertible and therefore reinterpret_cast (which is equivalent to two static_casts through void*) cannot cast the pointer value of one to a pointer value of the other.

Consequently static_cast<T(*)[N]>(static_cast<void*>(pValues)) will still point at the first element of the array, not the array object itself.

Derferencing this pointer is then undefined behavior, because of the type/value mismatch.

This can be potentially remedied with std::launder, which may change the pointer value where reinterpret_cast can't. Specifically the following may be well-defined:

T (&values)[N] = *std::launder(static_cast<T(*)[N]>(static_cast<void*>(pValues)));

or equivalently

T (&values)[N] = *std::launder(reinterpret_cast<T(*)[N]>(pValues));

but only if the pointer that would be returned by std::launder cannot be used to access any bytes that weren't accessible through the original pValues pointer. This is satified if the array is a complete object, but e.g. not satisfied if the array is a subarray of a two-dimensional array.

For the exact reachability condition, see https://en.cppreference.com/w/cpp/utility/launder.

Cormorant answered 18/2, 2022 at 0:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.