Arcyno
Arcyno

Reputation: 4593

What are the advantages of using std::array over C-style arrays?

If I want to build a very simple array like:

int myArray[3] = {1, 2, 3};

Should I use std::array instead?

std::array<int, 3> a = {{1, 2, 3}};

What are the advantages of using std::array over usual ones? Is it more performant? Just easier to handle for copy/access?

Upvotes: 173

Views: 100004

Answers (8)

Jan Schultke
Jan Schultke

Reputation: 39385

std::array solves numerous issues which C-style arrays have. It's not about performance for the most part; it's about correctness and convenience. Here is the list of issues with C-style arrays that std::array solves.

1. Arrays cannot be returned from functions

This one is particularly annoying. Especially for tasks like initializing look-up tables, returning arrays is useful:

constexpr auto lookup = [] {
    std::array<int, 128> result;
    // compute the contents
    return result;
}();

2. The hypothetical syntax for returning arrays is cursed

int get_array()[10];
auto get_array() -> int[10]; // workaround: trailing return types

According to the C declaration syntax, this is how a function get_array which returns an array of 10 ints would be declared. This syntax is very surprising, and one of the reasons why it's very unlikely to ever be standardized in C.

3. Arrays cannot be assigned

int arr[] = {0, 1, 2, 3};
arr = something_else; // ill-formed

Assigning arrays is fairly common for small arrays, such as pairs or triples. This feels particularly inconsistent because the syntax = can be used for initialization.

4. Arrays cannot be initialized to other arrays

int data[] = {0, 1};
int copy[] = data;

This is yet another restriction which feels arbitrary. What makes it even worse is that copy-initialization is valid for arrays in principle, just not using another array instead of a braced-init-list.

5. Comparison of arrays with == is a common mistake

int arr[] = {0, 1, 2, 3};
arr == arr; // true, but doesn't compare contents, but is pointer comparison

This is a common mistake, which is why array comparison was deprecated in C++23 and is likely going to be removed in C++26.

See also Why does == equality comparison between arrays not work?

6. Arrays decay to pointers, giving them surprising semantics

char arr[] = "hello ";
if (arr); // always true, even if array contains an empty string
+arr;     // OK, but what does it mean to apply unary plus to an array?!
arr + 1;  // OK, but result is not "hello 1", it is "ello "

The list goes on and on. The fact that arrays decay to pointers often leads to counter-intuitive behavior. Most of this behavior is not yet deprecated.

7. Arrays might be variable length arrays (VLAs)

int size = rand();
int vla[size]; // could be OK if the compiler supports VLAs as an extension
std::array<int, size> arr; // error, as expected

Without compiler warnings (-Wvla for GCC/clang), we can inadvertently create a VLA. This makes code non-portable because not every compiler has support for VLAs.

8. Array type adjustment in function parameters is misleading

void foo(int arr[4]) {            // equivalent to accepting a parameter of type int*
    sizeof(arr) / sizeof(arr[0]); // = sizeof(void*) / sizeof(int), most likely 2
}

void foo(std::array<int, 4> arr) {
    arr.size(); // 4, correct
}

Parameters of array type in function parameters are adjusted to pointer types. This means that sizeof(arr) doesn't work properly, and the provided size [4] is actually meaningless.

This is very surprising, and using sizeof in conjunction with pointers when arrays were expected is one of the most common beginner mistakes in C.

9. Array sizes can't be deduced from function parameters

template <std::size_t N>
void foo(int arr[N]); // N can't be deduced from the array parameter

template <std::size_t N>
void foo(int (&arr)[N]); // workaround with complicated syntax

This issue is a consequence of the type adjustment to pointers in the prior point. There is no array type in the parameter from which N could be deduced. A workaround is necessary, but this workaround is not pretty.

10. Arrays lead to special cases in generic code

Due to all the aforementioned irregularities, arrays require special cases in generic code. To name some examples:

  • std::swap must have an overload for arrays because arrays are immovable
  • arrays aren't classes, so lots of free functions like std::size, std::begin, std::empty, etc. are necessary that have a special case for arrays

Any developer who writes a library which contains range.begin() instead of std::begin(range) has to fear their code breaking if a user uses C-style arrays. Of course, using std::array doesn't solve the underlying issue, but it means that you never suffer from a library developer having made the false assumption that .begin() is always valid.

Furthermore, these countless special cases still don't cover everything. You can use std::swap to do an element-wise swap of two arrays, but you cannot use std::exchange to exchange arrays element-wise (because arrays cannot be returned from functions).

11. Arrays may have poor performance due to aliasing

It is easy to inadvertently write code which has performance penalties caused by aliasing. Consider the following functions which are meant to serialize a 32-bit unsigned integer array into a byte array with little endian byte order.

std::array<std::byte, 4> serialize_le(unsigned x) {
    return {
        std::byte(x >>  0),
        std::byte(x >>  8),
        std::byte(x >> 16),
        std::byte(x >> 24)
    };
}

void write_le_n(std::byte* mem, std::array<unsigned, 1024>& numbers) {
    for (unsigned n : numbers) {
        auto bytes = serialize_le(n);
        std::memcpy(mem, &bytes[0], sizeof(bytes));
        mem += 4;
    }
}
void write_le(std::byte mem[], unsigned x) {
    mem[0] = std::byte(x >>  0);
    mem[1] = std::byte(x >>  8);
    mem[2] = std::byte(x >> 16);
    mem[3] = std::byte(x >> 24);
}

void write_le_n(std::byte* mem, unsigned x[1024]) {
    for (unsigned i = 0; i < 1024; ++i) {
        write_le(mem + i * 4, x[i]);
    }
}

See live example at Compiler Explorer

Intuitively, it would seem like both code samples are doing pretty much the same thing. However, the code generation for the second sample is much worse. There is absolutely no auto-vectorization; it's an extremely naive loop.

std::array can often indicate to the compiler that no aliasing takes place, or if there is overlap between memory regions, it cannot be partial. This is a boost to optimizations.

Conclusion

std::array solves countless problems with C-style arrays. For the most part, this isn't about performance, although std::array may be better in specific cases. It's about C-style arrays having confusing syntax, common pitfalls, arbitrary restrictions, and other issues.

See Also

Upvotes: 19

Todd
Todd

Reputation: 19

While std::array has some advantages, as outlined in the other helpful answers, some have stated things like, "… there is no performance difference than a raw array."

This simply is not true and is misleading to anyone working in true embedded development (time critical, bare metal).

Quick and dirty test using an STM32G0B1 running at 50MHz (CPU): Writing to a std::array takes approximately 5 µS longer than to a raw C style array. To me, this is a significant performance difference that cannot be ignored.

Upvotes: 1

user10609288
user10609288

Reputation:

You will get the same perfomance results using std::array and c array If you run these code:

std::array<QPair<int, int>, 9> *m_array=new std::array<QPair<int, int>, 9>();
    QPair<int, int> *carr=new QPair<int, int>[10];
    QElapsedTimer timer;
    timer.start();
    for (int j=0; j<1000000000; j++)
    {

        for (int i=0; i<9; i++)
        {
            m_array->operator[](i).first=i+j;
            m_array->operator[](i).second=j-i;
        }
    }
    qDebug() << "std::array<QPair<int, int>" << timer.elapsed() << "milliseconds";
    timer.start();
    for (int j=0; j<1000000000; j++)
    {

        for (int i=0; i<9; i++)
        {
            carr[i].first=i+j;
            carr[i].second=j-i;
        }
    }
    qDebug() << "QPair<int, int> took" << timer.elapsed() << "milliseconds";
    return 0;

You will get these results:

std::array<QPair<int, int> 5670 milliseconds
QPair<int, int> took 5638 milliseconds

Mike Seymour is right, if you can use std::array you should use it.

Upvotes: 2

vsoftco
vsoftco

Reputation: 56547

A std::array is a very thin wrapper around a C-style array, basically defined as

template<typename T, size_t N>
struct array
{
    T _data[N];
    T& operator[](size_t);
    const T& operator[](size_t) const;
    // other member functions and typedefs
};

It is an aggregate, and it allows you to use it almost like a fundamental type (i.e. you can pass-by-value, assign etc, whereas a standard C array cannot be assigned or copied directly to another array). You should take a look at some standard implementation (jump to definition from your favourite IDE or directly open <array>), it is a piece of the C++ standard library that is quite easy to read and understand.

Upvotes: 67

Henryk
Henryk

Reputation: 421

Is it more performant ?

It should be exactly the same. By definition, it's a simple aggregate containing an array as its only member.

The situation seems to be more complicated, as std::array does not always produce identical assembly code compared to C-array depending on the specific platform.

I tested this specific situation on godbolt:

#include <array>
void test(double* const C, const double* const A,
          const double* const B, const size_t size) {
  for (size_t i = 0; i < size; i++) {
    //double arr[2] = {0.e0};//
    std::array<double, 2> arr = {0.e0};//different to double arr[2] for some compiler
    for (size_t j = 0; j < size; j++) {
      arr[0] += A[i] * B[j];
      arr[1] += A[j] * B[i];
    }
    C[i] += arr[0];
    C[i] += arr[1];
  }
}

GCC and Clang produce identical assembly code for both the C-array version and the std::array version.

MSVC and ICPC, however, produce different assembly code for each array version. (I tested ICPC19 with -Ofast and -Os; MSVC -Ox and -Os)

I have no idea, why this is the case (I would indeed expect exactly identical behavior of std::array and c-array). Maybe there are different optimization strategies employed.

As a little extra: There seems to be a bug in ICPC with

#pragma simd 

for vectorization when using the c-array in some situations (the c-array code produces a wrong output; the std::array version works fine).

Unfortunately, I do not have a minimal working example for that yet, since I discovered that problem while optimizing a quite complicated piece of code.

I will file a bug-report to intel when I am sure that I did not just misunderstood something about C-array/std::array and #pragma simd.

Upvotes: 38

b4hand
b4hand

Reputation: 9770

std::array has value semantics while raw arrays do not. This means you can copy std::array and treat it like a primitive value. You can receive them by value or reference as function arguments and you can return them by value.

If you never copy a std::array, then there is no performance difference than a raw array. If you do need to make copies then std::array will do the right thing and should still give equal performance.

Upvotes: 13

Mike Seymour
Mike Seymour

Reputation: 254411

What are the advantages of using std::array over usual ones?

It has friendly value semantics, so that it can be passed to or returned from functions by value. Its interface makes it more convenient to find the size, and use with STL-style iterator-based algorithms.

Is it more performant ?

It should be exactly the same. By definition, it's a simple aggregate containing an array as its only member.

Just easier to handle for copy/access ?

Yes.

Upvotes: 185

Baum mit Augen
Baum mit Augen

Reputation: 50043

std::array is designed as zero-overhead wrapper for C arrays that gives it the "normal" value like semantics of the other C++ containers.

You should not notice any difference in runtime performance while you still get to enjoy the extra features.

Using std::array instead of int[] style arrays is a good idea if you have C++11 or boost at hand.

Upvotes: 39

Related Questions