简体   繁体   中英

Treating 2D array as 1D array

Let's say we have a 2D int array:

int a[3][4] = { { 1,3,2,4 }, { 2,1,5,3 }, { 0,8,2,3 } };

Is it legal and valid to take its address and reinterpret it as a pointer to 1D array of int s? Basically:

int *p = reinterpret_cast<int *>(&a);

So that I can do things like (roughly):

template<typename T, size_t X, size_t Y>
void sort2(T(&arr)[X][Y])
{
    T *p = reinterpret_cast<T *>(&arr);
    std::sort(p, p + X*Y);
}

DEMO: https://ideone.com/tlm190

To my knowledge, the standard guarantees that alignment of 2D array would be contiguous in memory, and although p + X*Y technically is out of range is never accessed so should not lead to Undefined Behaviour either.

Can I absolutely treat 2D arrays as 1D arrays when needed?

Thank you all for replying and commenting, but I think the correct answer is - as it stands the code exhibits technical UB, though correctable. I have looked through some of those questions [ 1 , 2 ] @xskxzr linked and it led me to this quote from the standard :

If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_­cast . [ Note : An array object and its first element are not pointer-interconvertible, even though they have the same address . end note ]

Then on reinterpret_cast page there is the following note with an example:

Assuming that alignment requirements are met, a reinterpret_cast does not change the value of a pointer outside of a few limited cases dealing with pointer-interconvertible objects:

int arr[2];
int* p5 = reinterpret_cast<int*>(&arr); // value of p5 is unchanged by reinterpret_cast and
                                        // is "pointer to arr"

Even though this compiles without warning and runs , this is technically a UB because p5 is technically still a pointer to arr and not to arr[0] . So basically the use of reinterpret_cast the way I used it leads to UB. Taking the above into account, if I were to create int * directly to the 1st int (and this is ok according to the answer by @codekaizer), then this should be valid, right?:

template<typename T, size_t X, size_t Y>
void sort2(T(&arr)[X][Y])
{
    T *p = &arr[0][0]; // or T *p = arr[0];
    std::sort(p, p + X * Y);
}

But it probably is UB as well since the pointer p is pointing to the first T of the first array of T s which has Y elements. p + X*Y therefore will be pointing out of range of this 1st array of T s, hence UB (thanks again to @xskxzr for the link and comment).

If the expression P points to element x[i] of an array object x with n elements, the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i+j] if 0≤i+j≤n; otherwise, the behavior is undefined.

So here is my final attempt before I give up:

template<typename T, size_t X, size_t Y>
void sort2(T(&arr)[X][Y])
{
    T(&a)[X * Y] = reinterpret_cast<T(&)[X * Y]>(arr);
    std::sort(a, a + X * Y);
}

Here T arr[X][Y] is first converted to T a[X*Y] with, again, reinterpret_cast , which I think is now valid. The reinterpreted array a happily decays to a pointer to 1st element of array a[X*Y] ( a + X * Y is also within the range) and gets converted to an iterator in std::sort .

TL;DR version

Behaviour in the OP is Undefined because of improper use of reinterpret_cast . The correct way to convert 2D array to 1D array would be:

//-- T arr2d[X][Y]
T(&arr1d)[X*Y] = reinterpret_cast<T(&)[X*Y]>(arr2d);

An lvalue expression of type T1 can be converted to reference to another type T2. The result is an lvalue or xvalue referring to the same object as the original lvalue, but with a different type. No temporary is created, no copy is made, no constructors or conversion functions are called. The resulting reference can only be accessed safely if allowed by the type aliasing rules

Aliasing rules :

Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType, the behavior is undefined unless one of the following is true:

  • AliasedType and DynamicType are similar.

Type similarity :

Informally, two types are similar if, ignoring top-level cv-qualification

  • they are both arrays of the same size or both arrays of unknown bound, and the array element types are similar.

Array element type :

In a declaration T D where D has the form

D1 [ constant-expression opt ] attribute-specifier-seq opt

and the type of the identifier in the declaration T D1 is “ derived-declarator-type-list T ”, then the type of the identifier of D is an array type; if the type of the identifier of D contains the auto type-specifier , the program is ill-formed. T is called the array element type ;

From http://www.cplusplus.com/doc/tutorial/arrays/

int jimmy [3][5];   // is equivalent to
int jimmy [15];     // (3 * 5 = 15)  

when creating an array ( of any dimension ) the array memory is a fixed block of memory in size = sizeof(type) * dim0 * dim1 * ....;

So to your question, yes you can recast the array safely into one dimensional array.

Yes. It is legal and valid.

As per dcl.array :

If E is an n-dimensional array of rank i×j×⋯×k , then E appearing in an expression that is subject to the array-to-pointer conversion is converted to a pointer to an (n−1)-dimensional array with rank j×⋯×k. If the * operator, either explicitly or implicitly as a result of subscripting, is applied to this pointer, the result is the pointed-to (n−1)-dimensional array, which itself is immediately converted into a pointer.

A 2D array cannot be treated as a 1D array

As noted by @KillzoneKid, the address of an array is not pointer interconvertable with that of the first element even though they share the same address value.

constexpr provides a convenient way to evaluate UB. Compilers are required to detect UB when constexpr s are evaluated. Thus a simple test can be made. This function, when evaluated during compile time will detect accessing values beyond an array's range.

// sum "n" sequential ints
constexpr int sum(const int* pi, int n) {
    int sum = 0;
    while (n-- > 0)
        sum += *pi++;
    return sum;
};

When this is called at runtime with a pointer to the first element of a 1D or 2D array it will sum the elements but is UB if "n" goes beyond the boundary of the array. For a 2D array, this would be the extent of the lowest dimension. Not the size of the whole array.

Examining different instances of a pointer to an int we can see UB occurs once we try to access a value beyond the array dimension.

int main()
{
    constexpr int i0{1};
    constexpr int i1[2]{ 1,2 };
    constexpr int i2[2][2] = { { 1,2}, {3,4} };

    constexpr int sum0 = sum(&i0, 1);       // fails for n>1
    constexpr int sum1 = sum(&i1[0], 2);    // fails for n>2
    constexpr int sum2 = sum(&i2[0][0], 2); // fails for n>2
    const int sum3 = sum(&i2[0][0], 4);     // runtime calc, UB. fails if constexpr

    return sum0 + sum1 + sum2 + sum3;       // 17
}

For accesses beyond existing data, such as in sum0 and sum1, UB is clear. But sum2 points to existing data for n=[0:3) yet constexpr evaluation shows it to be UB for n=4.

I was somewhat surprised to learn this. Every compiler I've ever used worked as expected when doing things like scaling all the coefficients of a matrix by a fixed amount. But I can see rationales based on optimization assumptions that portions of a matrix won't change from the result of a function call on another part not in the same array sequence.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM