Can a std::array alias a fragment of a larger array?
Asked Answered
M

3

11

Suppose we have a pointer T* ptr; and ptr, ptr+1, … ptr+(n-1) all refer to valid objects of type T.

Is it possible to access them as if they were an STL array? Or does the following code:

std::array<T,n>* ay = (std::array<T,n>*) ptr

invoke undefined behaviour?

Mealie answered 8/4, 2016 at 20:42 Comment(0)
A
3

Yes, its an Undefined Behavior, a classic one...

First, understand that what you just did:

std::array<T,n>* ay = (std::array<T,n>*) ptr

can be translated as:

using Arr = std::array<T,n>;
std::array<T,n>* ay = reinterpret_cast<Arr*>( const_cast<TypeOfPtr>(ptr));

You've not just casted away all, const and volatile qualification but also casted the type. See this answer: https://mcmap.net/q/41091/-why-use-static_cast-lt-t-gt-x-instead-of-t-x ...indiscriminately casting away cv qualifications can also lead to UB.

Secondly, It is undefined behavior to access an object through a pointer that was casted from an unrelated type. See the strict aliasing rule (Thanks zenith). Therefore any read or write access through the pointer ay is undefined. If you are extremely lucky, the code should crash instantly. If it works, evil days are awaiting you....

Note that std::array is not and will never be the same as anything that isn't std::array.

Just to add... In the working draft of the C++ standard, it lists out explicit conversion rules. (you can read them) and has a clause stating that

.....

5.4.3: Any type conversion not mentioned below and not explicitly defined by the user ([class.conv]) is ill-formed.

.....


I suggest you cook up your own array_view (hopefully coming in C++17). Its really easy. Or, if you want some ownership, you can cook up a simple one like this:

template<typename T>
class OwnedArray{
    T* data_ = nullptr;
    std::size_t sz = 0;
    OwnedArray(T* ptr, std::size_t len) : data_(ptr), sz(len) {}
public:
    static OwnedArray own_from(T* ptr, std::size_t len)
    { return OwnedArray(ptr, len);  }

    OwnedArray(){}

    OwnedArray(OwnedArray&& o)
    { data_ = o.data_; sz = o.sz; o.data_=nullptr; o.sz=0; }

    OwnedArray& operator = (OwnedArray&& o)
    { delete[] data_; data_ = o.data_; sz = o.sz; o.data_=nullptr; o.sz=0; }

    OwnedArray(const OwnedArray& o) = delete;

    OwnedArray& operator = (const OwnedArray& o) = delete;    

    ~OwnedArray(){ delete[] data_; }

    std::size_t size() const { return sz; }

    T* data() return { data_; }

    T& operator[] (std::size_t idx) { return data_[idx]; }
};

...and you can roll out more member functions/const qualifications as you like. But this has caveats... The pointer must have been allocated the through new T[len]

Thus you can use it in your example like this:

auto ay = OwnedArray<decltype(*ptr)>::own_from(ptr, ptr_len);
Analogous answered 8/4, 2016 at 21:5 Comment(5)
Actuallu, std::array allocates its data when the compiled code that comes from a variable definition of type std::array is executed. Allocation always happens at runtime, except for character literals, and even then we're still in the land of implementation-defined behaviour.Hanselka
"It is undefined behavior to access an object through a pointer that was casted from an unrelated type" — Yes. This is called the strict aliasing rule, to which char is an exception.Comedic
@rubenvb, always is not correct here. Allocation for static objects happens at compile time.Feodor
I doubt that is guarantee to always be the case. It's a common optimization, sure, but namespace level static variables are initialized (and allocated) before main, not at compile time.Hanselka
@rubenvb, SergeyA, and zenith I have corrected it... ThanksAnalogous
F
2

Yes, this invokes undefined behaviour. Generally you can't cast pointers to unrelated types between each other.

The code is no different from

std::string str;
std::array<double,10>* arr = (std::array<double,10>*)(&str);

Explanation: Standard does not provide any guarantee for any compatibility between std::array<T,n> and T*. It is simply not there. It doesn't say that std::array is trivial type either. Absent such guarantees, any conversion between T* and std::array<T,n> is undefined behavior on the same scale as conversion between pointers to any unrelated types.

I also fail to see what is the benefit of accessing already constructed dynamic array as an std::array.

P.S. Usual disclaimer. Cast, on it's own, is always 100% fine. It is indirection of resulted pointer which triggers the fireworks - but this part is omited for simplicty.

Feodor answered 8/4, 2016 at 20:45 Comment(7)
This is not the same thing as the question, a string is an array of char.Speedboat
In the original question, the user has a valid pointer to an array of T, where T is the type in the array. In your example, T is string, but you do not have a pointer to an array of string of the same size. Your example sort of works if you change 10 to 1, but it's still rather misleading.Speedboat
@NirFriedman, you do not understand my example. (which I changed, to make it clear and compilable). There is no relationship whatsoever between std::array<N, T>* and T*. Zero, nada, nilch. You can't cast one to another. Moreover, std::array is not a trivial type, so it is not even a type-punning issue (which some people tend to go along), it is a very basic mistake.Feodor
std::array is guaranteed (not directly, but indirectly through the wording, #19103744) to begin with N copies of type T. So the two types in question have a common prefix. It's still UB but it's clearly different from your example.Speedboat
@NirFriedman, undefined behavior is still undefined behavior, and my example highlights it, unless you can point me to the point in standard which allows this conversion.Feodor
Your example doesn't highlight it, because the user clearly has an example where they think it may not be UB, and their example will probably "work" on nearly any implementation. Yours will not. So your example is not illustrative. You may as well delete your example and just write "Yes".Speedboat
I can only repeat myself. Since Standard provides no guarantees of compatibility between a pointer to T and std::array<T> (the same way it does not provide compatibility between T and `std::array<double>) both attempts display equall undefined behavior. Will put the same text into my answer.Feodor
U
0

I'm answering the first question here, as the second one has already been treated in the other answers:

Recap: you ...

have a pointer T* ptr; and ptr, ptr+1, … ptr+(n-1) all refer to valid objects of type T.

And you ask whether it is ...

possible to access them as if they were an STL array?

Answer: This is no problem -- but it works differently as you estimated in your code example:

std::array<T*, N> arr;
for(int i = 0; i<N; ++i)
{
    arr[i] = ptr + i;
}

Now you can use the array-elements as if they were the original pointers. And there is no undefined behaviour anywhere.

Unreal answered 8/4, 2016 at 21:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.