Вот уже выпал снег, на дворе декабрь, а значит и Новый Год где-то рядом. В преддверии праздников мы решили показать вам наиболее интересные ошибки, которые мы смогли найти в коде популярных Open Source проектов. Наши авторы написали много познавательных статей, разобрали множество ошибок в коде, и теперь мы подведём итоги.

1092_Top_10_CPP_Bugs_2023_ru/image1.png
Перед дальнейшим чтением мы рекомендуем

Комфортно расположитесь перед камином в тёплом, мягком кресле после тяжёлого рабочего дня (если нет камина, можно обойтись мягким креслом). + 10 к уюту.

У вас в руках кружка горячего шоколада или чая. Вы пьёте свой напиток осторожно, чтобы не обжечься и дольше наслаждаться его вкусом. +15 к настроению.

Берёте планшет или ноутбук и можете приступить к прочтению. Вы отдыхаете: + 9 к интеллекту и +5 к навыкам, связанным с программированием.

Вступление

Вечер, снег блестит под светом уличных фонарей, приятно щиплет лицо от мороза, вокруг тишина, а с неба, кружась, падают снежинки. Скоро новый год и всё замерло в предвкушении чуда. Думаю, каждому знакомо это ощущение надвигающегося праздника. Я с теплотой вспоминаю, как меня в детстве зимой перед праздниками водили в цирк. Это было великолепно! Везде горят разноцветные огни, и тебя наполняет праздничное настроение! Весёлая музыка, смешные клоуны, запах попкорна. Загадочный усатый ведущий в чёрном цилиндре, с тростью старинной, задающий атмосферу таинственности. И вот он объявляет номер, и на арене происходит маленькое приключение, или случаются чудеса, и весь зал замирает на время выступления. Затем аплодисменты, овации – всё смешивается в едином порыве. Сейчас мы уже выросли, но можем сами создавать свои чудеса для наших близких, друзей и знакомых, которых хотим порадовать.

А посему сегодня вас ждёт не просто статья о том, как где-то в коде допустили ошибку, а целое волшебство кодинга. 10 масштабных и зрелищных разборов ошибок – всё только для вас! Итак, *надевает маску Шпрехшталмейстера (Ш:)*, посмотрим, что у нас сегодня в программе:

Ш: "Наше предновогоднее шоу готово искренне удивить и восхитить вас своей новой программой! Незабываемое зрелище, множество жанров: от утечек памяти до разыменовывания указателей без страховки! Смешные опечатки!

Потрясающие выступления профессиональных программистов на открытых проектах – это шоу мирового уровня!"

Начинаем Шоу!

Ш: "Шоу для программистов! Так, так, так... Раз уж на то пошло, наше волшебство начнём... пожалуй, с цифры 9, а закончим 0! Воистину магические цифры, и зачем нам больше?"

Девятый "номер". Сомнительный цикл

Ш: "Дамы и Господа! Леди и Джентльмены! Программисты всех мастей! Время для магии и волшебства! Этот код заставит вас усомниться в самой концепции существования эффективного код-ревью."

Статья: "Проверка компилятора GCC 13 с помощью PVS-Studio".

static bool
combine_reaching_defs (ext_cand *cand,
                       const_rtx set_pat,
                       ext_state *state)
{
  ....
  while (   REG_P (SET_SRC (*dest_sub_rtx))
         && (REGNO (SET_SRC (*dest_sub_rtx)) == REGNO (SET_DEST (set))))
  {
    ....
    if (....)
      break;

    if (....)
      break;
    
    ....
    break;
  }
  ....
}

Предупреждение анализатора PVS-Studio: V612 An unconditional 'break' within a loop. ree.cc 985

Анализатор PVS-Studio обнаружил, что этот цикл безусловно прерывается на первой итерации. При дальнейшем рассмотрении мы также обнаруживаем множество других break под условиями. Возможно, это способ избежать написания оператора goto. Однако выглядит он странно, учитывая его постоянное открытое использование в целом по проекту.

Восьмой "номер". Самая опасная функция в мире C и C++

Ш: "Уважаемая публика! Сам король сегодня посетил нас! Но вместе с ним к нам пришло нечто очень опасное, запретное в мире программирования... Но не бойтесь! с этим можно бороться. Надо только знать, как!"

Статья: "Microsoft PowerToys: Король GitHub среди C# проектов с C++ ошибками".

void SetNumLockToPreviousState(....)
{
  int key_count = 2;
  LPINPUT keyEventList = new INPUT[size_t(key_count)]();
  memset(keyEventList, 0, sizeof(keyEventList));
  ....
}

На этот код PVS-Studio выдаёт сразу три предупреждения:

  • V579 The memset function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the third argument. KeyboardEventHandlers.cpp 16

  • V568 It's odd that 'sizeof()' operator evaluates the size of a pointer to a class, but not the size of the 'keyEventList' class object. KeyboardEventHandlers.cpp 16

  • V1086 A call of the 'memset' function will lead to underflow of the buffer 'keyEventList'. KeyboardEventHandlers.cpp 16

На тему подобных ошибок у нас даже есть отдельная статья: "Самая опасная функция в мире С/С++".

В данном случае разработчики хотели занулить массив keyEventList. Обратим внимание на 3 параметр – количество байт, которые хотели заполнить нулями. Вот только sizeof(keyEventList) вычисляет не размер массива, а размер указателя. Он зависит от целевой платформы, но чаще всего это 4 или 8 байт. При этом размер структуры явно больше 4 или 8 байт:

typedef struct tagINPUT 
{
  DWORD   type;

  union
  {
    MOUSEINPUT      mi;
    KEYBDINPUT      ki;
    HARDWAREINPUT   hi;
  } DUMMYUNIONNAME;
} INPUT, *PINPUT, FAR* LPINPUT;

Седьмой "номер". Дважды добавленный символ

Ш: "Загадки, загадки, загадки... Сейчас мы с вами поучаствуем в битве! Посмотрите на код! Сможете ли вы за короткое время найти здесь самый маленький, но очень важный символ? Часто этот символ означает конец всего! Но иногда он является началом чего-то нового и прекрасного. Ну, или обращения напрямую к полям объекта..."

Статья: "PVS-Studio vs CodeLite: битва за идеальный код".

std::unordered_set<wxChar> delimiters =
  { ':', '@', '.', '!', ' ', '\t', '.', '\\', 
    '+', '*', '-', '<', '>', '[', ']', '(', 
    ')', '{', '}',  '=', '%', '#', '^', '&', 
    '\'', '"', '/', '|',  ',', '~', ';', '`' };

Предупреждение анализатора: V766 An item with the same key ''.'' has already been added. wxCodeCompletionBoxManager.cpp:19

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

Шестой "номер". Ошибочное переопределение

Ш: "Вы хотите выиграть? Или не проиграть? Или это иллюзия выбора? Этот номер порадует вас как ответами на эти вопросы, так и иллюзией переопределения виртуальной функции!"

Статья: "PVS-Studio vs CodeLite: битва за идеальный код".

В базовом классе функция выглядит так:

class WXDLLIMPEXP_CORE wxGenericProgressDialog : public wxDialog
{
public:
  ....
  virtual bool Update(int value,
                      const wxString& newmsg = wxEmptyString,
                      bool *skip = NULL);
  ....
};

А вот как в наследнике:

class clProgressDlg : public wxProgressDialog
{
public:
  ....
  bool Update(int value, const wxString& msg);
  ....
};

Предупреждение анализатора: V762 It is possible a virtual function was overridden incorrectly. See third argument of function 'Update' in derived class 'clProgressDlg' and base class 'wxGenericProgressDialog'. progress_dialog.h:47, progdlgg.h:44

Исходя из совпадения первых двух параметров функций, можно сделать вывод, что действительно хотели переопределить виртуальную функцию. Однако параметры по умолчанию также являются частью сигнатуры. Поэтому функция clProgressDlg::Update на самом деле не переопределяет, а скрывает виртуальную функцию wxGenericProgressDialog::Update.

Корректное объявление виртуальной функции должно быть такое:

class clProgressDlg : public wxProgressDialog
{
public:
  ....
  bool Update(int value, const wxString& msg, bool *skip);
  ....
};

Чтобы избежать таких ошибок, начиная с C++11, можно и даже нужно использовать спецификатор override:

class clProgressDlg : public wxProgressDialog
{
public:
  ....
  bool Update(int value, const wxString& msg, bool *skip) override;
  ....
};

Теперь компилятор выдаст ошибку, если виртуальная функция ничего не переопределяет из базового класса. Вот и ответ.

Пятый "номер". Мультивселенная безумия

Ш: "Короли, иллюзии, магия и загадки. И правда — вселенная сегодня безумна, но сейчас вы увидите что-то по-настоящему такое, что повергает в шок! Слабонервным просьба отвернуться!"

Статья: "Ква! Как писали код во времена Quake".

// PVS-Studio: void Mod_LoadTexinfo (lump_t *l)
// PVS-Studio: client/model.c

mtexinfo_t *out;
// ... 
for (j=0 ; j<8 ; j++)
{
  ut->vecs[0][j] = LittleFloat (in->vecs[0][j]);
}

V557 Array overrun is possible. The value of 'j' index could reach 7.

Для понимания добавлено объявление для mtexinfo_t *out:

// PVS-Studio: client/model.c

typedef struct
{
  float       vecs[2][4]; // PVS-Studio: see what is wrong here?
  float       mipadjust;
  texture_t   *texture;
  int         flags;
} mtexinfo_t;
// PVS-Studio: client/common.h

float (*LittleFloat) (float l);

У нас есть двумерный массив, который используют как одномерный. Мало кто увидит проблему. Программист может подумать, что в памяти он лежит последовательно как минимум C99, 6.2.5 Types p20 — и будет прав.

"An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. The element type shall be complete whenever the array type is specified."

Однако в языке С существует специальный тип — указатель на массив T. Во время обращения к массиву внутри цикла, j обязательно превысит значение 3 и оператор [] вернёт переменную типа float[4]. Как следствие произойдёт чтение элемента массива вне его пределов, что является классическим примером неопределённого поведения (UB).

Четвёртый "номер". Только истина

Ш: "Подходите ближе, дамы и господа! Вас ждёт удивительный номер! Представьте, что условия — это всего лишь условности, а истина может обмануть ваши ожидания..."

Статья: "Герои Кода и Магии: анализ игрового движка VCMI".

void deallocate_segment(....) 
{
  ....
  if (seg_index >= first_block) 
  {
    segment_element_allocator_traits::deallocate(....);
  }
  else 
  if (seg_index == 0) 
  {
    elements_to_deallocate = first_block > 0
                               ? this->segment_size(first_block) 
                               : this->segment_size(0);
    ....
  }
}

Предупреждение анализатора: V547 Expression 'first_block > 0' is always true. concurrent_vector.h 669

В данном коде, если первая проверка не выполняется, то seg_index < first_block, а если seg_index == 0, то это означает, что first_block > 0. Затем идёт ещё одна проверка first_block > 0, которую анализатор справедливо помечает как всегда истинную. Следовательно, выражение this->segment_size(0) никогда не выполнится.

Хорошо, что у нас есть технологии для статического анализа, выявляющие подобные ошибки! Всё это возможно благодаря использованию в анализаторе технологии символьного выполнения (symbolic execution). Магия, не правда ли? И наверняка так и воспринимают это обычные люди. А вот представьте:

Существует некая высокотехнологичная раса. При колонизации очередной планеты технологии безвозвратно утеряны во время войны за власть... Прошло несколько сотен лет, и технологичная раса перенеслась во времена мудрых и расчётливых королей, доблестных рыцарей и загадочных магических руин. А работоспособность технологий воплощается при помощи различных ритуалов и артефактов. И, не понимая, как это работает на самом деле, эти существа взывают к магии. Кстати, как раз об этом и повествует сюжет Героев Меча и Магии.

А если вам интересно узнать об упомянутой выше технологи символьного выполнения, предлагаю вашему вниманию данную статью "Технологии статического анализа кода PVS-Studio".

Третий "номер". Очепятка неопределённого поведения

Ш: "Ох уж эти шутники-программисты. Один раз не дай им выспаться, как они начинают играть кодом! Сейчас на нашей арене перед вами выступят забавные опечатки и их коварные последствия!"

Статья: "FreeCAD и C++ код с неопределённым поведением для медитации".

QGVPage* QGIView::getQGVPage(TechDraw::DrawView* dView)
{
  ViewProviderPage* vpp = getViewProviderPage(dView);
  if (!vpp) {
    return vpp->getQGVPage();
  }
  return nullptr;
}

Предупреждение анализатора: V522 [CWE-476, CERT-EXP34-C] Dereferencing of the null pointer 'vpp' might take place. QGIView.cpp 592

Автор кода опечатался при написании условия:

  • Если указатель ненулевой, то функция ничего не делает и возвращает nullptr.

  • Если указатель нулевой, то произойдёт его разыменование. Возникнет неопределённое поведение.

Что интересно, по соседству присутствует функция-близнец, и в ней условие правильное:

QGVPage* QGIView::getQGVPage(TechDraw::DrawView* dView)
{
  ViewProviderPage* vpp = getViewProviderPage(dView);
  if (!vpp) {
    return vpp->getQGVPage();
  }
  return nullptr;
}

QGSPage* QGIView::getQGSPage(TechDraw::DrawView* dView)
{
  ViewProviderPage* vpp = getViewProviderPage(dView);
  if (vpp) {
    return vpp->getQGSPage();
  }
  return nullptr;
}

Непонятно, как так получилось. С большой вероятностью вторая функция была написана с помощью copy-paste. Странно то, что во второй функции ошибку поправили, а в первой она осталась, и, возможно, ошибка присутствовала в обеих функциях. Затем кто-то заметил, что функция getQGSPage работает неправильно, и исправил её.

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

Второй "номер"! Доверяй, но проверяй

Ш: "И снова загадка! И снова код, покрытый тайнами и иллюзиями правильности своего выполнения! Неотвратимость судьбы или чья-то нехорошая шутка? Попробуйте же отгадать, что здесь не так?"

Статья: "Игоры! Как пишут код для SDL (+ интервью с создателем)".

Файл: SDL/src/stdlib/SDL_iconv.c (GitHub permalink)

char *SDL_iconv_string(const char *tocode, const char *fromcode,
                       const char *inbuf, size_t inbytesleft)
{
  SDL_iconv_t cd;
  ....

  cd = SDL_iconv_open(tocode, fromcode);
  if (cd == (SDL_iconv_t)-1) {
    /* See if we can recover here (fixes iconv on Solaris 11) */
    if (tocode == NULL || !*tocode) {
      tocode = "UTF-8";
    }
    if (fromcode == NULL || !*fromcode) {
      fromcode = "UTF-8";
    }
    cd = SDL_iconv_open(tocode, fromcode);
  }
  ....
}

V595 The 'tocode' pointer was utilized before it was verified against nullptr. Check lines: 37, 789, 792. SDL/src/stdlib/SDL_iconv.c:792:1

Для понимания проблемы надо окунуться в реализацию функции SDL_iconv_open:

SDL_iconv_t SDL_iconv_open(const char *tocode, const char *fromcode)
{
  return (SDL_iconv_t)((uintptr_t)iconv_open(tocode, fromcode));
}

На вход функции SDL_iconv_string вполне могут быть переданы нулевые указатели в аргументах tocode и fromcode. В случае этого они перед проверкой с ветерком полетят и в саму iconv_open.

Если мы посмотрим в man страницу данной функции проекта GNU, то не увидим никакого упоминания о том, что NULL толкать в неё нельзя. Но нас не проведёшь! Поэтому надо взглянуть в исходники библиотеки С:

iconv_t
iconv_open (const char *tocode, const char *fromcode)
{
  /* Normalize the name.  We remove all characters beside alpha-numeric,
     '_', '-', '/', '.', and ':'.  */
  size_t tocode_len = strlen (tocode) + 3;
  ....
}

Ну... обращения по указателю, конечно, не происходит, но вполне себе присутствует вызов strlen без проверки на NULL. Все мы знаем, что strlen делает в этом случае, и мало кому это по душе!

Однако автор не остановился на одной имплементации — он проверил также BSD и Musl. И все они ведут себя равным образом.

Первый "номер"! Много потоков, много проблем

Ш: "А теперь ловкач-процессор покажет вам, как жонглировать потоками! И в этом ему будет помогать малыш-компилятор! Посмотрите на его точные акробатические движения с потоками, сможет ли кто-то из зала так же?"

Статья: "Проверяем YTsaurus. Доступность, надёжность, open source".

TAsyncSignalsHandler *SIGNALS_HANDLER = nullptr;

void SetAsyncSignalHandler(int signum, TAutoPtr<TEventHandler> handler)
{
  static TAdaptiveLock lock;

  if (Y_UNLIKELY(SIGNALS_HANDLER == nullptr))       // N1
  {
    TGuard dnd(lock);

    if (SIGNALS_HANDLER == nullptr)                 // N2
    {
      // NEVERS GETS DESTROYED
      SIGNALS_HANDLER = new TAsyncSignalsHandler(); // N3
    }
  }

  SIGNALS_HANDLER->Install(signum, handler);        // N4
}

Предупреждение анализатора: V547 Expression 'SIGNALS_HANDLER == nullptr' is always true. async_signals_handler.cpp:200

Несмотря на то, что здесь сработало диагностическое правило V547, был найден интересный экземпляр паттерна Double-checked locking.

Что же может пойти не так? Для начала имеем небольшую вероятность того, что компилятор может закэшировать значение переменной SIGNALS_HANDLER в регистре и не будет заново читать из неё новое значение. По крайней мере, если будет использоваться нетипичный способ блокировки. Лучшим решением будет объявить переменную как volatile или использовать std::atomic.

Дальше больше. Давайте попробуем разобраться, что здесь не очень хорошо. Будем считать, что с этим кодом оперируют поток A и поток B.

Поток A первым добирается до точки N1. Указатель нулевой, так что поток управления заходит в первый if и захватывает объект блокировки.

Поток A доходит до точки N2. Указатель также нулевой, поэтому поток управления заходит во второй if.

Дальше в точке N3 может произойти небольшая магия. Инициализацию указателя можно представить примерно в следующем виде:

auto tmp = (TAsyncSignalsHandler *) malloc(sizeof(TAsyncSignalsHandler));
new (tmp) TAsyncSignalsHandler();
SIGNALS_HANDLER = tmp;

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

И тут в бой вступает поток B — как раз в тот момент, когда поток A ещё не успел вызвать placement new. Поток управления приходит в точку N1, видит инициализированный указатель и перемещается в точку N4.

Дальше в потоке B происходит вызов функции-члена Install на объекте, у которого ещё не стартовало время жизни. Неопределённое поведение...

Как поправить этот паттерн? Надо сделать так, чтобы чтение и запись в SIGNALS_HANDLER происходили атомарно:

std::atomic<TAsyncSignalsHandler *> SIGNALS_HANDLER { nullptr };

void SetAsyncSignalHandler(int signum, TAutoPtr<TEventHandler> handler)
{
  static TAdaptiveLock lock;

  auto tmp = SIGNALS_HANDLER.load(std::memory_order_acquire); // <=
    
  if (Y_UNLIKELY(tmp == nullptr))
  {
    TGuard dnd(lock);

    tmp = SIGNALS_HANDLER.load(std::memory_order_relaxed);    // <=
    if (tmp == nullptr)
    {
      // NEVERS GETS DESTROYED
      tmp = new TAsyncSignalsHandler();
      SIGNALS_HANDLER.store(tmp, std::memory_order_release);  // <=
    }
  }

  tmp->Install(signum, handler);
}

Нулевой "номер"!!! Капитан Блад и его сокровища!

Ш: "А теперь то, чего вы все так долго ждали! Безжалостные Пираты! Сундук с сокровищами, оставленный в назидание нам, а также тем, кто придёт после... Бережно хранимый в нашем набитом другими редкими сокровищами трюме. Сегодня мы откроем его специально для вас! Йо-хо-хо и бутылка рома!"

Статья: "Приключения капитана Блада: потонет ли Арабелла?".

Приведу проблемную функцию полностью:

void appDebuger::CopyDump(const char * src, DWORD from, const char * dst)
{
  FILE * file = fopen(src, "rb");
  if (file)
  {
    fseek(file, 0, SEEK_END);
    DWORD size = ftell(file);
    fseek(file, from, SEEK_SET);
    if(size > from)
    {
      FILE * to = fopen(dst, "a+b");
      if(to)
      {
        char buf[128];
        while (from < size)
        {          
          DWORD s = size - from;
          if (s > sizeof(buf)) s = sizeof(buf);
          memset(buf, ' ', sizeof(buf));
          fread(buf, s, 1, file);
          fwrite(buf, s, 1, file);            // <=
          from += s;
        }
        fclose(to);
      }
    }
    fclose(file);
  }
}

V1075 The function expects the file to be opened for writing, but it was opened for reading. Debugger.cpp 172

Мы не думали, что диагностика когда-нибудь сработает, так как ошибка довольно редкая, однако первое трофейное срабатывание произошло сразу после выхода в релизе PVS-Studio 7.15. Данное диагностическое правило ругается на запись в файлы, открытые для чтения, и наоборот.

Итак, функция должна прочитать данные из одного файла по пути src, начиная с позиции from, и записать их в другой файл по пути dst. За исходный файл отвечает переменная file, за результирующий файл — переменная to.

К сожалению, в коде происходит чтение из переменной file и запись в переменную file. Поправить код можно таким образом:

fwrite(buf, s, 1, to);

Конец представления!

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

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

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

Традиционно предлагаю попробовать наш анализатор PVS-Studio. Для Open Source проектов у нас предоставляется бесплатная лицензия.

Берегите себя и всего доброго! Приятных новогодних праздников!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Gorshkov. New Year's Eve show: Top 10 errors in C and C++ projects in 2023.

Комментарии (22)


  1. VladimirFarshatov
    19.12.2023 16:48

    Номер 0 - кмк, классическая опечатка, вылавливается первым же тестированием, нет?

    С асинхронным исполнением и гонками - да тоже по сути "классика", и в т.ч. как понимаю, одна из причин не любви Торвальдса к С++, ибо "нефиг" ;)

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

    В целом, пасибки. Интересный статический анализатор, надо будет погонять на своих старых кодах (найти бы их ещё..) ;)


    1. Nanamuru Автор
      19.12.2023 16:48

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


      1. VladimirFarshatov
        19.12.2023 16:48

        Ну, всё же классика sizeof кмк, не опечатки, а больше от непонимания..


        1. Nanamuru Автор
          19.12.2023 16:48

          От непонимания много ошибок, согласен) поэтому надо внимательно изучать матчасть)


    1. SvyatoslavMC
      19.12.2023 16:48

      Номер 0 - кмк, классическая опечатка, вылавливается первым же тестированием, нет?

      Вероятно, мы первые и последние, кто протестировал эти проекты на ошибки)


  1. hiewpoint
    19.12.2023 16:48

    В номере 9 использован абсолютно классический подход для множества выходов за пределы if блока через замену его на while или for и использование break, видел такое в коде много раз, так что ничего необычного.


    1. Sazonov
      19.12.2023 16:48

      Я такое через ‘[] () {…} ();’ и return где надо делаю. Имхо более очевидно чем через break. Плюс в таком же стиле можно сложную инициализацию констант делать.


      1. Sazonov
        19.12.2023 16:48

        Минусующие прокомментируют? Ассемблерный код получается аналогичный, читаемость - лучше, потому что нет непонятного цикла. Подобным образом, к примеру, устроен макрос QStringLiteral - это лямбда с моментальным вызовом, в котором есть инкапсулированная статическая переменная.


      1. KanuTaH
        19.12.2023 16:48

        Все правильно, я тоже так делаю. Но номер 9 - это код из GCC, а он написан главным образом на, так сказать, "C с шаблонами", и while с break можно использовать в C коде, а лямбды - только в C++.


        1. Sazonov
          19.12.2023 16:48

          Да, вы правы, я на автопилоте подумал что там с++


      1. ZirakZigil
        19.12.2023 16:48

        Моментальный вызов вообще весьма прикольный. Можно с его помощью делать затычки для тернарного оператора, когда один из операндов void, можно в pack expansion пихать для сложных операций.


      1. hiewpoint
        19.12.2023 16:48

        Лямбды появились только в С++11, а у меня, например, появилась возможность их использовать в рабочих проектах только со сменой работодателя 5 лет назад, до того полтора десятка лет вел кроссплатформенные разработки и часть использовавшихся компиляторов поддерживала только С++03, и боли больше было даже не из-за отсутствия лямбд, а из-за отсутcтвия variadic templates и constexpr.


        1. Sazonov
          19.12.2023 16:48

          Сочувствую. Но в большинстве случаев стопор в переходе на новый стандарт - хреновый менеджмент и олдскульные техлиды которым лень изучать что-то новое. С 03 на 20 плюсы можно практически бесшовно перейти.


  1. Engenigger
    19.12.2023 16:48

    В номере 5 нет никакого UB. Данные ведь лежат последовательно и, следовательно, обращение vecs[0][4] эквивалентно обращению [1][0].


    1. Andrey2008
      19.12.2023 16:48

      1. Engenigger
        19.12.2023 16:48

        Прошу прощения, но я не понял, что Вы имеете в виду.
        Возвращаясь к описанному случаю:
        1. Элементы массива расположены в памяти последовательно (по стандарту языка Си)
        2. Обращение к элементу массива любой размерности осуществляется через смещение относительно указателя на начало массива. Поэтому оба выражения vecs[0][4] и vecs[1][0], в конечном счете преобразуются в *(vecs + 4). По сути, программист просто выполнил работу компилятора.
        Напомню, мы говорим о чистом Си! Именно за такие вот трюки одни программисты его любят, а другие ненавидят )))


        1. Nanamuru Автор
          19.12.2023 16:48

          Делюсь с вами ярким примером UB для этого случая: https://godbolt.org/z/9nx83GE64

          Так же с санитайзером, который так же заметил, что может присутствовать UB: https://godbolt.org/z/v7EE1jr8f

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


        1. tntnkn
          19.12.2023 16:48

          Undefined Behavior Can Format Your Drive =)

          Но шутки в сторону -- если Си задуман как язык небезопасный, то зачем лишний раз полагаться на случай?


  1. foooof
    19.12.2023 16:48

    В 9 я бы обратил внимание на возвращаемое значение, которого в приведенном окончании функции нет.


    1. Nanamuru Автор
      19.12.2023 16:48

      тот самый код с GitHub: https://github.com/gcc-mirror/gcc/blob/master/gcc/ree.cc

      возврат значений есть, проблема в отредактированном нами коде, поправил)


  1. kolya7k
    19.12.2023 16:48

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

    Чего стоят только вложенные if, макросы, лучше бы показывали примеры ошибок в красивом идеальном коде. Где кроме найденной ошибки нет ни одной проблемы из разряда «так делать нельзя».


    1. Nanamuru Автор
      19.12.2023 16:48

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

      1. Чаще всего код пишут простые программисты, которым нужен результат. Коммерция или нет, чаще приоритет в процессе написания проекта отдается в эффективность выполнения кода и скорость его написания. Грубо говоря, если ты занимаешься коммерцией, для тебя главное чтобы твой продукт работал и приносил деньги. Так же, если ты пишешь нечто бесплатное, первое что ты делаешь это пишешь работающую программу, чтобы ей могли пользоваться. Только потом уже идет процесс оптимизации. Если ты будешь идеально его писать с самого начала то это займет намного больше времени и сил. Только после того как программа написана ты начинаешь оптимизировать программу, ты можешь её переписать хоть с нуля, но главное что твоя программа уже работает и используется.

      2. Дебажить большие проекты на самом деле очень трудно. Часто в проекте не остается никого из программистов писавших код. И вот ты приходишь в компанию, начинаешь копать какой ни будь файлик в котором более 50тыс строк кода и через пару часов ты уже сидишь и на автомате начинаешь пытаться что то найти. Чем дольше ты листаешь, тем труднее искать ошибку. Анализатор тебе сразу показывает возможные проблемные участки кода, и ты сразу можешь начать исправлять ситуацию. Опять же есть специальная база по ошибкам и ты можешь там глянуть приблизительно в чем может быть проблема и быстро проверить нет ли где проблем в тех или иных файлах.

      Что интересно, все ошибки найдены в крупных проектах. Как мне кажется, вовсе не джуны эти проекты разрабатывали) Суть: код пишут люди, и мы по массе причин не идеальны и допускаем ошибки. Самые частые ошибки - очень простые. Те же самые копи-паст и сравнения.

      можете ознакомится если интересно:

      Зло живёт в функциях сравнения

      Последствия использования технологии Copy-Paste

      И это только про простые ошибки)