Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Treating 2D array as 1D array

Tags:

c++

arrays

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 ints? 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?

like image 551
Killzone Kid Avatar asked Jun 11 '18 10:06

Killzone Kid


People also ask

How do you convert a two-dimensional array to a one dimensional array?

public static int mode(int[][] arr) { ArrayList<Integer> list = new ArrayList<Integer>(); int temp = 0; for(int i = 0; i < arr. length; i ++) { for(int s = 0; s < arr.

Can it be considered as 1D array or 2D array?

Arrays can be created in 1D or 2D. 1D arrays are just one row of values, while 2D arrays contain a grid of values that has several rows/columns. 1D: 2D: An ArrayList is just like a 1D Array except it's length is unbounded and you can add as many elements as you need.

Is a 1D array more efficient than a 2D array?

Unless you are talking about static arrays, 1D is faster. Clearly the 2D case loses the cache locality and uses more memory. It also introduces an extra indirection (and thus an extra pointer to follow) but the first array has the overhead of calculating the indices so these even out more or less.


1 Answers

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 Ts which has Y elements. p + X*Y therefore will be pointing out of range of this 1st array of Ts, 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;

like image 180
Killzone Kid Avatar answered Oct 06 '22 09:10

Killzone Kid