Антипаттерн “константа размера массива”

в 13:06, , рубрики: antipatterns, c++, Блог компании OTUS. Онлайн-образование, Программирование

Перевод статьи подготовлен в преддверии старта курса «C++ Developer. Professional».


Хочу обратить ваше внимание на антипаттерн, который я часто встречаю в коде студентов на Code Review StackExchange и даже в довольно большом количестве учебных материалов (!) других людей. У них имеется массив, скажем, из 5 элементов; а затем, поскольку магические числа — это плохо, они вводят именованную константу для обозначения количества элементов «5».

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

Но решение это так себе! В приведенном выше коде число пять повторяется: сначала в значении myArraySize = 5, а затем еще раз, когда вы фактически присваиваете элементы myArray. Приведенный выше код столь же ужасен с точки зрения обслуживания, как:

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

— который, конечно, никто из нас никогда не напишет.

Код, который повторяется, хорошим не является

Обратите внимание, что в обоих приведенных выше фрагментах кода каждый раз, когда вы меняете содержимое массива или формулировку сообщения, вы должны обновить две строки кода вместо одной. Вот пример того, как мейнтейнер может обновить этот код неправильно:

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

Патч выше выглядит так, как будто он изменяет содержимое массива с 2,7,1,8,2 на 3,1,4, но это не так! Фактически он меняет его на 3,1,4,0,0 — с дополнением нулями — потому что мейнтейнер забыл скорректировать myArraySize в соответствии с myArray.

Надежный подход

Что до подсчета, то компьютеры в этом чертовски хороши. Так пусть же считает компьютер!

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

Теперь вы можете изменить содержимое массива, скажем, с 2,7,1,8,2 на 3,1,4, изменив только одну строку кода. Дублировать изменение нигде не нужно.

Даже больше, для манипуляций с myArray реальный код обычно использует циклы for и/или алгоритмы на основе диапазона итератора, поэтому ему вообще не понадобится именованная переменная для хранения размера массива.

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

// Warning: Unused variable 'myArraySize'

В «плохой» версии этого кода myArraySize всегда используется (в объявлении myArray), и поэтому программист вряд ли увидит, что ее можно исключить. В «хорошей» версии компилятору легко обнаружить, что myArraySize не используется.

Как это сделать с помощью std::array?

Иногда программист делает еще один шаг к Темной Стороне и пишет:

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

Это должно быть переписано по крайней мере как:

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

Однако простого способа избавиться от ручного подсчета в первой строке нет. CTAD C++17 позволяет писать

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

но это работает, только если вам нужен массив int — это не сработает, если вам нужен массив short, например, или массив uint32_t.

C++20 дает нам std::to_array, что позволяет нам писать

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

Обратите внимание, что это создает C-массив, а затем перемещает (move-constructs) его элементы в std::array. Все наши предыдущие примеры инициализировали myArray с помощью списка инициализаторов в фигурных скобках, который запускал агрегатную инициализацию и создавал элементы массива непосредственно на месте.

В любом случае, все эти варианты результируют в большом количестве дополнительных экземпляров шаблонов по сравнению со старыми добрыми C-массивами (которые не требуют создания экземпляров шаблонов). Поэтому я настоятельно предпочитаю T[] более новому std::array<T, N>.

В C++11 и C++14 у std::array было эргономическое преимущество, заключающееся в возможности сказать arr.size(); но это преимущество испарилось, когда C++17 предоставил нам std::size(arr) и для встроенных массивов. У std::array больше нет эргономических преимуществ. Используйте его, если вам нужна его семантика переменной целостного объекта (передать весь массив в функцию! Вернуть массив из функции! Присваивать массивы с помощью =! Сравнить массивы с помощью ==!), Но в противном случае я рекомендую избегать использование std::array.

Точно так же я рекомендую избегать std::list, если вам не нужна стабильность его итератора, быстрая склейка, сортировка без замены элементов и т. д. Я не говорю, что этим типам нет места в C++; Я просто говорю, что у них есть «очень специфический сет скилов», и если вы не используете эти скилы, вы, скорее всего, переплачиваете понапрасну.

Выводы: не городите телегу впереди лошади. На самом деле, телега может даже и не понадобиться. И если вам необходимо использовать зебру для выполнения работы лошади, вам также не следует городить телегу впереди зебры.

Автор: Дмитрий

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js