Как часто вам приходится сталкиваться с конструкцией sizeof(array)/sizeof(array[0]) для определения размера массива? Очень надеюсь, что не часто, ведь на дворе уже 2024 год. В заметке поговорим о недостатках конструкции, откуда она берётся в современном коде и как от неё наконец избавиться.
Чуть больше контекста
Не так давно я бороздил просторы интернета в поисках интересного проекта для проверки. Глаз зацепился за OpenTTD — Open Source симулятор, вдохновлённый Transport Tycoon Deluxe (aka симулятор транспортной компании). "Хороший, зрелый проект", — изначально подумал я. Тем более и повод имеется — недавно ему исполнилось целых 20 лет! Даже PVS-Studio и то моложе :)
Примерно здесь уже было бы хорошо переходить к ошибкам, которые нашёл анализатор, но не тут-то было. Хочется похвалить разработчиков — несмотря на то, что проект существует более 20 лет, их кодовая база выглядит прекрасно: CMake, работа с современными стандартами C++ и относительно небольшое количество ошибок в коде. Всем бы так.
Однако, как вы понимаете, если бы совсем ничего не нашлось, то и не было бы этой заметки. Предлагаю вам посмотреть на следующий код (GitHub):
NetworkCompanyPasswordWindow(WindowDesc *desc, Window *parent)
: Window(desc)
, password_editbox(
lengthof(_settings_client.network.default_company_pass) // <=
)
{
....
}
С виду ничего интересного, но анализатор смутило вычисление размера контейнера _settings_client.network.default_company_pass. При более детальном рассмотрении оказалось, что lengthof — это макрос, и в реальности код выглядит так (чуть-чуть отформатировал для удобства):
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 [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 вы будете получать одно и то же значение вне зависимости от реальных размеров буфера. Убедиться в этом можно, взглянув на небольшой пример, который я уже приготовил для вас.
Конечно же, реализация и конечный размер контейнера зависят от используемой стандартной библиотеки, а также от различных оптимизаций (см. Small String Optimization), поэтому результат у вас может отличаться. Интересное исследование на тему внутренностей std::string можно прочитать здесь.
Почему?
Итак, в проблеме разобрались и выяснили, что так делать не надо. Но ведь интересно, как к этому пришли?
В случае OpenTTD всё достаточно просто. Судя по blame, почти четыре года назад тип поля default_company_pass изменили с char[NETWORK_PASSWORD_LENGTH] на std::string. Любопытно, что текущее значение, возвращаемое макросом lenghtof, отличается от прошлого ожидаемого: 32 против 33. Каюсь, не стал сильнее вникать в код проекта, но надеюсь, что разработчики учли этот нюанс. Судя по комментарию, после поля default_company_pass 33 символ отвечал за нуль-терминал.
// The maximum length of the password, in bytes including '\0'
// (must be >= NETWORK_SERVER_ID_LENGTH)
Legacy и небольшая невнимательность при рефакторинге — казалось бы, вот она, причина. Но, как ни странно, такой способ вычисления размера массива встречается даже в новом коде. Если с языком C все понятно — иначе никак, то что не так с С++? За ответом я пошёл в Google Поиск и не сказать, чтобы удивился...
Прямо в самом начале, даже до основных результатов поиска, выдаётся вот это :( Здесь стоит сделать ремарку, что для поиска использовался приватный режим, чистый компьютер и прочие нюансы, которые отметают подозрения в том, что это поиск на основе моих прошлых запросов.
Прим. автора: стало даже немного интересно. Напишите в комментариях, что показывает вам в топе выдачи по такому же запросу.
Печально. Надеюсь, что ИИ, обучающиеся на текущем коде, не будут совершать подобных ошибок.
Как надо
Было бы некрасиво обозначить проблему и не предложить хороших путей решения. Осталось только понять, что с этим делать. Предлагаю начать по порядку и постепенно дойти до наилучшего на текущий момент решения.
Итак, sizeof((expr)) / sizeof((expr)[0]) — это настоящий магнит для ошибок. Посудите сами:
Для динамически выделенных буферов sizeof посчитает не то, что надо;
Если builtin-массив передали в функцию по копии, то sizeof на нём тоже вернёт не то, что надо.
Раз уж мы тут пишем на С++, то давайте воспользуемся мощью шаблонов! Тут мы приходим к легендарным ArraySizeHelper'ам (aka "безопасный sizeof" в некоторых статьях), которые рано или поздно пишутся почти в каждом проекте. В стародавние времена — до C++11 — можно было встретить таких монстров:
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
#define countof(array) (sizeof(ArraySizeHelper(array)))
Для тех, кто не понял, что тут происходит:
ArraySizeHelper — это шаблон функции, который принимает массив типа T и размера N по ссылке. При этом функция возвращает ссылку на массив типа char размера N.
Чтобы понять, как эта штука работает, рассмотрим небольшой пример:
void foo()
{
int arr[10];
const size_t count = countof(arr);
}
При вызове ArraySizeHelper компилятор должен будет вывести шаблонные параметры из шаблонных аргументов. В нашем случае T будет выведен как int, а N как 10. Возвращаемым типом функции при этом будет тип char (&)[10]. В итоге sizeof вернёт размер этого массива, который и будет равен количеству элементов.
Как можно заметить, у функции отсутствует тело. Сделано это для того, чтобы такую функцию можно было использовать ТОЛЬКО в невычисляемом контексте. Например, когда вызов функции находится в sizeof.
Отдельно замечу, что в сигнатуре функции явно указано, что она принимает именно массив, а не что угодно. Благодаря этому и работает защита от указателей. Если всё же попытаться передать указатель в такой ArraySizeHelper, то получим ошибку компиляции:
void foo(uint8_t* data)
{
auto count = countof(arr); // ошибка компиляции
....
}
Насчёт стародавних времён я не преувеличиваю. Мой коллега ещё в 2011 году разбирался, как работает эта магия в проекте Chromium. С приходом в нашу жизнь C++11 и C++14 писать такие вспомогательные функции стало намного проще:
template <typename T, size_t N>
constexpr size_t countof(T (&arr)[N]) noexcept
{
return N;
}
Но и это ещё не все — можно лучше!
Скорее всего, далее вы столкнётесь с тем, что захотите считать размер контейнеров: std::vector, std::string, QList, — не важно. В таких контейнерах уже есть нужная нам функция — size. Её-то нам и нужно позвать. Добавим перегрузку для функции выше:
template <typename Cont>
constexpr auto countof(const Cont &cont) -> decltype(cont.size())
noexcept(noexcept(cont.size()))
{
return cont.size();
}
Здесь мы просто определили функцию, которая будет принимать любой объект и возвращать результат вызова его функции size. Теперь наша функция имеет защиту от указателей, умеет работать как с builtin-массивами, так и с контейнерами, да ещё и на этапе компиляции.
Ииии я вас поздравляю, мы успешно переизобрели std::size. Его-то я и предлагаю использовать, начиная с C++17, вместо устаревших sizeof-костылей и ArraySizeHelper'ов. Ещё и не нужно каждый раз писать заново: он становится доступен после включения заголовочного файла практически любого контейнера :)
Современный C++: правильное вычисление количества элементов в массивах и контейнерах
Ниже я также предлагаю рассмотреть пару распространённых сценариев для тех, кто вдруг попал сюда из поиска. Далее я буду подразумевать, что std::size доступен в стандартной библиотеке. В ином случае можно скопировать описанные выше функции и использовать их как аналоги.
Я использую какой-нибудь современный контейнер (std::vector, QList и т.п.)
В большинстве случаев лучше использовать функцию-член класса size. Например: std::string::size, std::vector::size, QList::size и т.п. Начиная с C++17, рекомендую перейти на std::size, описанный выше.
std::vector<int> first { 1, 2, 3 };
std::string second { "hello" };
....
const auto firstSize = first.size();
const auto secondSize = second.size();
У меня обычный массив
Также используйте свободную функцию std::size. Как мы уже выяснили выше, она может вернуть количество элементов не только в контейнерах, но в обычных массивах.
static const int MyData[] = { 2, 9, -1, ...., 14 };
....
const auto size = std::size(MyData);
Очевидным плюсом этой функции является то, что при попытке подсунуть ей неподходящий тип или указатель, мы получим ошибку компиляции.
Я внутри шаблона и не знаю, что за контейнер/объект используется на самом деле
Также используйте свободную функцию std::size. В дополнение к неприхотливости в плане типа объекта она ещё и работает на этапе компиляции.
template <typename Container>
void DoSomeWork(const Container& data)
{
const auto size = std::size(data);
....
}
У меня есть два указателя или итератора (начало и конец)
Здесь возможны два варианта в зависимости от ваших потребностей. Если нужно только узнать размер, то достаточно воспользоваться std::distance:
void SomeFunc(iterator begin, iterator end)
{
const auto size = static_cast<size_t>(std::distance(begin, end));
}
Если нужно что-то интереснее простого получения размера, то можно использовать read-only классы-обёртки: std::string_view для строк, std::span в общем случае и т.д. Например:
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 * и т.п.) и точно знаете, что строка содержит нуль-терминал, то ситуация немного лучше. В таком случае можно воспользоваться std::basic_string_view:
const char *text = GetSomeText();
std::string_view view { text };
Как и в примере выше, получаем все достоинства view-классов, имея изначально только один указатель.
Также упомяну менее предпочтительный, но полезный в некоторых ситуациях вариант с использованием std::char_traits::length:
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 или std::wsclen. Обратите внимание, что я не просто так уточнил про обязательное наличие в строке нуль-терминала. В противном случае получите неопределённое поведение (undefined behavior).
Заключение
Надеюсь, мне удалось показать хорошие альтернативы для замены такой простой, но опасной конструкции как sizeof(array) / sizeof(array[0]). Если вам кажется, что я что-то незаслуженно забыл или умолчал — добро пожаловать в комментарии :)
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Mikhail Gelvikh. How not to check array size in C++.
Комментарии (53)
rogoz
05.04.2024 11:48+4За ответом я пошёл в Google Поиск и не сказать, чтобы удивился...
Так всё правильно. std::string не массив, даже std:array не массив, но может им притворяться.
google
Mixxxxa Автор
05.04.2024 11:48+2Так всё правильно. std::string
Изначально там был массив - потому и такой запрос. Если же перефразировать немного правильнее, и написать "контейнер", то результат будет гораздо лучше :) Беда в том, что новичку до этого ещё нужно "дойти".
adeshere
05.04.2024 11:48+5Я из своей фортрановской камеры иногда подглядываю в плюсовые статьи... Сейчас честно порадовался обычаям своего заповедника ;-) Все-таки, у нас с массивами
как-то попроще
Например, вот так будет для массивов с тремя измерениями (код можно легко обобщить на n-мерные массивы):
! Объявляем трехмерные "динамические" массивы:
real, allocatable :: A(:,:,:), B(:,:,:)
! Выделяем память (с этого момента размер массивов
integer :: n = 42
(...)
! и диапазоны изменения индексов зафиксированы):
allocate (A(1:n, 4, 3), B(0:n-1,41:44,1:3))
! Передаем их в какую-нибудь процедуру:
call myfunc(A,B)
.......................
.......................! А это где-то внутри
myfunc(A,B)
:
...integer :: Ashape(3), Bshape(3),
dShape(3)integer :: A_min_index(3), B_min_index(3)
Ashape=shape(A); Bshape=shape(B)
dShape=Ashape-Bshape
A_min_index=lbound(A); B_min_index=lbound(B)
! теперь вектора (массивы) Ashape и Bshape
! содержат размер массивов A и B
! по каждому измерению (т.е.[42,4,3]),
! dShape хранит различия в размерах массивов,
! а A_min_index и B_min_index хранят
! нижнюю границу каждого индекса
! Например, B_min_index=[0,41,1]
(...)
! поэлементное умножение:
if (maxval(abs(dShape)) == 0) A=B*13
! но только ЕСЛИ все три размера у массивов
! одинаковые (модули разностей = 0).
! Диапазоны индексов при этом могут не совпадать -
! главное, чтобы размерчики совпадали...
Ну и отдельным бонусом есть ключ компилятора, который включает проверку, что массив B инициализирован, либо выключает ее (если программа хорошо отлажена и очень торопится, это ее немного ускорит). Кстати, если проверка выключена, а значения каких-то элементов в массиве B не присвоены, то с помощью
A=B*13
у нас в фортране тоже можно добиться UB!UPD: а вот с форматированием кода я по-прежнему справиться не могу. Пробелы как глотались хаброредактором, так и глотаются. Причем в непредсказуемых местах и количестве. Да, у нас в фортране компилятор пробелы игнорит... но вообще-то мы их используем для форматирования и удобочитаемости кода.
numb13
05.04.2024 11:48+2Скрытый текст
subroutine show_arr(arr) integer, intent(in) :: arr(:) print *, arr end subroutine show_arr
Блок кода типа MatLab
subroutine show_arr(arr) integer, intent(in) :: arr(:) print *, arr end subroutine show_arr
Лучше использовать специальный блок для исходного кода.
MiyuHogosha
05.04.2024 11:48+2Я бы не сказал... когда доходило до common blocks. Хотя после 2001го я на Фортране не писал, что-нибудь поменялось?
adeshere
05.04.2024 11:48+3Я бы не сказал... когда доходило до common blocks
;-)
Вы правы, конечно!
Разумеется, любой по-древнему хороший язык программирования должен уметь стрелять себе в ногу очередью из граблей ;-)) Более того, заложенная в фортран совместимость со старым кодом прямо требует, чтобы у программиста было такое право (в теории, наш код из 1956г все еще до сих пор обязан компилироваться и работать ;-)
Но на практике так уже никто не пишет сейчас. Вместо этого есть понятие модуля, где все переменные и структуры описываются один раз, и затем они по необходимости используются в разных местах (со стандартными в современных языках средствами ограничения видимости). Причем, оператор USE сразу оказался намного удобнее всяких includ-ов, так как он автоматически контролирует, чтобы блок включался (и компилировался) ровно один раз (привет, $IF DEFINED ;-). Одно только это обстоятельство было для многих фортранщиков достаточным аргументом сменить стиль, даже еще ничего не понимая в ООП. А еще сейчас принято методы работы с модульными данными тоже писать в тех же модулях. Фактически получаются те же объекты-классы, только с привычным для первобытных людей интерфейсом.
С учетом этого, теперь программисту на фортране необходимо достичь определенного уровня квалификации, чтобы прострелить именно свою ногу, а не ногу соседа ;-)
А вообще, язык, конечно стал заметно другим даже не в плане спецификаций, а скорее по стилю. Например, я знаю только фортран, и вроде бы знаю довольно неплохо. Но прочитать старую (из 1980-х гг) программу на фортране мне порой намного сложнее, чем аналогичную современную (=адекватно оформленную) программу на незнакомом мне (и весьма непростом!) С++. Просто потому, что тот старый фортран порой по читаемости напоминает Brainfuck. Уж не знаю, умышленно, или нет...
Melirius
05.04.2024 11:48+2std::size хорош, но для многомерных массивов всё равно приходится городить огород.
Mixxxxa Автор
05.04.2024 11:48+1Если есть свежий компилятор с поддержкой С++23, то можно использовать std::mdspan.
MiyuHogosha
05.04.2024 11:48+1интересно, а если его переделать в рекусию для случая с массивами... Да только Си++23 превращение многомерного массива в одномерный является UB.
sergio_nsk
05.04.2024 11:48+3Для них есть
std::extent()
.Melirius
05.04.2024 11:48+2Вы мой спаситель! Вот реально, сколько ж всего уже есть в языке, про что узнаёшь внезапно.
buratino
05.04.2024 11:48+3Что заявлено в заголовке? Как не надо проверять размер массива в С++
О чем на самом деле пишет автор? О чем угодно, только не о массивах в С++ и их размерах.
Более того, судя по тексту, получается как в том анекдоте про нам без разницы, лишь бы с ног валило. Т.е. не с ног валило, а данные содержало - хоть массивом назови, хоть контейнером
Mixxxxa Автор
05.04.2024 11:48+2Я думаю, что отвел достаточную часть статьи на массивы, их собратьев и вычисление размеров в каждом случае. Не могли бы вы написать чего именно не хватило?
Если вопрос конкретно к заголовку "Как не надо проверять размер массива в С++", то ответ идёт через всю статью - используйте современные средства вместо
sizeof((expr)) / sizeof((expr)[0])
. Варианты на замену я также расписал в статье.buratino
05.04.2024 11:48+3контейнер - не массив. Объект - не массив. И если уж на то пошло - надо уточнять, что такое размер массива.
sizeof((expr)) / sizeof((expr)[0])
- размер в элементах,sizeof((expr)) - в байтах. В вашей же многобукве про это как-то скромно умалчивается
Более того, вы вот претендуете на не начинающего программиста
т.к. целевой аудиторией заметки являются начинающие программисты. Не будем учить их плохому :)
flame on
но почему-то таки неявно учите, что массивы в С++ - это std::xxx, а с ними нужны современные подходы, цыфровая трансформация и всякий прочий эффективный менеджмент
flame off
MiyuHogosha
05.04.2024 11:48+1Здесь проблема подхода неустойчивого к рефакторингу. Когда-то в коде эта строка была фиксированным массивом и подход был легален. Массив заменили на строку, но использования не отследили. Самое смешное, что с короткими строками в некоторых реализациях это даже работает.
Хотя std::size появилось в С++17, ее аналог мусолят где-то с 1999 во всех возможных блогах, даже Мейерс об этом писал.
buratino
05.04.2024 11:48+1Здесь проблема подхода неустойчивого к рефакторингу.
ну так это совсем другое и из другой оперы - "Кто чем и о чем думает, когда что-то делает". ... тут в параллельной вселенной пишут "не думая, она кинулась"
Хотя std::size появилось в С++17, ее аналог мусолят где-то с 1999 во всех возможных блогах,
и что? Заявлены массивы, а не std:: со своими граблями
MiyuHogosha
05.04.2024 11:48+3Как говорил один сотрудник, это не грабли, а плохо продуманные костыли. Увы, в языке без отражений иначе не получится. Кстати, я только сейчас заметил комментарий про функцию в Хромиуме... это код скопированный у меня одним бывшим сотрудником, ушедшим туда (с точностью до символа). А я брал идею у Мейерса. Как тесен мир.
Собственно это намеренно проталкивалость через все щели, включая буст, в стандарт. std::size, std::data и прочее. У меня были идеи как решить прблему с многомерными массивами, но фишка в том что стандарт ими не оперирует. С точки зрения стандарта многомерный массив размерности N - это одномерный массив элементов-массивов размерности N-1.И представление его как одномерного попахивает UB до Си++23.
Главное отучить народ от этой адской формулы деления.
buratino
05.04.2024 11:48Как говорил один сотрудник, это не грабли, а плохо продуманные костыли.
ну, в общем (не только std::) случае может быть все что угодно
Как тесен мир.
аааааааааааааааааааа....
Mixxxxa Автор
05.04.2024 11:48+3В вашей же многобукве про это как-то скромно умалчивается
Действительно, есть такое упущение...
но почему-то таки неявно учите, что массивы в С++ - это std::xxx, а с ними нужны современные подходы, цыфровая трансформация и всякий прочий эффективный менеджмент
Не совсем так. Заманчиво просто советовать использовать средства стандартной библиотеки везде и вся, но я понимаю, что ситуации бывают разные - потому в конце есть целый раздел с подборкой вариантов в зависимости от ситуации.
В целом, не вижу ничего плохого в том чтобы новички изучали
std
контейнеры до build-in массивов. Да, получается не hardcore-программирование, но благодаря этому можно будет перестать изобретать неудачные велосипеды вместо стандартных реализаций. Набрался опыта - добро пожаловать на уровень "глубже".Пока писал ответ, вспомнилась неудачная реализация кастомной функции конкатенации строк из LFortran. Мой коллега даже написал про тот случай отдельную статью.
Cfyz
05.04.2024 11:48+1неявно учите, что массивы в С++ - это std::xxx
Если начинающие программисты будут думать что массивы в C++ это std::array и std::vector, а T[] это такая штука из далекого прошлого примерно как register или std::auto_ptr, то всем будет только лучше.
9241304
05.04.2024 11:48+2Тут не очень понятно, для кого статья. Новички сразу заюзают нужную функцию из библиотеки. Кто хотел, уже переучился. Ну а остальные необучаемые мамонты из принципа будут писать sizeof)
Mixxxxa Автор
05.04.2024 11:48Новички сразу заюзают нужную функцию из библиотеки
Если бы всё было так просто... Про тот-же std::size ещё нужно знать (если вы про эту функцию).
Несколько лет назад, в моём университе, всё ещё преподавали C++ как Си с классами со всеми вытекающими. Однокурсники просто не знали, что можно реализовать многие вещи гораздо проще / удобнее / безопаснее. Эта статья как раз написана в надежде, что интересующийся человек увидит современные варианты работы с плюсами вместо очередной копипасты про
sizeof
.buratino
05.04.2024 11:48+2Несколько лет назад, в моём университе, всё ещё преподавали C++ как Си с классами со всеми вытекающими. Однокурсники просто не знали, что можно реализовать многие вещи гораздо проще / удобнее / безопаснее
очень просто и удобно навернуться в эксепшн еще до main(), особенно в коде, который везет какую-нить бадью с расплавленным металлом или еще где...
Mixxxxa Автор
05.04.2024 11:48+2В коде, который везёт бадью с расплавленным металлом, скорее всего, будут запрещены исключения, динамические аллокации памяти, RTTI и т.д. Написание насколько критичного ПО это целый отдельный мир с кучей ограничений, стандартов безопасности, аудитов и т.п. Не думаю, что целесообразно обсуждать данную область в рамках этой статьи.
buratino
05.04.2024 11:48+2Эти запреты не помогут в вышеуказанной ситуации. Особенно если писатель кода будет не в курсе чем отличается Си с классами, от плюсов на всю голову. Насколько критично ПО в случае с бадьей выяснилось после того как она подъехала к ящику с системой управления бадьей и вылила все содержимое на нее. Правда это было давно и я лично процесс не видел. Рамки статьи определяются заголовком, а заголовок ну ооочень обобщающий. А конкретное - по поводу преподавания в вашем университете. Между миром х--х-и в продакшен и там, где куча ограничений и стандартов безопасности не такая уж и большая разница и нет четкой границы
9241304
05.04.2024 11:48+2вайтишник пойдёт на стэковерфлоу и увидит самый простой вариант. крайне ленивый вайтишник спросит у чатгпт и скорее всего получит правильный ответ. про непризнающих прогресс преподов см. пункт про мамонтов (вообще не понимаю, зачем этим людям плюсы). про не умеющих ничего искать однокурсников - отдельная песня. помнится, в моей группе один чувак (будущий электромеханик) ГЕНЕРИЛ курсачи для программистов на нашем факультете (это одновременно про уровень преподов и студентов). Так что не путайте тех, кто пришёл просто за коркой, с теми, кто будет реально что-то писать.
Ну и хотел добавить, что использование сырых массивов в современных плюсах - это само по себе редкое явление... ну если использующий - не тот самый принципиальный мамонт. А брать размер таких массивов - ну вообще не понятно, зачем ))
eptr
05.04.2024 11:48+2крайне ленивый вайтишник спросит у чатгпт и скорее всего получит правильный ответ.
Скорее всего, нет: ссылка.
Там код криво отображается, однако, если его выделить, то он становится виден.И даже если заставлять его перегенерировать ответ, станет ясно, что он сам не знает, что лучше, и в конце все равно даёт совет использовать
sizeof
для статических массивов: ссылка.
buratino
05.04.2024 11:48+1Ну и хотел добавить, что использование сырых массивов в современных плюсах - это само по себе редкое явление... ну если использующий - не тот самый принципиальный мамонт. А брать размер таких массивов - ну вообще не понятно, зачем ))
да... а давайте мы изображения обрабатывать без сырых массивов, тут всякие распальцованные говорят, что это редкое явление
Mixxxxa Автор
05.04.2024 11:48а давайте мы изображения обрабатывать без сырых массивов
Скорее всего, использовать именно сырые массивы и указатели вас будет вынуждать какая-нибудь библиотека с Си интерфейсом. Вне её зоны ответсвенности вполне можно использовать контейнеры и различные *view.
buratino
05.04.2024 11:48+1рука-лицо. Покажите как вы с этой лабудой выполняете элементарную операцию типа фильтрации окном 3x3 изображения 102x1024 и на сколько все это будет тормознее по сравнению с классическим подходом, без библиотек
Mixxxxa Автор
05.04.2024 11:48К сожалению, я не компетентен в заданной вами области. Поэтому не могу конкретно сказать, насколько использование какого-нибудь std::span, вместо сырых массивов, замедлит те или иные операции в обработке изображений. Если у вас есть достоверные бенчмарки - буду рад ознакомиться. Информации на эту тему в сети не много, а потому вы могли бы написать интересную статью на эту тему.
Если вы думаете что я топлю за запрет сырых указателей, built-in массивов и прочего, то вовсе нет. Мне бы тогда пришлось бросить embedded-разработку :) В общем случае я предпочту абстракции, но если уж дело дошло до оптимизаций, то тут все средства хороши. И я очень рад что С++ предоставляет настолько широкое поле для маневров.
buratino
05.04.2024 11:48К сожалению, я не компетентен в заданной вами области.
рука-лицо.
Уж не знаю, какой такой embedded-разработкой вы занимаетесь, но сейчас embedded вполне может быть и с обработкой изображений, начиная с его получения, с, внезапно, матрицы- двумерного массива.... В другом embedded случае совершенно внезапно используется перемножение матриц, т.е. двумерных массивов в полетном контроллере. И никто дурью не мается
Если у вас есть достоверные бенчмарки - буду рад ознакомиться. Информации на эту тему в сети не много, а потому вы могли бы написать интересную статью на эту тему.
никому в голову не приходит маяться дурью, все хорошо в своей области использования. Если вам совсем заняться нечем - перепишите хорошо известную библиотеку с контейнерами и без raw массивов. Заодно можно сделать то же самое с jpglib и pnglib а потом запилить бенчмарки..
Mixxxxa Автор
05.04.2024 11:48но сейчас embedded вполне может быть и с обработкой изображений, начиная с его получения, с, внезапно, матрицы- двумерного массива...
Внезапно, но embedded, как и разработка в целом, не зациклена на одной только обработке изображений и операциях над матрицами.
Если вам совсем заняться нечем - перепишите хорошо известную библиотеку с контейнерами и без raw массивов.
За ссылку спасибо, а вот переписывание и эксперименты над библиотекой предпочту оставить заинтересованным лицам.
MiyuHogosha
05.04.2024 11:48Смотря как на это посмотреть.. огромное количество задачь реализуется через работу с матрицами и изображениями, не только в эмбеддед, даже если они не связаны. Вся сложная криптография, например. обработка сигналов - это матрицы, матрицы и еще раз матрицв. Одна из бед этой сферы необходимость оптимизироваться под потоковые опервции (SIMD, SSE, NEON и т.д.) где абстракции границ объектов будут только удалять гарантиии... вот пишется либо на Си, либо С++. Одна здакомая проводила сравнение на примере спектрального анализа на андроидедля Software Defined Radio: Java в 20 медленнее чем нативный "высокородный" C++, а тот в 3-5 раз медленнее чем C++\C с явно использованнимы функциями NEON
sumrak00
05.04.2024 11:48+2Большое спасибо за такую интересную статью!
Я читала статью как начинающий программист, прошедший половину базового курса ООП на плюсах: как будто немного не хватает уточнений по ходу текста. Используются термины, но не поясняется, в чем между ними разница. Например, как я в итоге поняла из статьи, проблема sizeof() в том, что она считает разницу между указателем на начало и на конец. Но в абзаце это сформулировано не очень явно. Было бы здорово, наверное, добавить какое-то краткое обобщение абзаца в конце с простыми пояснениями (у нас есть вот это, но оно работает вот так — поэтому нам это не подходит в современной жизни).
Дальше по тексту начинается упоминание и перечисление разных структур. Разницу между некоторыми я знаю на опыте, но в целом я восприняла это как отрывок разговора между опытными программистами на кухне — что-то краем уха удалось уловить и осознать, но в целом лавина из терминов. Могу предложить, что здесь помогла бы более резкая структуризация текста, чтобы разделить переход от обсуждения использования функций вычисления размера одних типов от других.
Примеры и ссылки очень познавательные. В целом прочитать статью было одно удовольствие. Большое спасибо! :)
isadora-6th
05.04.2024 11:48+2Используйте std контейнеры (почти всегда std::vector, где надо - std::unordered_map/unordered_set), а массивы и ручное придумывание себе головняка оставьте свидетелям проблем в стд.
Проблема sizeof в том, что им очень просто выстрелить в указатель, сделать не то, что задумано, вообще не получить толкового результата, а все от того, что кому-то лень узнать про существование массива в котором уже лежит сайз-капасити (вектор) и не городить головняк с узнаванием размера постфактум.
Хотя честно говоря std::array, не то чтобы очень удобен в использовании. Постоянно упираешься в то, что он фикс длинны и надо городить вокруг него зоопарк с size/capacity чтобы красиво заполнять данными. Вот бы заехал SSO вариант вектора в стандарт, ух зажили бы...
MiyuHogosha
05.04.2024 11:48Нужно еще не забыть что size и capacity - разные вещи. Да и оригинал кода писался еще в страхе лишних выделений памяти -SSO тогда не было. Хотя нынче на некоторых реализациях строка в 32 символа в SSO не влезает.
А еще некоторые люди боятся этих классов потому что "их нет в документации и они - сторонняя библиотека". Это результат методики обучения в ВУЗах где под страхом топора нельзя ими пользоваться.
eptr
05.04.2024 11:48+2Я читала статью как начинающий программист, прошедший половину базового курса ООП на плюсах: как будто немного не хватает уточнений по ходу текста. Используются термины, но не поясняется, в чем между ними разница.
Уровень сложности статьи заявлен как "Средний", видимо, этот уровень у вас ещё впереди.
Например, как я в итоге поняла из статьи, проблема sizeof() в том, что она считает разницу между указателем на начало и на конец.
Нет, операция
sizeof
не считает разницу между указателем на начало и конец.
И это — не функция, а операция.Проблема состоит в том, что в выражении
sizeof(a) / sizeof(a[0])
в качествеa
может выступать не только массив, и при этом никаких ошибок компиляции и даже предупреждений не будет, вместо этого будет неправильно вычисленное значение.В качестве
a
может выступать, например, указатель или тип, для которого определёнoperator []
, возвращающий подходящий тип: в этих случаях выражениеsizeof(a) / sizeof(a[0])
валидно, и поэтому нет ни ошибки компиляции, ни предупреждения, но результат, тем не менее, почти наверняка, — неверен.Использование шаблонной функции
std::size()
вместо выраженияsizeof(a) / sizeof(a[0])
решает эту проблему. Добавление специализаций этой функции решает дополнительные проблемы подобного рода.Было бы здорово, наверное, добавить какое-то краткое обобщение абзаца в конце с простыми пояснениями (у нас есть вот это, но оно работает вот так — поэтому нам это не подходит в современной жизни).
Для этого есть комментарии, если, конечно, найдётся кто-то, кто кратко объяснит суть дела.
Mixxxxa Автор
05.04.2024 11:48Спасибо за отзыв и подмеченные недостатки. Постраюсь учесть и избежать их в будущих публикациях :)
На остальные вопросы, вроде, уже успели ответить неравнодушные комментаторы.
datacompboy
05.04.2024 11:48+1Очевидным плюсом этой функции является то, что при попытке подсунуть ей неподходящий тип или указатель, мы получим ошибку компиляции.
А если у подсунутого типа перегружен оператор []... Мы получим ошибку компиляции, или результат будет подобен std::string и классическому делению?
eptr
05.04.2024 11:48+1А если у подсунутого типа перегружен оператор []... Мы получим ошибку компиляции, или результат будет подобен std::string и классическому делению?
Если посмотреть справочник, максимально близкий к первоисточнику, то выяснится, что шаблонная функция
std::size()
вызывает методsize()
у переданного объекта, и имеется лишь только одна специализацияstd::size()
для массивов.Никакого отношения к наличию или отсутствию
operator []
эта шаблонная функция не имеет.Однако, сказано:
Custom overloads of
size
may be provided for classes and enumerations that do not expose a suitablesize()
member function, yet can be detected.То есть, вопрос не в наличии или отсутствии
operator []
, а в наличии или отсутствии подходящего методаsize()
у объекта.При этом, если подходящего метода нет, допустимо добавить свою специализацию шаблонной функции
std:size()
, и тогдаstd::size()
начнёт работать правильно и для этого типа. В этом сила свободной функции по сравнению с вызовом метода.Вот пример.
У
struct one
вообще нет методаsize()
.
А уstruct two
он есть, но возвращает совсем не то, что надо.
Но это решается добавлением соответствующих специализацийstd::size()
.
После раскомментирования#if 0
в двух местах всё начинает работать правильно.Именно по этой причине следует предпочитать использованию методов использование свободных (шаблонных) функций
std::begin()
,std::end()
,std::size()
,std::data()
и так далее.datacompboy
05.04.2024 11:48+1¿А, то есть для стандартных массивов она не делает sizeof/sizeof[]?
Понятно, спасибо!
eptr
05.04.2024 11:48+2¿А, то есть для стандартных массивов она не делает sizeof/sizeof[]?
Да, верно.
В этом справочнике, который я настоятельно рекомендую, поскольку он, фактически, содержит выжимки из стандартов, а стандарт это — последняя инстанция, для
std::size()
есть раздел Possible Implementation, в котором есть частьsize (3)
.Вот эту реализацию можно попробовать самому и убедиться, что она работает, причём функция является
constexpr
, то есть, в тех случаях, когда это возможно, её значение может быть вычислено и использовано прямо на этапе компиляции.Вот пример, в котором количество элементов массива
a1
задаётся с помощью вызова функцииstd::size()
для массиваa0
, а количество элементов массиваa2
задаётся с помощью вызова функцииmy::size()
для массиваa1
, которая взята из того самого раздела Possible Implementation, и всё прекрасно компилируется, ни один компилятор даже ни пикнул.datacompboy
05.04.2024 11:48+1Логично, можно же шаблонное раскрытие взять. Спасибо!
MiyuHogosha
05.04.2024 11:48И реализация (с поправками) работала в самом древнейшем компиляторе, я сто лет пользуюсь. VC6? gcc3? без проблем. Не знаю почему он знаковую версию только сейчас содрали.
MiyuHogosha
05.04.2024 11:48+1У меня первым результатом вылезно https://ru.stackoverflow.com/questions/578109/Как-узнать-размер-массива-переданного-в-функцию
Повезло... и Гугл.
Ну может, потому что я - мод на стеке.
А вообще на русском часто вообще бред вылезает, ссылки на индусские ютубы или поддельные сайты (составленные из нелегальных автопереводов из стека и реддита). Я редко ищу на русском. Слова-враги переводчика тоже мешаются.
Яндекс не нашел ничего приличного... вернее выдал Си-шарп почему-то.
Бинг нашел MSDN
Mixxxxa Автор
05.04.2024 11:48+1В нашем блоге есть англоязычная версия данной статьи - для неё я сделал запрос и скриншот на английском. Результат +/- тот-же, только на сайт другой указывает.
MiyuHogosha
05.04.2024 11:48Зависит от того, что искать. я часто ищу по Qt и бусту. Ну и да, профиль обучения поисковика у вас может быть получше
dyadyaSerezha
У вас есть результаты сравнения вашего продукта с такими известными продуктами, как Fortify, Checkmarx, SonarQube?
Mixxxxa Автор
Как раз недавно записывали подкаст с ответом на подобный вопрос. Есть короткая версия.