Как часто вам приходится сталкиваться с конструкцией 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]) — это настоящий магнит для ошибок. Посудите сами:

  1. Для динамически выделенных буферов sizeof посчитает не то, что надо;

  2. Если 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)


  1. dyadyaSerezha
    05.04.2024 11:48

    У вас есть результаты сравнения вашего продукта с такими известными продуктами, как Fortify, Checkmarx, SonarQube?


    1. Mixxxxa Автор
      05.04.2024 11:48
      +5

      Как раз недавно записывали подкаст с ответом на подобный вопрос. Есть короткая версия.


  1. rogoz
    05.04.2024 11:48
    +4

    За ответом я пошёл в Google Поиск и не сказать, чтобы удивился...

    Так всё правильно. std::string не массив, даже std:array не массив, но может им притворяться.

    google


    1. Mixxxxa Автор
      05.04.2024 11:48
      +2

      Так всё правильно. std::string

      Изначально там был массив - потому и такой запрос. Если же перефразировать немного правильнее, и написать "контейнер", то результат будет гораздо лучше :) Беда в том, что новичку до этого ещё нужно "дойти".


    1. 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: а вот с форматированием кода я по-прежнему справиться не могу. Пробелы как глотались хаброредактором, так и глотаются. Причем в непредсказуемых местах и количестве. Да, у нас в фортране компилятор пробелы игнорит... но вообще-то мы их используем для форматирования и удобочитаемости кода.


      1. 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

        Лучше использовать специальный блок для исходного кода.


      1. MiyuHogosha
        05.04.2024 11:48
        +2

        Я бы не сказал... когда доходило до common blocks. Хотя после 2001го я на Фортране не писал, что-нибудь поменялось?


        1. adeshere
          05.04.2024 11:48
          +3

          Я бы не сказал... когда доходило до common blocks

          ;-)

          Вы правы, конечно!

          Разумеется, любой по-древнему хороший язык программирования должен уметь стрелять себе в ногу очередью из граблей ;-)) Более того, заложенная в фортран совместимость со старым кодом прямо требует, чтобы у программиста было такое право (в теории, наш код из 1956г все еще до сих пор обязан компилироваться и работать ;-)

          Но на практике так уже никто не пишет сейчас. Вместо этого есть понятие модуля, где все переменные и структуры описываются один раз, и затем они по необходимости используются в разных местах (со стандартными в современных языках средствами ограничения видимости). Причем, оператор USE сразу оказался намного удобнее всяких includ-ов, так как он автоматически контролирует, чтобы блок включался (и компилировался) ровно один раз (привет, $IF DEFINED ;-). Одно только это обстоятельство было для многих фортранщиков достаточным аргументом сменить стиль, даже еще ничего не понимая в ООП. А еще сейчас принято методы работы с модульными данными тоже писать в тех же модулях. Фактически получаются те же объекты-классы, только с привычным для первобытных людей интерфейсом.

          С учетом этого, теперь программисту на фортране необходимо достичь определенного уровня квалификации, чтобы прострелить именно свою ногу, а не ногу соседа ;-)

          А вообще, язык, конечно стал заметно другим даже не в плане спецификаций, а скорее по стилю. Например, я знаю только фортран, и вроде бы знаю довольно неплохо. Но прочитать старую (из 1980-х гг) программу на фортране мне порой намного сложнее, чем аналогичную современную (=адекватно оформленную) программу на незнакомом мне (и весьма непростом!) С++. Просто потому, что тот старый фортран порой по читаемости напоминает Brainfuck. Уж не знаю, умышленно, или нет...


  1. Melirius
    05.04.2024 11:48
    +2

    std::size хорош, но для многомерных массивов всё равно приходится городить огород.


    1. Mixxxxa Автор
      05.04.2024 11:48
      +1

      Если есть свежий компилятор с поддержкой С++23, то можно использовать std::mdspan.


    1. MiyuHogosha
      05.04.2024 11:48
      +1

      интересно, а если его переделать в рекусию для случая с массивами... Да только Си++23 превращение многомерного массива в одномерный является UB.


    1. 9241304
      05.04.2024 11:48
      +1

      опытный огородник найдёт себе сложности )


    1. sergio_nsk
      05.04.2024 11:48
      +3

      Для них есть std::extent() .


      1. Melirius
        05.04.2024 11:48
        +2

        Вы мой спаситель! Вот реально, сколько ж всего уже есть в языке, про что узнаёшь внезапно.


  1. buratino
    05.04.2024 11:48
    +3

    Что заявлено в заголовке? Как не надо проверять размер массива в С++

    О чем на самом деле пишет автор? О чем угодно, только не о массивах в С++ и их размерах.

    Более того, судя по тексту, получается как в том анекдоте про нам без разницы, лишь бы с ног валило. Т.е. не с ног валило, а данные содержало - хоть массивом назови, хоть контейнером


    1. Mixxxxa Автор
      05.04.2024 11:48
      +2

      Я думаю, что отвел достаточную часть статьи на массивы, их собратьев и вычисление размеров в каждом случае. Не могли бы вы написать чего именно не хватило?

      Если вопрос конкретно к заголовку "Как не надо проверять размер массива в С++", то ответ идёт через всю статью - используйте современные средства вместо sizeof((expr)) / sizeof((expr)[0]). Варианты на замену я также расписал в статье.


      1. buratino
        05.04.2024 11:48
        +3

        контейнер - не массив. Объект - не массив. И если уж на то пошло - надо уточнять, что такое размер массива. sizeof((expr)) / sizeof((expr)[0]) - размер в элементах, sizeof((expr)) - в байтах. В вашей же многобукве про это как-то скромно умалчивается

        Более того, вы вот претендуете на не начинающего программиста

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

        flame on

        но почему-то таки неявно учите, что массивы в С++ - это std::xxx, а с ними нужны современные подходы, цыфровая трансформация и всякий прочий эффективный менеджмент

        flame off


        1. MiyuHogosha
          05.04.2024 11:48
          +1

          Здесь проблема подхода неустойчивого к рефакторингу. Когда-то в коде эта строка была фиксированным массивом и подход был легален. Массив заменили на строку, но использования не отследили. Самое смешное, что с короткими строками в некоторых реализациях это даже работает.

          Хотя std::size появилось в С++17, ее аналог мусолят где-то с 1999 во всех возможных блогах, даже Мейерс об этом писал.


          1. buratino
            05.04.2024 11:48
            +1

            Здесь проблема подхода неустойчивого к рефакторингу.

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

            Хотя std::size появилось в С++17, ее аналог мусолят где-то с 1999 во всех возможных блогах,

            и что? Заявлены массивы, а не std:: со своими граблями


            1. MiyuHogosha
              05.04.2024 11:48
              +3

              Как говорил один сотрудник, это не грабли, а плохо продуманные костыли. Увы, в языке без отражений иначе не получится. Кстати, я только сейчас заметил комментарий про функцию в Хромиуме... это код скопированный у меня одним бывшим сотрудником, ушедшим туда (с точностью до символа). А я брал идею у Мейерса. Как тесен мир.

              Собственно это намеренно проталкивалость через все щели, включая буст, в стандарт. std::size, std::data и прочее. У меня были идеи как решить прблему с многомерными массивами, но фишка в том что стандарт ими не оперирует. С точки зрения стандарта многомерный массив размерности N - это одномерный массив элементов-массивов размерности N-1.И представление его как одномерного попахивает UB до Си++23.

              Главное отучить народ от этой адской формулы деления.


              1. buratino
                05.04.2024 11:48

                Как говорил один сотрудник, это не грабли, а плохо продуманные костыли.

                ну, в общем (не только std::) случае может быть все что угодно

                Как тесен мир.

                аааааааааааааааааааа....


        1. Mixxxxa Автор
          05.04.2024 11:48
          +3

          В вашей же многобукве про это как-то скромно умалчивается

          Действительно, есть такое упущение...

          но почему-то таки неявно учите, что массивы в С++ - это std::xxx, а с ними нужны современные подходы, цыфровая трансформация и всякий прочий эффективный менеджмент

          Не совсем так. Заманчиво просто советовать использовать средства стандартной библиотеки везде и вся, но я понимаю, что ситуации бывают разные - потому в конце есть целый раздел с подборкой вариантов в зависимости от ситуации.

          В целом, не вижу ничего плохого в том чтобы новички изучали std контейнеры до build-in массивов. Да, получается не hardcore-программирование, но благодаря этому можно будет перестать изобретать неудачные велосипеды вместо стандартных реализаций. Набрался опыта - добро пожаловать на уровень "глубже".

          Пока писал ответ, вспомнилась неудачная реализация кастомной функции конкатенации строк из LFortran. Мой коллега даже написал про тот случай отдельную статью.


        1. Cfyz
          05.04.2024 11:48
          +1

          неявно учите, что массивы в С++ - это std::xxx

          Если начинающие программисты будут думать что массивы в C++ это std::array и std::vector, а T[] это такая штука из далекого прошлого примерно как register или std::auto_ptr, то всем будет только лучше.


  1. 9241304
    05.04.2024 11:48
    +2

    Тут не очень понятно, для кого статья. Новички сразу заюзают нужную функцию из библиотеки. Кто хотел, уже переучился. Ну а остальные необучаемые мамонты из принципа будут писать sizeof)


    1. Mixxxxa Автор
      05.04.2024 11:48

      Новички сразу заюзают нужную функцию из библиотеки

      Если бы всё было так просто... Про тот-же std::size ещё нужно знать (если вы про эту функцию).

      Несколько лет назад, в моём университе, всё ещё преподавали C++ как Си с классами со всеми вытекающими. Однокурсники просто не знали, что можно реализовать многие вещи гораздо проще / удобнее / безопаснее. Эта статья как раз написана в надежде, что интересующийся человек увидит современные варианты работы с плюсами вместо очередной копипасты про sizeof.


      1. buratino
        05.04.2024 11:48
        +2

        Несколько лет назад, в моём университе, всё ещё преподавали C++ как Си с классами со всеми вытекающими. Однокурсники просто не знали, что можно реализовать многие вещи гораздо проще / удобнее / безопаснее

        очень просто и удобно навернуться в эксепшн еще до main(), особенно в коде, который везет какую-нить бадью с расплавленным металлом или еще где...


        1. Mixxxxa Автор
          05.04.2024 11:48
          +2

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


          1. flx0
            05.04.2024 11:48
            +2

            Так в плюсах исключения и rtti неотключаемые по стандарту:)


          1. buratino
            05.04.2024 11:48
            +2

            Эти запреты не помогут в вышеуказанной ситуации. Особенно если писатель кода будет не в курсе чем отличается Си с классами, от плюсов на всю голову. Насколько критично ПО в случае с бадьей выяснилось после того как она подъехала к ящику с системой управления бадьей и вылила все содержимое на нее. Правда это было давно и я лично процесс не видел. Рамки статьи определяются заголовком, а заголовок ну ооочень обобщающий. А конкретное - по поводу преподавания в вашем университете. Между миром х--х-и в продакшен и там, где куча ограничений и стандартов безопасности не такая уж и большая разница и нет четкой границы


      1. 9241304
        05.04.2024 11:48
        +2

        вайтишник пойдёт на стэковерфлоу и увидит самый простой вариант. крайне ленивый вайтишник спросит у чатгпт и скорее всего получит правильный ответ. про непризнающих прогресс преподов см. пункт про мамонтов (вообще не понимаю, зачем этим людям плюсы). про не умеющих ничего искать однокурсников - отдельная песня. помнится, в моей группе один чувак (будущий электромеханик) ГЕНЕРИЛ курсачи для программистов на нашем факультете (это одновременно про уровень преподов и студентов). Так что не путайте тех, кто пришёл просто за коркой, с теми, кто будет реально что-то писать.

        Ну и хотел добавить, что использование сырых массивов в современных плюсах - это само по себе редкое явление... ну если использующий - не тот самый принципиальный мамонт. А брать размер таких массивов - ну вообще не понятно, зачем ))


        1. eptr
          05.04.2024 11:48
          +2

          крайне ленивый вайтишник спросит у чатгпт и скорее всего получит правильный ответ.

          Скорее всего, нет: ссылка.
          Там код криво отображается, однако, если его выделить, то он становится виден.

          И даже если заставлять его перегенерировать ответ, станет ясно, что он сам не знает, что лучше, и в конце все равно даёт совет использовать sizeof для статических массивов: ссылка.


        1. buratino
          05.04.2024 11:48
          +1

          Ну и хотел добавить, что использование сырых массивов в современных плюсах - это само по себе редкое явление... ну если использующий - не тот самый принципиальный мамонт. А брать размер таких массивов - ну вообще не понятно, зачем ))

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


          1. Mixxxxa Автор
            05.04.2024 11:48

            а давайте мы изображения обрабатывать без сырых массивов

            Скорее всего, использовать именно сырые массивы и указатели вас будет вынуждать какая-нибудь библиотека с Си интерфейсом. Вне её зоны ответсвенности вполне можно использовать контейнеры и различные *view.


            1. buratino
              05.04.2024 11:48
              +1

              рука-лицо. Покажите как вы с этой лабудой выполняете элементарную операцию типа фильтрации окном 3x3 изображения 102x1024 и на сколько все это будет тормознее по сравнению с классическим подходом, без библиотек


              1. Mixxxxa Автор
                05.04.2024 11:48

                К сожалению, я не компетентен в заданной вами области. Поэтому не могу конкретно сказать, насколько использование какого-нибудь std::span, вместо сырых массивов, замедлит те или иные операции в обработке изображений. Если у вас есть достоверные бенчмарки - буду рад ознакомиться. Информации на эту тему в сети не много, а потому вы могли бы написать интересную статью на эту тему.

                Если вы думаете что я топлю за запрет сырых указателей, built-in массивов и прочего, то вовсе нет. Мне бы тогда пришлось бросить embedded-разработку :) В общем случае я предпочту абстракции, но если уж дело дошло до оптимизаций, то тут все средства хороши. И я очень рад что С++ предоставляет настолько широкое поле для маневров.


                1. buratino
                  05.04.2024 11:48

                  К сожалению, я не компетентен в заданной вами области.

                  рука-лицо.

                  Уж не знаю, какой такой embedded-разработкой вы занимаетесь, но сейчас embedded вполне может быть и с обработкой изображений, начиная с его получения, с, внезапно, матрицы- двумерного массива.... В другом embedded случае совершенно внезапно используется перемножение матриц, т.е. двумерных массивов в полетном контроллере. И никто дурью не мается

                  Если у вас есть достоверные бенчмарки - буду рад ознакомиться. Информации на эту тему в сети не много, а потому вы могли бы написать интересную статью на эту тему.

                  никому в голову не приходит маяться дурью, все хорошо в своей области использования. Если вам совсем заняться нечем - перепишите хорошо известную библиотеку с контейнерами и без raw массивов. Заодно можно сделать то же самое с jpglib и pnglib а потом запилить бенчмарки..


                  1. Mixxxxa Автор
                    05.04.2024 11:48

                    но сейчас embedded вполне может быть и с обработкой изображений, начиная с его получения, с, внезапно, матрицы- двумерного массива...

                    Внезапно, но embedded, как и разработка в целом, не зациклена на одной только обработке изображений и операциях над матрицами.

                    Если вам совсем заняться нечем - перепишите хорошо известную библиотеку с контейнерами и без raw массивов.

                    За ссылку спасибо, а вот переписывание и эксперименты над библиотекой предпочту оставить заинтересованным лицам.


                    1. MiyuHogosha
                      05.04.2024 11:48

                      Смотря как на это посмотреть.. огромное количество задачь реализуется через работу с матрицами и изображениями, не только в эмбеддед, даже если они не связаны. Вся сложная криптография, например. обработка сигналов - это матрицы, матрицы и еще раз матрицв. Одна из бед этой сферы необходимость оптимизироваться под потоковые опервции (SIMD, SSE, NEON и т.д.) где абстракции границ объектов будут только удалять гарантиии... вот пишется либо на Си, либо С++. Одна здакомая проводила сравнение на примере спектрального анализа на андроидедля Software Defined Radio: Java в 20 медленнее чем нативный "высокородный" C++, а тот в 3-5 раз медленнее чем C++\C с явно использованнимы функциями NEON


  1. sumrak00
    05.04.2024 11:48
    +2

    Большое спасибо за такую интересную статью!

    Я читала статью как начинающий программист, прошедший половину базового курса ООП на плюсах: как будто немного не хватает уточнений по ходу текста. Используются термины, но не поясняется, в чем между ними разница. Например, как я в итоге поняла из статьи, проблема sizeof() в том, что она считает разницу между указателем на начало и на конец. Но в абзаце это сформулировано не очень явно. Было бы здорово, наверное, добавить какое-то краткое обобщение абзаца в конце с простыми пояснениями (у нас есть вот это, но оно работает вот так — поэтому нам это не подходит в современной жизни).

    Дальше по тексту начинается упоминание и перечисление разных структур. Разницу между некоторыми я знаю на опыте, но в целом я восприняла это как отрывок разговора между опытными программистами на кухне — что-то краем уха удалось уловить и осознать, но в целом лавина из терминов. Могу предложить, что здесь помогла бы более резкая структуризация текста, чтобы разделить переход от обсуждения использования функций вычисления размера одних типов от других.

    Примеры и ссылки очень познавательные. В целом прочитать статью было одно удовольствие. Большое спасибо! :)


    1. isadora-6th
      05.04.2024 11:48
      +2

      Используйте std контейнеры (почти всегда std::vector, где надо - std::unordered_map/unordered_set), а массивы и ручное придумывание себе головняка оставьте свидетелям проблем в стд.

      Проблема sizeof в том, что им очень просто выстрелить в указатель, сделать не то, что задумано, вообще не получить толкового результата, а все от того, что кому-то лень узнать про существование массива в котором уже лежит сайз-капасити (вектор) и не городить головняк с узнаванием размера постфактум.

      Хотя честно говоря std::array, не то чтобы очень удобен в использовании. Постоянно упираешься в то, что он фикс длинны и надо городить вокруг него зоопарк с size/capacity чтобы красиво заполнять данными. Вот бы заехал SSO вариант вектора в стандарт, ух зажили бы...


      1. MiyuHogosha
        05.04.2024 11:48

        Нужно еще не забыть что size и capacity - разные вещи. Да и оригинал кода писался еще в страхе лишних выделений памяти -SSO тогда не было. Хотя нынче на некоторых реализациях строка в 32 символа в SSO не влезает.

        А еще некоторые люди боятся этих классов потому что "их нет в документации и они - сторонняя библиотека". Это результат методики обучения в ВУЗах где под страхом топора нельзя ими пользоваться.


    1. 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]) решает эту проблему. Добавление специализаций этой функции решает дополнительные проблемы подобного рода.

      Было бы здорово, наверное, добавить какое-то краткое обобщение абзаца в конце с простыми пояснениями (у нас есть вот это, но оно работает вот так — поэтому нам это не подходит в современной жизни).

      Для этого есть комментарии, если, конечно, найдётся кто-то, кто кратко объяснит суть дела.


    1. Mixxxxa Автор
      05.04.2024 11:48

      Спасибо за отзыв и подмеченные недостатки. Постраюсь учесть и избежать их в будущих публикациях :)

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


  1. NikolayNikT
    05.04.2024 11:48
    +1

    Всякая функция уместна на своем месте.


  1. datacompboy
    05.04.2024 11:48
    +1

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

    А если у подсунутого типа перегружен оператор []... Мы получим ошибку компиляции, или результат будет подобен std::string и классическому делению?


    1. 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 suitable size() 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() и так далее.


      1. datacompboy
        05.04.2024 11:48
        +1

        ¿А, то есть для стандартных массивов она не делает sizeof/sizeof[]?

        Понятно, спасибо!


        1. 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, и всё прекрасно компилируется, ни один компилятор даже ни пикнул.


          1. datacompboy
            05.04.2024 11:48
            +1

            Логично, можно же шаблонное раскрытие взять. Спасибо!


            1. MiyuHogosha
              05.04.2024 11:48

              И реализация (с поправками) работала в самом древнейшем компиляторе, я сто лет пользуюсь. VC6? gcc3? без проблем. Не знаю почему он знаковую версию только сейчас содрали.


  1. MiyuHogosha
    05.04.2024 11:48
    +1

    У меня первым результатом вылезно https://ru.stackoverflow.com/questions/578109/Как-узнать-размер-массива-переданного-в-функцию

    Повезло... и Гугл.

    Ну может, потому что я - мод на стеке.

    А вообще на русском часто вообще бред вылезает, ссылки на индусские ютубы или поддельные сайты (составленные из нелегальных автопереводов из стека и реддита). Я редко ищу на русском. Слова-враги переводчика тоже мешаются.

    Яндекс не нашел ничего приличного... вернее выдал Си-шарп почему-то.

    Бинг нашел MSDN


    1. Mixxxxa Автор
      05.04.2024 11:48
      +1

      В нашем блоге есть англоязычная версия данной статьи - для неё я сделал запрос и скриншот на английском. Результат +/- тот-же, только на сайт другой указывает.


      1. MiyuHogosha
        05.04.2024 11:48

        Зависит от того, что искать. я часто ищу по Qt и бусту. Ну и да, профиль обучения поисковика у вас может быть получше