﻿# Как не надо проверять размер массива в С\+\+

Как часто вам приходится сталкиваться с конструкцией sizeof\(array\)/sizeof\(array\[0\]\) для определения размера массива? Очень надеюсь, что не часто, ведь на дворе уже 2024 год\. В заметке поговорим о недостатках конструкции, откуда она берётся в современном коде и как от неё наконец избавиться\.

![1112_wrong_array_size_check_ru/image1.png](https://import.viva64.com/docx/blog/1112_wrong_array_size_check_ru/image1.png)

## Чуть больше контекста

Не так давно я бороздил просторы интернета в поисках интересного проекта для проверки\. Глаз зацепился за [OpenTTD](https://www.openttd.org/) — Open Source симулятор, вдохновлённый Transport Tycoon Deluxe \(aka симулятор транспортной компании\)\. "Хороший, зрелый проект", — изначально подумал я\. Тем более и повод имеется — недавно ему исполнилось целых [20 лет](https://www.openttd.org/news/2024/03/06/happy-birthday)\! Даже PVS\-Studio и то моложе :\)\.

Примерно здесь уже было бы хорошо переходить к ошибкам, которые нашёл анализатор, но не тут\-то было\. Хочется похвалить разработчиков — несмотря на то, что проект существует более 20 лет, их кодовая база выглядит прекрасно: CMake, работа с современными стандартами C\+\+ и относительно небольшое количество ошибок в коде\. Всем бы так\.

Однако, как вы понимаете, если бы совсем ничего не нашлось, то и не было бы этой заметки\. Предлагаю вам посмотреть на следующий код \([GitHub](https://github.com/OpenTTD/OpenTTD/blob/4af089b9be8f56418cc96447b3ca26f037f7888b/src/network/network_gui.cpp#L2254)\):

```cpp
NetworkCompanyPasswordWindow(WindowDesc *desc, Window *parent) 
: Window(desc)
, password_editbox(
    lengthof(_settings_client.network.default_company_pass)    // <=
  )
{
  ....
}
```

С виду ничего интересного, но анализатор смутило вычисление размера контейнера _\_settings\_client\.network\.default\_company\_pass_\. При более детальном рассмотрении оказалось, что _lengthof_ — это макрос, и в реальности код выглядит так \(чуть\-чуть отформатировал для удобства\):

```cpp
NetworkCompanyPasswordWindow(WindowDesc *desc, Window *parent) 
: Window(desc)
, password_editbox(
    (sizeof(_settings_client.network.default_company_pass) /
       sizeof(_settings_client.network.default_company_pass[0]))
  )
{
  ....
}
```

Ну и раз уж мы выкладываем карты на стол, то можно показать и предупреждение анализатора:

[V1055](https://pvs-studio.ru/ru/docs/warnings/v1055/) \[CWE\-131\] The 'sizeof \(\_settings\_client\.network\.default\_company\_pass\)' expression returns the size of the container type, not the number of elements\. Consider using the 'size\(\)' function\. network\_gui\.cpp 2259

В этом случае за _\_settings\_client\.network\.default\_company\_pass_ скрывается _std::string_\. Чаще всего размер объекта контейнера, полученный через _sizeof,_ ничего не говорит о его истинных размерах\. Попытка таким образом получить размер строки практически всегда является ошибкой\.

Всё дело в особенностях реализации современных контейнеров стандартной библиотеки и _std::string_ в частности\. Чаще всего они реализуются с помощью двух указателей \(начало и конец буфера\), а также переменной, содержащей реальное количество элементов\. Именно поэтому при попытке вычислить размер_ std::string_ c помощью _sizeof_ вы будете получать одно и то же значение вне зависимости от реальных размеров буфера\. Убедиться в этом можно, взглянув на небольшой [пример](https://godbolt.org/z/WvMKrrbhn), который я уже приготовил для вас\.

Конечно же, реализация и конечный размер контейнера зависят от используемой стандартной библиотеки, а также от различных оптимизаций \(см\. [Small String Optimization](https://pvs-studio.ru/ru/blog/terms/6658/)\), поэтому результат у вас может отличаться\. Интересное исследование на тему внутренностей _std::string_ можно прочитать [здесь](https://shaharmike.com/cpp/std-string/)\.

## Почему?

Итак, в проблеме разобрались и выяснили, что так делать не надо\. Но ведь интересно, как к этому пришли?

В случае OpenTTD всё достаточно просто\. Судя по blame, почти четыре года назад тип поля _default\_company\_pass_ [изменили](https://github.com/OpenTTD/OpenTTD/commit/c73d64adf984036a99d6974b130eda65dfc18c6c#diff-f6372bcb055e88a1265135229ee51d619b8ab8cee004ff3f10d4d2c6188c9ddeL271) с _char\[NETWORK\_PASSWORD\_LENGTH\]_ на _std::string_\. Любопытно, что текущее значение, возвращаемое макросом _lenghtof,_ отличается от прошлого ожидаемого: 32 против 33\. Каюсь, не стал сильнее вникать в код проекта, но надеюсь, что разработчики учли этот нюанс\. Судя по комментарию, после поля _default\_company\_pass_ 33 символ отвечал за нуль\-терминал\.

```cpp
// The maximum length of the password, in bytes including '\0'
// (must be >= NETWORK_SERVER_ID_LENGTH)
```

Legacy и небольшая невнимательность при рефакторинге — казалось бы, вот она, причина\. Но, как ни странно, такой способ вычисления размера массива встречается даже в новом коде\. Если с языком C все понятно — иначе никак, то что не так с С\+\+? За ответом я пошёл в Google Поиск и не сказать, чтобы удивился\.\.\.

![1112_wrong_array_size_check_ru/image2.png](https://import.viva64.com/docx/blog/1112_wrong_array_size_check_ru/image2.png)

Прямо в самом начале, даже до основных результатов поиска, выдаётся вот это :\( Здесь стоит сделать ремарку, что для поиска использовался приватный режим, чистый компьютер и прочие нюансы, которые отметают подозрения в том, что это поиск на основе моих прошлых запросов\.

_Прим\. автора: стало даже немного интересно\. Напишите в комментариях, что показывает вам в топе выдачи по такому же запросу\._

Печально\. Надеюсь, что ИИ, обучающиеся на текущем коде, не будут совершать подобных ошибок\.

## Как надо

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

Итак, _sizeof\(\(expr\)\) / sizeof\(\(expr\)\[0\]\)_ — это настоящий [магнит](https://pvs-studio.ru/ru/blog/examples/v511/) для ошибок\. Посудите сами:

1. Для динамически выделенных буферов _sizeof_ посчитает не то, что надо;
1. Если _builtin_\-массив передали в функцию по копии, то _sizeof_ на нём тоже вернёт не то, что надо\.

Раз уж мы тут пишем на С\+\+, то давайте воспользуемся мощью шаблонов\! Тут мы приходим к легендарным ArraySizeHelper'ам \(aka "безопасный _sizeof_" в некоторых статьях\), которые рано или поздно пишутся _почти_ в каждом проекте\. В стародавние времена — до C\+\+11 — можно было встретить таких монстров:

```cpp
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];

#define countof(array) (sizeof(ArraySizeHelper(array)))
```

<details>
   <summary>Для тех, кто не понял, что тут происходит:</summary>

_ArraySizeHelper_ — это шаблон функции, который принимает массив типа _T_ и размера _N_ по ссылке\. При этом функция возвращает ссылку на массив типа _char_ размера _N_\.



Чтобы понять, как эта штука работает, рассмотрим небольшой пример:



```cpp
void foo()
{
  int arr[10];
  const size_t count = countof(arr);
}
```



При вызове _ArraySizeHelper_ компилятор должен будет вывести шаблонные параметры из шаблонных аргументов\. В нашем случае _T_ будет выведен как _int_, а _N_ как 10\. Возвращаемым типом функции при этом будет тип _char \(&\)\[10\]_\. В итоге _sizeof_ вернёт размер этого массива, который и будет равен количеству элементов\.



Как можно заметить, у функции отсутствует тело\. Сделано это для того, чтобы такую функцию можно было использовать ТОЛЬКО в [невычисляемом](https://en.cppreference.com/w/cpp/language/expressions#Potentially-evaluated_expressions) контексте\. Например, когда вызов функции находится в _sizeof_\.



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



```cpp
void foo(uint8_t* data)
{
  auto count = countof(data); // ошибка компиляции
  ....
}
```


</details>
Насчёт стародавних времён я не преувеличиваю\. Мой коллега ещё в 2011 году [разбирался](https://pvs-studio.ru/ru/blog/posts/cpp/a0074/), как работает эта магия в проекте Chromium\. С приходом в нашу жизнь C\+\+11 и C\+\+14 писать такие вспомогательные функции стало намного проще:

```cpp
template <typename T, size_t N>
constexpr size_t countof(T (&arr)[N]) noexcept
{
  return N;
}
```

Но и это ещё не все — можно лучше\!

Скорее всего, далее вы столкнётесь с тем, что захотите считать размер контейнеров: _std::vector_, _std::string_, _QList_, — не важно\. В таких контейнерах уже есть нужная нам функция — _size_\. Её\-то нам и нужно позвать\. Добавим перегрузку для функции выше:

```cpp
template <typename Cont>
constexpr auto countof(const Cont &cont) -> decltype(cont.size())
  noexcept(noexcept(cont.size()))
{
  return cont.size();
}
```

Здесь мы просто определили функцию, которая будет принимать любой объект и возвращать результат вызова его функции _size_\. Теперь наша функция имеет защиту от указателей, умеет работать как с _builtin_\-массивами, так и с контейнерами, да ещё и на этапе компиляции\.

Ииии я вас поздравляю, мы успешно переизобрели [_std::size_](https://en.cppreference.com/w/cpp/iterator/size)\. Его\-то я и предлагаю использовать, начиная с C\+\+17, вместо устаревших _sizeof_\-костылей и ArraySizeHelper'ов\. Ещё и не нужно каждый раз писать заново: он становится доступен после включения заголовочного файла практически любого контейнера :\)

## Современный C\+\+: правильное вычисление количества элементов в массивах и контейнерах

Ниже я также предлагаю рассмотреть пару распространённых сценариев для тех, кто вдруг попал сюда из поиска\. Далее я буду подразумевать, что _std::size_ доступен в стандартной библиотеке\. В ином случае можно скопировать описанные выше функции и использовать их как аналоги\.

### Я использую какой\-нибудь современный контейнер \(std::vector, QList и т\.п\.\)

В большинстве случаев лучше использовать функцию\-член класса _size_\. Например: [_std::string::size_](https://en.cppreference.com/w/cpp/string/basic_string/size), [_std::vector::size_](https://en.cppreference.com/w/cpp/container/vector/size), [_QList::size_](https://doc.qt.io/qt-6/qlist.html#size) и т\.п\. Начиная с C\+\+17, рекомендую перейти на [_std::size_](https://en.cppreference.com/w/cpp/iterator/size), описанный выше\.

```cpp
std::vector<int> first  { 1, 2, 3 };
std::string      second { "hello" };
....
const auto firstSize  = first.size();
const auto secondSize = second.size();
```

### У меня обычный массив

Также используйте свободную функцию [_std::size_](https://en.cppreference.com/w/cpp/iterator/size)\. Как мы уже выяснили выше, она может вернуть количество элементов не только в контейнерах, но в обычных массивах\.

```cpp
static const int MyData[] = { 2, 9, -1, ...., 14 };
....
const auto size = std::size(MyData);
```

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

### Я внутри шаблона и не знаю, что за контейнер/объект используется на самом деле

Также используйте свободную функцию [_std::size_](https://en.cppreference.com/w/cpp/iterator/size)\. В дополнение к неприхотливости в плане типа объекта она ещё и работает на этапе компиляции\.

```cpp
template <typename Container>
void DoSomeWork(const Container& data)
{
  const auto size = std::size(data);
  ....
}
```

### У меня есть два указателя или итератора \(начало и конец\)

Здесь возможны два варианта в зависимости от ваших потребностей\. Если нужно только узнать размер, то достаточно воспользоваться [_std::distance_](https://en.cppreference.com/w/cpp/iterator/distance):

```cpp
void SomeFunc(iterator begin, iterator end)
{
  const auto size = static_cast<size_t>(std::distance(begin, end));
}
```

Если нужно что\-то интереснее простого получения размера, то можно использовать read\-only классы\-обёртки: [_std::string\_view_](https://en.cppreference.com/w/cpp/header/string_view) для строк, [_std::span_](https://en.cppreference.com/w/cpp/container/span) в общем случае и т\.д\. Например:

```cpp
void SomeFunc(const char* begin, const char * end)
{
  std::string_view view { begin, end };
  const auto size = view.size();
  ....
  char first = view[0];
}
```

Опытные читатели также могут добавить вариант с адресной арифметикой, но, пожалуй, я оставлю его за скобками, т\.к\. целевой аудиторией заметки являются начинающие программисты\. Не будем учить их плохому :\)

### У меня есть только один указатель \(например, создали массив через new\)

В большинстве случаев придётся немного переписать программу и добавить передачу размера массива\. Увы\.

Если же вы работаете именно со строками \(_const char \*_, _const wchar\_t \*_ и т\.п\.\) и точно знаете, что строка содержит [нуль\-терминал](https://pvs-studio.ru/ru/blog/terms/0088/), то ситуация немного лучше\. В таком случае можно воспользоваться [_std::basic\_string\_view_](https://en.cppreference.com/w/cpp/string/basic_string_view):

```cpp
const char *text = GetSomeText();
std::string_view view { text };
```

Как и в примере выше, получаем все достоинства view\-классов, имея изначально только один указатель\.

Также упомяну менее предпочтительный, но полезный в некоторых ситуациях вариант с использованием [_std::char\_traits::length_](https://en.cppreference.com/w/cpp/string/char_traits):

```cpp
const char *text = GetSomeText();
const auto size = std::char_traits<char>::length(text);
```

_std::char\_traits_ — это настоящий швейцарский нож для работы со строками\. С его помощью можно писать обобщённые алгоритмы вне зависимости от используемого типа символов в строке \(_char_, _wchar\_t_, _char8\_t_, _char16\_t_, _char32\_t_\)\. Это позволяет не думать о том, какую функцию требуется использовать в тот или иной момент: [_std::strlen_](https://en.cppreference.com/w/cpp/string/byte/strlen) или [_std::wsclen_](https://en.cppreference.com/w/cpp/string/wide/wcslen)\. Обратите внимание, что я не просто так уточнил про обязательное наличие в строке нуль\-терминала\. В противном случае получите [неопределённое поведение](https://pvs-studio.ru/ru/blog/terms/0066/) \(undefined behavior\)\.

## Заключение

Надеюсь, мне удалось показать хорошие альтернативы для замены такой простой, но опасной конструкции как _sizeof\(array\) / sizeof\(array\[0\]\)_\. Если вам кажется, что я что\-то незаслуженно забыл или умолчал — добро пожаловать в комментарии :\)