The “array size constant” antipattern

Here’s a bad pattern I see frequently in student code on Code Review StackExchange, and even in a fair amount of (other people’s) teaching material. They’ll have an array of, let’s say, 5 elements; and then because magic numbers are bad, they’ll introduce a named constant to refer to the count of elements “5”.

void example()
{
    constexpr int myArraySize = 5;
    int myArray[myArraySize] = {2, 7, 1, 8, 2};
    ...

But this is the wrong way around! In the code above, the number five is repeated: first in the value of myArraySize = 5 and then again when you actually write down the elements of myArray. The code above is just as horrible, maintenance-wise, as:

constexpr int messageLength = 45;
const char message[messageLength] =
    "Invalid input. Please enter a valid number.\n";

—which surely none of us would ever write.

Code that repeats itself is fragile code

Notice that in both of the above code snippets, any time you change the contents of the array or the wording of the message, you must update two lines of code instead of just one. Here’s an example of how a maintainer might update this code incorrectly:

    constexpr int myArraySize = 5;
-   int myArray[myArraySize] = {2, 7, 1, 8, 2};
+   int myArray[myArraySize] = {3, 1, 4};

The patch above looks like it’s changing the contents of the array from 2,7,1,8,2 to 3,1,4, but it’s not! It’s actually changing it to 3,1,4,0,0 — padded with zeroes — because the maintainer forgot to change myArraySize in sync with myArray.

The robust approach

Computers are really good at counting things. Let the computer do the counting!

int myArray[] = {2, 7, 1, 8, 2};
constexpr int myArraySize = std::size(myArray);

Now you can change the array’s contents — say, from 2,7,1,8,2 to 3,1,4 — by changing only a single line of code. There’s no second place to update.

In fact, real-world code will typically use range-based for loops and/or iterator-range algorithms to manipulate myArray, so it won’t need a named variable to hold the size of the array at all.

for (int elt : myArray) {
    use(elt);
}
std::sort(myArray.begin(), myArray.end());
std::ranges::sort(myArray);

// Warning: Unused variable 'myArraySize'

In the “bad” version of this code, myArraySize is always used (by the declaration of myArray) and so the programmer is unlikely to see that it can be eliminated. In the “good” version, it’s trivial for the compiler to detect that myArraySize is unused.

How do we do this with std::array?

Sometimes the programmer goes one step farther toward the Dark Side, and writes:

constexpr int myArraySize = 5;
std::array<int, myArraySize> myArray = {2, 7, 1, 8, 2};

This should at least be rewritten as:

std::array<int, 5> myArray = {2, 7, 1, 8, 2};
constexpr int myArraySize = myArray.size();  // or std::size(myArray)

However, there’s no simple way to eliminate the manual counting in the first line. C++17’s CTAD does permit us to write

std::array myArray = {2, 7, 1, 8, 2};

but that works only if you want an array of int — it wouldn’t work if you wanted an array of short, for example, or an array of uint32_t.

C++20 gives us std::to_array, which permits us to write

auto myArray = std::to_array<int>({2, 7, 1, 8, 2});
constexpr int myArraySize = myArray.size();

Notice that this makes a C array and then move-constructs its elements into the std::array. All our previous examples initialized myArray with a braced initializer list, which triggered aggregate initialization and constructed the array’s elements directly in-place.

Anyway, all of these options result in a lot of extra template instantiations, compared to plain old C-style arrays (which require zero template instantiations). Therefore I strongly prefer T[] over std::array<T, N>.

In C++11 and C++14, std::array did have the ergonomic benefit of being able to say arr.size(); but that benefit evaporated when C++17 gave us std::size(arr) for built-in arrays too. There’s no ergonomic benefit to std::array anymore. Use it if you need its whole-object value semantics (pass a whole array to a function! return an array from a function! assign between arrays with =! compare arrays with ==!) but otherwise I recommend to avoid std::array.

Similarly, I recommend to avoid std::list unless you need its iterator stability, fast splicing, sorting without swapping elements, and so on. I’m not saying these types have no place in C++; I’m just saying that they have a “very particular set of skills,” and if you’re not using those skills, you’re probably overpaying.

Conclusions: Don’t put the cart before the horse. In fact, you might not need a cart. And if you must use a zebra to do a horse’s job, you shouldn’t put the cart before the zebra, either.

Posted 2020-08-06