std::array vs c-style array

Within this article I want to compare the base array type and the array offered by the std library. I want to call these two types “c-style array” and “std::array”. The comparison will show you the pros and cons of the different types and give you a guideline when to use which kind of array.

Base concept of std::array

The std::array is designed as zero-overhead wrapper for c-style arrays. It will provide a value like semantics equally to the other C++ containers. A std::array should have same runtime performance as a c-style array.

Within the next chapters I want to compare both array types by looking at some base use cases.

Initialize, change and read

The following example application shows the initialization of an array with three elements. Furthermore, an array element will be changed and all elements will be read.

int _tmain(int argc, _TCHAR* argv[])
{
  // c-style array
  int array1[3] = {1, 2, 3};

  array1[1] = 15;

  std::cout << "size: " << (sizeof(array1) / sizeof(*array1)) << std::endl;

  for (auto element : array1)
  {
    std::cout << element << std::endl;
  }
  
  // std-style array
  std::array array2 = { 4, 5, 6 };

  array2[1] = 25;

  std::cout << "size: " << array2.size() << std::endl;

  for (auto element : array2)
  {
    std::cout << element << std::endl;
  }
}

Within this first example you can see that the syntax for the variable declaration is slightly different, but element access and usage of array in a range based for loop is the same for both types. Furthermore, the example shows a first big difference of both types: the std::array knows its size. For the c-style array you have to create an additional variable to store the array size or, as shown in the example, you can calculate the size by dividing the memory reserved for the array by the memory needed to store a single element.

Array as method parameter

In a real application, you will split up your program logic into functions and therefore a standard use case is to pass parameters to functions. So, let’s change the example a little bit and create functions which should print the array.

void PrintArray(int* values, int size)
{
  std::cout << "size: " << size << std::endl;

  for (int i = 0; i < size; ++i)
  {
    std::cout << values[i] << std::endl;
  }
}

void PrintArray(const std::array &values)
{
  std::cout << "size: " << values.size() << std::endl;

  for (auto& value : values)
  {
    std::cout << value << std::endl;
  }
}

int _tmain(int argc, _TCHAR* argv[])
{
  int array1[3] = { 1, 2, 3 };
  PrintArray(array1, 3);
  
  std::array array2 = { 4, 5, 6 };
  PrintArray(array2);
}

This simple use case shows a very important difference between a c-style array and a std::array. The c-style array will be passed as pointer to its first parameter. The std::array instead offers the standard copy semantic. Therefore, you can use standard coding practices and for example pass the parameter by const reference. So, the std::array doesn’t convert to a pointer unless you explicitly ask it to.

A c-style array is passed as pointer to the first element. This principle is called “pointer decay”. As the array decays to a pointer, the size information is loss. Therefore, we have to pass an additional function parameter which contains the array size. The previously shown calculation depending on the memory size will no longer work, as the memory size of the passed parameter is the size of a pointer now. Furthermore, as the size information is lost, the range based for loop cannot be used anymore.

You can see, this little design change of the application, has nearly no impact if you use a std::array. You can simple refactor your code and extract the according code section as method. But if you try to do the same for a c-style array, you will get in trouble and as result of the pointer decay you have to change your implementation fundamentally. Within the following article you can see some other impacts of the pointer decay, especially in base and derived class relationships: Arrays and inheritance, a source of errors.

std::array as method parameter with variable size

Maybe you already noticed one limitation of the print function we implemented for the std::array. The function will accept an array with three integer values only. It will not work with array of other types or other sizes.

At first you may think this is a limitation or disadvantage of std::array. But it isn’t. Moreover it is a big advantage of this data type as std::array will perfectly match with the C++ base design which uses a strict type system. We have a fixed size as part of type declaration. So each std::array with a different size is a different type. This will allow the compiler to detect and prevent implementation errors.

Of course, a strict type system will make it difficult to create higher-order functions. For these cases c++ offers function overloading, for example by implementing templates.

The following source code shows a template based implementation which allows to write a print function which accepts a std::array of any size. Furthermore, you will see a second function which is even more generic as it accepts any kind of container object.

template
void PrintArray(const std::array &values)
{
  for (auto value : values)
  {
    std::cout << value << std::endl;
  }
}

template
void PrintContainer(T &values)
{
  for (auto value : values)
  {
    std::cout << value << std::endl;
  }
}

int _tmain(int argc, _TCHAR* argv[])
{
  std::array array2 = { 4, 5, 6 };
  PrintArray(array2);
  PrintContainer<std::array>(array2);
}

c-style array as method parameter with size information

Of course, the template concept can also be used for a c-style array. Instead of passing a pointer to the first element, you can pass the array as a reference to template function. This will allow to pass the array with its specific size.

template 
void PrintArray(T(&values)[Size])
{
  for (auto value : values)
  {
    std::cout << value << std::endl;
  }
}

int _tmain(int argc, _TCHAR* argv[])
{
  int array1[3] = { 1, 2, 3 };
  PrintArray(array1);
}

Array as return value

C-style arrays are among the few types which cannot be used as L-value. So, c-style arrays cannot be used on the left side of an assignment and therefore they cannot be used as return values too. But you can return a pointer to a c-style array. Unfortunately, this will result in with two big issues: you have to know the size of the created array outside of the method and you have to implement the resource management based on this wild pointer. Both issues are a good source for errors.

A std::array supports the c++ copy semantic. Therefore, it can be used as return value. The following example shows possible implementation for both kinds of arrays.

int* CreateArray()
{
  int* array = new int[3] { 1, 2, 3 };
  return array;
}

std::array CreateStdArray()
{
  std::array array = { 4, 5, 6 };
  return array;
}

int _tmain(int argc, _TCHAR* argv[])
{
  // c-style array
  int* array1 = CreateArray();
  int size = 3;   // you have to know the size of the array

  for (int i = 0; i < size; ++i)
  {
    std::cout << array1[i] << std::endl;
  }

  delete[] array1; // you have to delete the array
   
  // std-style array
  std::array array2 = CreateStdArray();

  for (auto element : array2)
  {
    std::cout << element << std::endl;
  }
}

In both cases you can create factory methods for arrays of any length. In case of a c-style array I would prefer the length as input parameter and in case of the std::array I would suggest the template based approach, shown in a previous section about method parameters.

If the client does not know the resulting size of the array and the factory method itself should be responsible to create a collection of the needed size, you should not use a std::array at all. The array is a collection of fixed size but you want to use a collection of variable size. This sound like a use case for the std::vector. At this moment, I don’t want to go further into details as this doesn’t match with the topic of this article. But in a further article I want to continue with a comparison of c-style array and std::vector.

Range check

One of the base concepts of c and c++ is to provide build in types without any overhead which could result in performance loss or higher memory usage. Therefore, arrays do not contain a management layer with range checks and similar features. Of course, this missing secure access layer could result in implementation errors which cause undefined behavior of your application.

The std::array expands this concept and provides a combination of the advantages of both ideas. In favor of performance it comes without any management layer in release build and on the other hand it offers a secure access layer in debug builds. If you create a debug build, the std::array contains range checks. This will help to find implementation errors. The following source code shows an according example. If you execute this example as debug build, the erroneous std::array access will throw an out of range exception.

int _tmain(int argc, _TCHAR* argv[])
{
  // c-style array
  int array1[3] = {7, 5, 3};
  
  std::sort(std::begin(array1), std::end(array1));

  for (auto element : array1)
  {
    std::cout << element << std::endl;
  }
  
  // std-style array
  std::array array2 = { 7, 5, 3 };

  std::sort(array2.begin(), array2.end());
  
  for (auto element : array2)
  {
    std::cout << element << std::endl;
  }
}

Performance and memory usage

As mentioned before, the std::array is designed as zero-overhead wrapper for c-style arrays. Therefore, both types are optimized regarding performance and memory usage. The std::array has an identical memory layout to c-style arrays. Furthermore, they have the same performance, except in debug builds. As shown in the previous chapter, the std::array contains range checks in debug build and therefore it has a slightly lower performance. In release build the performance of std::array and c-style array are same. By definition, the std::array is a simple aggregate containing a c-style array as its only member.

Array and standard algorithms

The std::array provides an STL-consistent interface and can be used with the STL algorithms. The STL algorithms also provide support for c-style arrays. If you have c-style arrays with known size, you can directly use them together with the algorithms. As shown in the previous examples, pointer decay will result in a loss of the size information and therefore in a loss of direct support of algorithms. But again, you can use the template based approach to keep the size information of a c-style method parameter.

The following example shows how to use both array types in STL algorithms. In this case the sort algorithm is used.

int _tmain(int argc, _TCHAR* argv[])
{
  // c-style array
  int array1[3] = {1, 2, 3};

  array1[5] = 15;   // undefined behavior
   
  // std-style array
  std::array array2 = { 4, 5, 6 };

  array2[5] = 25;   // out of range exception in debug build
}

Multi-dimensional arrays

The std::array as well as the c-style array support multi-dimensions. These multi-dimensions are designed as arrays of nested arrays. The syntax to declare a std::array directly reflects this nested design. But this comes with the downside of a more awkward syntax than compared to a c-style array. The following example shows how to declare, write and read a multi-dimensional array.

int _tmain(int argc, _TCHAR* argv[])
{
  // c-style array
  int array1[2][3] = { { 1, 2, 3 }, { 4, 5, 6} };

  array1[0][1] = 10;
  
  for (auto &row : array1)
  {
    for (auto &element : row)
    {
      std::cout << element << std::endl;
    }
  }     

  // std-style array
  std::array<std::array, 2> array2{ { { 7, 8, 9 }, { 10, 11, 12 } } };

  array2[0][1] = 20;

  for (auto &row : array2)
  {
    for (auto &element : row)
    {
      std::cout << element << std::endl;
    }
  }
}

Summary

The std::array is designed as zero-overhead wrapper for c-style arrays. Therefore, it comes with all the advantages of a c-style array and ads the additional advantages of C++ containers. I would recommend to use std::arrays except you have a rare special use case which comes with good reasons to use a c-style array.
Advantages of std::array

  • Perfectly matches with the strict type system concept of c++
  • Copy semantic / no pointer decay / the array doesn’t convert to a pointer unless you explicitly ask it to
  • It knows its size
  • Range check in debug builds
  • No memory overhead beyond what it needs to hold its elements
  • Same performance as a c-style array
  • Direct support of use in STL algorithms

Disadvantages of std::array

  • Slightly more awkward syntax for declaration of the array, especially when creating multi-dimensional arrays
Advertisements
Dieser Beitrag wurde unter C++ veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Eine Antwort zu std::array vs c-style array

  1. Pingback: std::vector vs c-style array | coders corner

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s