Three ways to avoid arrays in modern C++
The use of C-style arrays is considered technical debt in modern C++. C arrays are inconvenient to use and are sources of bugs but even today I continue to see a lot of new code that uses them. By following these tips you will be able to significantly improve the quality of your code in just a few simple steps.
Array problems
Array problems can be divided into two broad categories:
1) access beyond the limits;
2) oversizing.
The first is when the programmer does not correctly calculate the size of the array, the second is when the programmer does not know in advance the number of elements to be managed and therefore uses a “fairly” large number.
Let’s look at a few examples:
- Here the programmer intended to create 10 buttons to make a numeric keypad but got confused and made a mistake with the NUMPAD_DIGITS value.
#define NUMPAD_DIGITS 9
void create_widgets() {
Widget *widgets[NUMPAD_DIGITS + 1];
for (int i = 0; i < NUMPAD_DIGITS; i++)
widgets[i] = new Widget();
}
- In these examples the programmer is creating incorrectly sized arrays. Interestingly, if the type of
foo_no_size ()
were different fromchar *
, we would not have a function equivalent tostrlen ()
available to know the length of the array
void foo_no_size(const char *arr) {
int a[sizeof(arr)]; // error: should be strlen(arr) + 1
}
void foo_size(const char *arr, int count) {
int a[sizeof(arr)]; // error: should be count
}
- Here the programmer has created an over-sized array with respect to requirements, hoping that it will be sufficient.
void read_data() {
char buffer[256];
// read from socket
}
- Here the programmer would like to know if the two arrays have the same elements but the comparison is always false.
void main() {
int arr[2] {1,2};
int arr2[2] {1,2};
if (arr == arr2) { // Error: never true
cout << "Arrays are equal\n";
}
}
These problems derive from automatic behaviour of the arrays called “decay” (pointer deterioration): any array can be assigned to a pointer of the same type, thus losing the size of the array, without this being a compilation error.
Another problem resulting from pointer “decay” is the comparison between arrays. Typically, when comparing two arrays, the aim is to compare the value of the elements contained in the array, but the default behaviour is the comparison between pointers.
Finally, I can’t use assignment to make a member-to-member copy between arrays.
All the alternatives presented in this article do not automatically “decay” the pointer, thus keeping information on the size, and offer operators for comparison and assignment.
Possible solutions
Let’s look at how it is possible to overcome these problems both in STL and in in Qt.
Use std::vector
Paraphrasing a well-known saying, no-one has ever been fired for using a vector. Its features make it the right data structure for 90% of use cases and this should be your default choice (as suggested by Herb Sutter in “C++ Coding Standards: 101 Rules, Guidelines, And Best Practices”). Let’s look at some of them together:
- it guarantees the contiguity of the elements in memory, so it is compatible with C arrays;
- its iterators are RandomAccess;
- insertion in the queue is guaranteed by standard being O(1) amortised;
- thanks to the fact that the elements are contiguous in memory it provides practically unbeatable performance over all modern CPUs;
- it supports operators, such as =, <, >, ==;
- it has dynamic size.
Let’s look at how the examples presented in the previous paragraph can be rewritten.
#define NUMPAD_DIGITS 10
void create_widgets() {
vector<Widget*> widgets;
for (int i = 0; i < NUMPAD_DIGITS; i++)
widgets.push_back(new Widget);
}
void foo_no_size(vector<char> v) {
vector<int> a(v.size());
}
Use std::array
If you can’t use dynamic allocation, starting with C++ 11 the standard library provides a fixed-size container that knows its size: std::array
.
The advantages of std::array
over a C array are:
std::array
knows its size;- it has the same performance as the respective C array;
- it provides checked access to an element via
at()
; - it provides RandomAccess iterators;
- it supports operators, such as =, <, >, ==;
- it has the same API as the other containers in the standard library, so it can be used in generic code.
This container is very easy to use, let’s look at an example:
int main()
{
std::array<int, 3> a2 = {1, 2, 3};
std::array<std::string, 2> a3 = { std::string("a"), "b" };
// container operations are supported
std::sort(a1.begin(), a1.end());
// ranged for loop is supported
for(const auto& s: a3)
std::cout << s << ' ';
std::cout << '\n';
std::array<int, 3> a4 = a1; // Ok
//std::array<int, 4> a5 = a1; // Error, wrong number of elements
}
We can rewrite the examples presented in the introduction in a more idiomatic way as follows:
#define NUMPAD_DIGITS 10
void create_widgets() {
array<Widget*, NUMPAD_DIGITS> widgets;
for (auto &w : widgets)
w = new Widget;
}
Use QVector
If you are writing a Qt program, the default data structure can be either std::vector
or QVector
. QVector
provides all the guarantees of std::vector
and in addition guarantees API-level compatibility with the rest of the Qt program.
If you are a long-time Qt user, perhaps your default choice falls on QList
. However, this choice must be reconsidered because there are limitations that impact on performance, both in terms of speed and memory occupied. In fact, to date QList
should only be used to interact with the Qt APIs that require it.
Personally, I prefer to use QVector
than std::vector
because I find the API more comfortable to use.
Compatibility with C code
At this point I hope I have convinced you that there is no reason to use C arrays in C++. In some cases, however, you should be able to interact with C APIs that read or write on C arrays, for example strncpy(char *dest, const char *src, size_t n)
.
All the containers I have described have compatibility methods with C arrays precisely to handle cases like this.
Let’s look at some examples:
int main()
{
constexpr size_t len = 14;
const char s[len] = "Hello world!\n";
array<char, len> a;
strncpy(a.data(), s, a.size());
vector<char> v(len); // allocate space for `len` elements
strncpy(v.data(), s, v.size());
QVector<char> v(len); // allocate space for `len` elements
strncpy(v.data(), s, v.size());
}
The data()
method returns a pointer to the first element of memory and the size()
method returns the number of available elements.
Note that I used a vector constructor which creates a number of elements before calling the function, otherwise size()
is 0.
Conclusions
In C++ there are no plausible reasons to use C-style arrays. The solutions we have looked at in this article are all type and size safe, have additional features (such as automatic resize management or comparison operators) and are compatible with C arrays in cases where it is necessary to interact with C libraries.
std::vector
is the correct replacement for most cases, QVector
is preferable for code that uses Qt, while std::array
can be used in those cases where granular memory control is critical.