﻿# I \_\_\_\_\_ hate arrays in C\+\+\!

Or why I think developers need to know about them but should not use them\.

![1134_IDontLikeArrays/image1.png](https://import.viva64.com/docx/blog/1134_IDontLikeArrays/image1.png)



## Introduction

Do you remember the first time you put a pointer to the first array element to the _sizeof_ operator, and your code stopped working the way you intended? It certainly doesn't come close to the thrill of sticking your fingers in a power socket but\.\.\.

Here's an array:  

```cpp
int arr[5] = {1, 2, 3, 4, 5};
```

And here it turned into a pointer:

```cpp
int *ptr = arr;
```

The [array\-to\-pointer conversion happened](https://timsong-cpp.github.io/cppwp/n4950/conv.array#1), we lost information about the array size and some nerve on top of that\. Don't get me wrong: before using an array, one should first learn how to "cook" it, just like any other feature of our favorite language\. But I don't like it when language rules seem to be constantly trying to trick the programmer and make things more complicated for no reason\.

I think the problem is that this infamous _array\-to\-pointer_ _conversion_ imposes a certain mindset\. It assumes that an array and a pointer to its first element are absolute equivalents and always lead to the same behavior when the same code is used\. However, arrays and pointers are far from being interchangeable\. Moreover, C\+\+ has contexts where you may regret using one instead of the other\.

## Pointer isn't array

First, let's get some very basic things straight from the start\. A pointer has its own type, namely the _compound_ _type_ of the _pointer\-to\-T\._ An array is a separate entity, so it has a different type than a pointer\. Why don't we check it right off the bat?

To determine the type, let's use the _typeid_ built\-in operator and the _name\(\)_ function that returns a string containing the type name\. The _typeid_ operator works over RTTI, so the string differs depending on the compiler\. However, the type is still determined as the same one\.

<details>
   <summary>NOTA BENE for those who dislike typeid\\\.</summary>

Yes, _typeid_ discards references and _cv\-_qualification\. We don't apply these concepts specifically in this part of the article\. If we had used _typeid_ to find out how a type is deducted in templates, we'd be stepping on a massive rake\. Fortunately, there are no such rakes in our garden, we're just looking at the type of the explicitly declared variable\.


</details>
We declare an array and look at its type:

```cpp
int arr[3] = { 1, 2, 3 };
std::cout << typeid(arr).name() << std::endl;
```

Our compiler defines it as _A3\_i_, an array of three elements of the _int_ type\.

At the same time, we can use the _typeid_ operator on a pointer to the first element of an array:

```cpp
int *ptr = arr;
std::cout << typeid(ptr).name() << std::endl;
```

In this case, _Pi_, which is a _pointer_ _to_ _int_, is printed to _stdout_\.

If the types are different, where does the confusion come from? 

## Array\-to\-pointer conversion 

The confusion in using arrays and pointers comes from the fact that C\+\+, as well as its direct ancestor C, has contexts where an array becomes a pointer\. Technically, it's hard to find contexts where an array is used by itself\! Let's leave the reasons for these sudden changes out of this article and just say thanks to good old [Dennis Ritchie](https://en.wikipedia.org/wiki/Dennis_Ritchie)\. 

This is called _array\-to\-pointer_ _conversion_\. It occurs in cases defined by the C\+\+ standard, causing confusion among beginners\. Things are complicated by the fact that the C\+\+ standard doesn't have a single list of cases where such conversion occurs\. We have to retrieve them throughout the text, as if we're completing another quest in some Korean MMORPG\.

### When conversion doesn't occur

Let's first look at the cases where conversion does not occur\.

#### The discarded\-value expression

The _discarded\-value expression_ is an expression whose result is not used\. No conversion occurs in the example below:

```cpp
int arr[3] = { 1, 2, 3 };
arr;
```

#### typeid

As we've already seen at the very beginning of the article, the _typeid_ operator can handle arrays and returns different strings for arrays and pointers\.

#### sizeof

The same applies to the _sizeof_ operator\. It can count sizes of both arrays and pointers\. The code below works fine:

```cpp
int arr[3] = { 1, 2, 3 };
auto ptr   = arr;
static_assert(
    sizeof(arr) != sizeof(ptr)
);
```

#### References

References\. Operations that would otherwise lead to the _array\-to\-pointer_ conversion would be performed on an array itself when references are used\. You can pass an array to a function by reference\. You can deduce a reference to an array in a template\. Referances are cool\. Be like a reference\! 

Be careful with function overloading\! If a compiler has to choose between a function that takes a reference and a function that takes a pointer, it prefers not to choose at all\. The code below doesn't compile because both functions fit equally well:

```cpp
void foo(int (&a)[3]) {};
void foo(int *p) {};

int main()
{
    int arr[3] = { 1, 2, 3 };
    foo(arr);
}
```

### When conversion does occur 

Back in C days, it was easier\. There were \(and still are\) several cases where the _array\-to\-pointer_ _conversion_ didn't happen\. So, in all other cases, it did\. C\+\+ is a slightly more complex language, so you can't get around this rule anymore\. Well, let's examine the cases where conversion occurs\.

#### Built\-in operators

We'll start with operators\. All these addition, subtraction, division, multiplication, indexing, and other built\-in binary and unary operators can't work with arrays\. If you try to add up two arrays, they convert to pointers\. This doesn't mean of course that we can add up two pointers\. This operation is meaningless at the very least, and at most not part of the standard\.  

So, if that's how it works with built\-in operators, what about user\-defined operators?

#### Functions

Overloaded operators, like other functions, take arguments\. The standard says that an _array\-to\-pointer_ _conversion_ is applied to function arguments when an argument is an array\. This means that an array itself can't be passed to a function, only a pointer to its element can be\. 

Even if you write a function parameter as an array:

```cpp
int foo(int arr[3]);
```

Inside the function, the _arr_ parameter is a pointer\. The array isn't copied\.

#### Cast operators

When using _static\_cast_ and _reinterpret\_cast_, we can't convert anything to an array: the output results in a pointer to an element\.

Strictly speaking, we can't convert something to an array using _const\_cast_ and _dynamic\_cast_ either\. The other thing is, these two simply don't compile when we try to perform such a conversion\. 

#### Ternary operator

If the second or third operators of the ternary operator \(the one with the question mark\!\) are arrays, the operator returns a pointer instead of an array\.

#### Template parameters

As far as arrays are concerned, _non\-type_ template parameters are similar to function parameters\. If we declare a parameter as an array, it turns out to be a pointer, actually:

```cpp
template <int arr[3]>
void foo();
```

#### Template arguments deduction

Arrays used as template arguments in a function call are deducted as pointers\. In the example below, the template parameter is deducted as a pointer:

```cpp
template <typename T>
void foo(T arr) {};
//....
int arr[3] = { 1, 2, 3};
foo(arr);
```

#### Conversion function template

The same applies to conversion function template\. Arrays become pointers:

```cpp
template <typename T>
struct A {
    operator T() {
        static int arr[3];
        return arr;
    }
};
```

Don't blame it on me\. I didn't force you to learn C\+\+\.

### Enough with the theory\. Let's shoot in a foot

I doubt you started reading this article for the sake of theoretical research that you, dear reader, have probably already done\. However, it was necessary to smoothly guide the narrative to real\-life aspects that can unexpectedly pop up in a programmer's text editor, making developers exclaim, "I hate C\+\+ arrays\!"

![1134_IDontLikeArrays/image2.png](https://import.viva64.com/docx/blog/1134_IDontLikeArrays/image2.png)

### Function that takes pointer to base class

Let's imagine we have two classes\. One is the base one, and the other derives from it:

```cpp
struct B {
    char i;
};

struct D : B {
    int y;
};
```

Besides that, there's a function specified somewhere nearby that traverses an array of base class objects:

```cpp
void foo(B *b, size_t size)
{
    for(auto &&el : std::span(b, size)) {
        std::cout << el.i << std::endl;
    }
}
```

There's a catch: it takes a pointer and some size\. Such a composition gives some creative freedom to a careless programmer who may write something like the following code:

```cpp
int main()
{
    D arr[3] = { {'a', 1}, {'b', 2}, {'c', 3} };
    foo(arr, 3);
}
```

This is definitely UB\. But that's not the issue here, or at least not the only one\. 

In the example above, the next element in the loop isn't calculated correctly\. Rather than displaying the next object _B::i _member, the program displays the _padding_ _bytes_\. They follow the variable and were added by a compiler for alignment purposes\. In our compiler, _sizeof\(B\)_ is one and _sizeof\(D\)_ is eight\.

Now let's consider a case where both of these classes are polymorphic\. We add virtual functions to them:

```cpp
struct B {
    char i;
    B(char i) : i(i) {};
    virtual void print() { std::cout << "BASE " << i << "\n"; }
};

struct D : B {
    int y;
    D(char i, int y) : B(i), y(y) {};
    void print() { std::cout << "DERIVED " << i << " " << y << "\n"; }
};
```

We can [create an example](https://godbolt.org/z/YfKhM3zfn) where this change affects how the program looks and behaves\. In the linked example, you can see how the _virtual_ keyword alone causes the elements in the loop to be calculated correctly\. However, UB remains\.

In the example above, the behavior is caused by the implementation of polymorphic classes used in the compiler\. A reasonable but not mandatory way to implement polymorphism is to add the _vtable_ pointer to objects of a polymorphic class that points to a table containing various information about an object class\. Apparently, in our case, the pointer is added to the end of the structure, causing the compiler to align the entire structure size to that pointer\. To do this, the compiler adds 7 _padding_ _bytes_ after the _i_ variable in the _B_ class objects and 3 bytes after the same variable when using the _D_ class objects \(since 4 bytes go to the _y_ variable\)\. As a result, the size of both structures becomes the same, and the iteration runs correctly\. But if we change the type of the _y_ variable to _long_, we won't be so lucky anymore\.

The big inconvenience here is that the compiler doesn't warn about this, since converting a pointer to a derived class to a pointer to the base class is supported by the language rules\. So, we can imagine a case where the code works with one compiler and platform \(even though it shouldn't\), and crashes under other circumstances\. If the function had accepted parameters like _std::array_ or _std::span_, there would have been no issues\.

### Lambda

Let's look at the following code:

```cpp
#include <iostream>

int main()
{
    int arr[3] = {1, 2, 3};

    auto sizeof_1 = [arr] {
        return sizeof(arr);
    };

    auto sizeof_2 = [arr = arr] {
        return sizeof(arr);
    };

    auto sizeof_3 = [=] {
        return sizeof(arr);
    };

    std::cout << sizeof_1() << std::endl;
    std::cout << sizeof_2() << std::endl;
    std::cout << sizeof_3() << std::endl;
}
```

What do we know about lambda expressions? Well, they have captures\. Captures capture \(\!sic\) variables by value \(\=\) or by reference \(&\)\. Captures are implemented by the compiler creating a service class where each captured variable is a non\-static class field\.

If a variable is written without ampersand and yet isn't _this_ variable, it's passed by value\. In the code above, all arrays are passed by value\. So, they are all cast using the _array\-to\-pointer_ _conversion_\. This means that the program displays the same number three times\. 

Alternatively, as we can read on [cppreference](https://en.cppreference.com/w/cpp/language/lambda), lambda data members that are in a capture expression without an initializer go through _direct\-initialization_\. If an array is captured, each of its elements is initialized via _direct\-initialization_ in the ascending index direction\. So, the program displays the same numbers, just not the ones we had in mind\.

Or, as we can also read there, if there's an initializer, the variable to be captured is initialized in the way the initializer dictates\. The previously declared array can be used to initialize only a variable of the _pointer_ _to_ _that_ _array_ _element_ type\. So, when we write _\[arr_ _\=_ _arr\]_ in the capture, the pointer to the first element is still captured, unlike in other ways of capture by value notation\. 

This little detail is easy enough to overlook, so one may, for example, overwrite the external array elements when the second type \(mentioned above\) lambda expression is used\.

It seems logical, but there are still some mixed feelings\. Most importantly, we've found a C\+\+ context where we can still implicitly copy an array without resorting to library functions\!

Be careful, programmer: using a regular pointer instead of an array in this context displays the size of the pointer itself in all three cases\!

### Iteration

However, enough has been said about array copying\. Let's address iterating over it as well\.

There are currently two types of iteration in C\+\+: the classic _for_ loop and its _range\-based_ version\. We can use both to iterate over arrays\. Both have known issues with iterating through a pointer\.

We'll cover the nuances of using the classic _for_ loop in the next chapter of the article\. In this one, however, we'll focus on its _range\-based_ little brother\. Let's have a quick recap of how it works\.

```cpp
int arr[3] = {1, 2, 3};
for(auto &&element : arr) std::cout << element << std::endl;
```

Under the hood, it expands into a regular _for_ loop that handles iterators\. This structure also works correctly if you create the _prvalue_ array in place of the _arr_ variable in the loop\. The temporary array is [bound to the _forwarding_ reference](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n4950.pdf) and exists until the loop ends\.

Some readers may have already started typing in the comments below that the _range\-for_ loop example is incorrect because it uses library functions to get iterators\. Like, the _element_ parameter would be obtained using the _std::begin_ library function \(or _std::cbegin_, depending on the element _const_\-ness\), and the iterator pointing to the array boundary is obtained via _std::end_ \(or _std::cend_\)\. Indeed, these functions have array overloads\. But be careful, programmer, because at the same link to the standard, you can read that iterating over arrays doesn't involve iterators—only good old pointers\. 

At the same time, you will come a cropper trying to replace the array with the pointer\. The following code doesn't even compile:

```cpp
int *ptr = arr;
for(auto &&element : ptr) std::cout << element << std::endl;
```

If pointers in a _range\-based_ loop lead to unbuildable code, using them in its big brother _for_ can be even more nasty\.

### Iteration over multidimensional array

Let's say we have a multidimensional array—a matrix of integers, for example:

```cpp
int arr[2][2][2] = { 0, 1, 2, 3, 4, 5, 6, 7 };
```

Suddenly, we need to iterate over this array to change each value\. We know that when indexing an array of the _T\[N\]_ type, we get back _T_\. In our case, _T_ is _int\[2\]\[2\]_\. We can also apply the indexing operation to the obtained construction, thus getting an object of the _int\[2\]_ type and once again finally reaching the desired _int_\. In fact, if we do it via regular _for_ loops, we need three of those\.

We also know that the standard guarantees that array elements are arranged one after the other\. In fact, the rule applies recursively to all parts of a multidimensional array\. All elements of the _int\[2\]\[2\]_ type are arranged in sequence and within them, all elements of the _int\[2\]_ type are arranged in sequence, and so on\.

Of course, everybody knows that taking this logic too far is dangerous to the program health\. The code below is incorrect:

```cpp
#include <iostream>

int main()
{
    int arr[2][2][2] = { 0, 1, 2, 3, 4, 5, 6, 7 };
    for(size_t i = 0; i < 8; ++i) {
        std::cout << arr[0][0][i] << std::endl;
    }
}
```

Here we try to access elements of the _int_ type via the very first _int\[2\]_ sub\-array of the very first _int\[2\]\[2\]_ sub\-array, thus going beyond no less than three array boundaries\. But elements of the _int_ type are still in memory one after the other—the standard ensures it\! Indeed, it does, as well as ensuring that the behavior is undefined for the given code\.

UB [is _malum in se_\.](https://pvs-studio.com/en/blog/posts/cpp/1024/) What's worse is that the code may work quite well because the elements are actually arranged in a sequence\. Or it may crash pretty hard\. If you don't believe us, [you can see for yourself](https://godbolt.org/z/bEhdqecW4)\.

Great\! We know that adhering to the standard is a good thing\! Let's write three loops\. But what if the array is four\-dimensional, though? And what if it's five\-dimensional? What if templates come into play and the dimensionality can be whatever you want?

Do the dark forces possess some sorcery after all? Some magic that may allow us to get around the restriction, maybe\. Let's rewrite the code as follows:

```cpp
#include <iostream>
#include <type_traits>

int main()
{
    int arr[2][2][2] = { 0, 1, 2, 3, 4, 5, 6, 7 };
    
    auto ptr = reinterpret_cast<std::remove_all_extents_t<decltype(arr)>*>(arr);
    for(size_t i = 0; i < 8; ++i) {
        std::cout << ptr[i] << std::endl;
    }
}
```

Let's say some evil genius decided to get the type of an element in a multidimensional array using _std::remove\_all\_extents\_t_\. Then they cast that array to a pointer to that element using _reinterpret\_cast_\. In fact, such intricacies result in an analog of the _flat\(\)_ function in other programming languages\. That function squashes a multi\-dimensional array into a one\-dimensional one\. We even save on pointer arithmetic by adding only one index to _ptr_ instead of three, as in the case of _arr_\.

Unfortunately, this is still UB\. In this case, in addition to crossing array boundaries, _strict_ _aliasing_ rules also come into play: we can't access an object using a type other than the one it was created with\. Another thing is that when we use the same compiler as in the previous example, the [current example](https://godbolt.org/z/eEsW93aKn) not only compiles but also runs without crashing, even with the sanitizer enabled\. 

Be careful, friends, arbitrary conversions between array types won't keep a doctor away\!

### Array size

Much has already been said about the issues one may face when mixing up an array and a pointer to its first element in the _sizeof_ operator arguments\. We won't repeat ourselves and invite you to read more about it [here](https://pvs-studio.com/en/blog/posts/cpp/1112/)\.

### What's next?

We hope you didn't take this article as a criticism of _built\-in_ arrays in C\. After all, they had their place and their use there, due to the language particularities\.

However, we're talking about C\+\+ now\. So, it'd be wrong to leave an absolutely logical question unanswered at the end of the article\. This question goes something like, "What should we do in C\+\+ with such an irritating feature of the C language?" 

Well, we couldn't come up with a better answer than "use _std::array_ or _std::span_"\.

The issue with using a pointer to a base class when iterating over an array of derived class objects is solved by using _std::array_ or _std::span_\. The compiler doesn't allow us to push an array of derived elements into an array of base elements\.

In "_lambdas_", we talked about the degenerate case of passing an array to _captures_ by value_,_ where the behavior is different\. Again, _std::array_ helps\. In all cases where it's passed by value, all elements are fully copied\. When it comes to _std::span_, the elements aren't copied, again, in all three cases\. 

"_Iterating_" over _std::array_ and _std::span_ works like a charm\. You can choose between the regular _for_, the _range\-based_ one, or library functions\. 

However, _std::array_ and _std::span_ can't help much with _iterating_ _over_ a _multidimensional_ _array_\. If one tries hard enough, they can make similar mistakes with them too\. Moreover, it's a pain in the neck to declare multidimensional _std::array_ and _std::span_\. Well, not every cloud has a silver lining, right? But if we want to declare multidimensional stuff in a compact way, and C\+\+ 23 is already used in the project, we can consider using [_std::mdspan_](https://en.cppreference.com/w/cpp/container/mdspan)\. 

![1134_IDontLikeArrays/image3.png](https://import.viva64.com/docx/blog/1134_IDontLikeArrays/image3.png)

### Conclusion

That's it\. One should know how to use built\-in arrays, but here's a question: are they worth it when C\+\+ has safer alternatives? This question is rather rhetorical, maybe even philosophical\. We'll just hope that this short note has made it a little easier for you, dear reader, to answer it\!

And if not, feel free to leave your questions in the comments\!

Thank you for reading it to the end\! _El_ _Psy_ _Kongroo_\.