В мире программирования существует огромное количество багов, и если бы каждый баг стал бабочкой, то программеру в раю уже давно оставлена пара полян для развития навыков энтомолога. Несмотря на все совершенства этого мира: компиляторы, pvs-studio и другие статические анализаторы, юниттесты и отделы QA, мы всегда находим способы преодолеть преграды кода и выпустить на волю парочку новых красивых и удобных видов. Есть у меня txt файлик, которому очень много лет, и куда я складываю интересные экземпляры. Все примеры и действия описанные в статье вымышленные, ни один стажер, джун или студент уволены не были. Hello, World! Where are your bugs?


First...

Начну, пожалуй, с очепятки в коде. Этот код использовался в движке Unity 2014 для управления поворотом объектов с помощью гизмо (такой элемент управления, обычно в форме трех окружностей, который позволяет крутить объект на сцене). В данном случае, функция setDeltaPitch применяется для изменения угла наклона объекта относительно его вертикальной оси (pitch). При углах близких к 0 (зависело от настроек редактора) просто переворачивала объект вверх ногами, что очень бесило дизайнеров уровней.

void	UObject::setDeltaPitch(const UMatrix &gizmo) {
        
		....

		if (_fpzero(amount, eps))
			return

		rotateAccum.setAnglesXYZ(axis);
        ....
}
А где ошибка?

После return не стоит точки с запятой, что при определенных условиях приводило к вызову функции setAnglesXYZ в управляемом объекте и переворачивало его на произвольный угол.

Next...

А тут повеселился компилятор, эта функция использовалась для вычисления хеш-суммы данных в контенте при проверке целостности файлов. При создании контента вычислялся хеш файлов и сохранялся вместе с файлами. Позже, при загрузке того же файла, плеер юнити снова вычислял хеш файла и сравнивал его с сохраненным хешем, чтобы убедиться, что файл не был изменен или поврежден. Шифрование использовалось на финальном этапе упаковки. По замыслу автора этого кода, ключ не должен был утечь за пределы этой функции, но что-то пошло не так. Так как это асимметричная система шифрования, то любой, кто владеет закрытым ключом может зашифровать и подписать двоичные файлы. При загрузке эти файлы будут выглядеть "подлинными". Наличие части утекших исходников Unity и этого бага в движке помогло SKIDROW в 2017 поломать Сибирь 3 и еще несколько больших игр на этом движке, которые использовали нативные средства шифрования контента. Там конечно был еще Denuvo, но его кедры научились обходить еще до этого.

void UContentPackage::createPackage(dsaFile *m, const void *v, int size) {
  unsigned char          secretDsaKey[3096], L;
  const unsigned char *p = v;
  int                    i, j, t;
  ....
  UContent::generateDsaKey(secretDsaKey, sizeof(salt));
  ....
  // тут какойто код был, работали с переменными
  // шифруем и подписываем файл
  ....
  memset                 (secretDsaKey, 0, sizeof(secretDsaKey));
}
А где ошибка?

Функция memset() не вызывалась из-за агрессивной оптимизации компилятора, действия с secretDsaKey не проводились после memset() поэтому компилятор её просто выкинул. Все содержимое ключа оставалось на стеке.

Next...

А тут могут быть проблемы при работе из двух и более потоков с любой из переменных a/b. Эта ошибка присутствовала в движке CryTek при синхронизации стейта автомобилей по сети, из-за чего наблюдались рывки и телепорты при передвижение на авто в мультиплеере FarCry 1. Чем больше игроков было на карте, тем больше была вероятность телепорта у последнего игрока, при 16 игроках на карте, последнего стабильно телепортило если он использовал авто.

struct X {
  int a : 2
  int b : 2
} x;

Thread 1:
void foo() { x.a = 1 }

Thread 2:
void boo() { x.b = 1 }
А где ошибка?

Нарушение атомарности операции записи. Поля a и b не являются атомарными, что означает, что они могут выполняться частично и прерываться другими операциями. В данном коде присутствует общий доступ к комплексной переменной AB, которая состоит из двух двубитовых частей. Но компилятор не может выполнить такие присвоение атомарно, захватывая сразу байт ab и битовыми операциями выставляя нужное значение. Это может привести к гонкам данных и неопределенному поведению в многопоточной среде

foo():                                # @foo()
        mov     AL, BYTE PTR [RIP + x] ; разорваное присвоение 1
        and     AL, -4 ; разорваное присвоение 2
        or      AL, 1 ; разорваное присвоение 3
        mov     BYTE PTR [RIP + x], AL ; закончили упражнение
        ret

boo():                                # @boo()
        mov     AL, BYTE PTR [RIP + x]
        and     AL, -13
        or      AL, 4
        mov     BYTE PTR [RIP + x], AL
        ret

Next...

Этот код содержит гонку даже при наличии "вроде бы живого" мьютекса. Ошибка была замечена в прошивке Nintendo Switch 4.5.1 и выше. Наткнулись мы на неё совершенно случайно, когда пытались ускорить создание атласов ui текстур на старте игры. Если пробовали загрузить в атлас больше 100 текстур, то ломали её. А если меньше то атлас собирался нормально. Такие мьютексы зомби на свиче до сих пор не пофикшены. А еще на свиче, можно было сделать не больше 256 мьютексов на приложение, вот такая вот веселая система.

const size_t maxThreads = 10;
 
void fill_texture_mt(int thread_id, std::mutex *pm) {
  std::lock_guard<std::mutex> lk(*pm);
  
  // Access data protected by the lock.
}
 
void prepare_texture() {
  std::thread threads[maxThreads];
  std::mutex m;
 
  for (size_t i = 0; i < maxThreads; ++i) {
    threads[i] = std::thread(fill_texture_mt, i, &m);
  }
}
А где ошибка?

Мы удаляем мютекс сразу по завершению функции prepare_texture(). Операционная система никак не реагирует на неактивный мютекс, потому что как объект ядра продолжает еще некоторое время жить и адрес является валидным и содержит корректные данные, но не предоставляет реальной блокировки тредов.

Next...

Функции могут быть определены так, чтобы принимать на месте вызова больше аргументов, чем указано в объявлении. Такие функции называются (variadic) вариативными. C++ предоставляет два механизма, с помощью которых можно определить вариативную функцию: шаблон с переменным числом параметров и использование многоточия в стиле C в качестве окончательного объявления параметра. Очень неприятное поведение было встречено в популярной библиотеке для работы со звуком FMOD Engine. Код привожу в том виде, какой он был в исходниках, похоже ребята хотели сэкономить на шаблонах. (https://onlinegdb.com/v4xxXf2zg)

int var_add(int first, int second, ...) {
  int r = first + second; 
  va_list va;
  va_start(va, second);
  while (int v = va_arg(va, int)) {
    r += v;
  }
  va_end(va);
  return r;
}
А где ошибка?

В этой вариативная функции в стиле C для сложения ряда целых чисел. аргументы будут считываться до тех пор, пока не будет найдено значение 0. Вызов этой функции без передачи значения 0 в качестве аргумента (после первых двух аргументов) приводит к неопределенному поведению. Более того, передача любого типа, кроме int, также приводит к неопределенному поведению.

Next...

А вот тут мы опять ничего не залочили, хотя на первый взгляд залочили. Этот код был обнаружен в движке Metro: Exodus в некоторых местах при работе с ресурсами, что приводило к странным крашам. Нашли благодаря баг репортам и одному умному французу.

static std::mutex m;
static int shared_resource = 0;
 
void increment_by_42() {
  std::unique_lock<std::mutex>(m);
  shared_resource += 42;
}
А где ошибка?

Это неоднозначность кода - ожидается, что анонимная локальная переменная типа std::unique_lock будет блокировать и разблокировать мьютекс m посредством RAII. Однако объявление синтаксически неоднозначно, поскольку его можно интерпретировать как объявление анонимного объекта и вызов его конструктора преобразования с одним аргументом. Компиляторы почему-то предпочитают второе, поэтому объект мьютекса никогда не блокируется.

Next...

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

// a.h
#ifndef A_HEADER_FILE
#define A_HEADER_FILE
  
namespace {
int v;
}
  
#endif // A_HEADER_FILE
А где ошибка?

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

namespace _abcd { int v; } в файле a.cpp
и 
namespace _bcda { int v; } в файле b.cpp

в итоге используя такую переменную нет уверенности в её текущем состоянии

Next...

А вот такой код рандомно сбоил в релизной сборке на разных компиляторах, использовался для подсчета контрольных сумм областей в PathEngine (https://www.pathengine.com/). Солюшен для плойки содержал определенный флаг, который маскировал проблему, а на боксе его не было. Ошибку обнаружили, когда попробовали собрать либу клангом на пк.

struct AreaCrc32 {
  unsigned char buffType = 0;
  int crc = 0;
};

AreaCrc32 s1 {};
AreaCrc32 s2 {};

void similarArea(const AreaCrc32 &s1, const AreaCrc32 &s2) {
  if (!std::memcmp(&s1, &s2, sizeof(AreaCrc32))) {
    // ...
  }
}
А где ошибка?

Структура будет выровнена до 4байт, и между buffType и сrc образуются мусорные байты, которые могут быть заполнены нулями, а могут мусором.

struct S {
  unsigned char buffType = 0;
  char[3] _garbage = {} // something, compiler don't care about content in release
  int сrc = 0;
};


memcmp() сравнивает память побитово захватывая и мусорные биты тоже, поэтому получается неизвестный результат этой операции для s1, s2. Настройки компилятора для плойки, явно указывали компилятору забивать мусорные байты нулями. Правила C++ для структур говорят, что нам нужно использовать в этом месте оператор()==, и memcmp не предназначен для структур равенства.

Next...

Вдогонку к предыдущему, как-то раз на ревью было вот такое. Вовремя заметили.

class C {
  int i;
  
public:
  virtual void f();
   
  // ...
};
  
void similar(C &c1, C &c2) {
  if (!std::memcmp(&c1, &c2, sizeof(C))) {
    // ...
  }
}
А где ошибка?

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

Next...

Знаете зачем программисту два монитора? Чтобы на одном он мог смотреть на код, который только что сломал, а на втором код, который еще компилится.

А где ошибка?

Она где-то точно есть, кода без ошибок не бывает.

Next...

Тут вроде совсем просто должно быть, но что-то часто на таких вещах претенденты спотыкаются на собесах.

struct S { S(const S *) noexcept; /* ... */ };
  
class T {
  int n;
  S *s1;
  
public:
  T(const T &rhs) : n(rhs.n),
                    s1(rhs.s1 ? new S(rhs.s1) : nullptr) {}
  ~T() { delete s1; }
  
  // ...
  
  T& operator=(const T &rhs) {
    n = rhs.n;
    delete s1;
    s1 = new S(rhs.s1);
    return *this;
  }
};
А где ошибка?

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

Next...

Даже в четырех строках кода можно словить неочевидный баг. Особенно, если это Unity Engine, который любит включать общие заголовки в разных DLL, а потом сравнивать их вот так super_secret() == super_secret(). Казалось бы, что может пойти не так, но секрет != секрету, и игра не грузится на Windows Phone.

// header a.h
constexpr inline const char* super_secret(void) {
  constexpr const char *STRING = "string";
  return STRING;
}

// a.dll
super_secret() == super_secret()

// b.dll
super_secret() != super_secret()
А где ошибка?

А ошибка в том, что общий хедер a.h использовался в разных либах, все работало пока движок собирался как единая длл, а собрав две разные DLL, мы получим два разных адреса для STRING. А вообще ошибка в том, что строки сравнивают поинтерами, а не через strcmp.

Next...

Можно вечно смотреть как течет вода, горит огонь, и как работает сортировка.

Next...

Я настолько не доверяю компилятору, что проверяю даже что this != null. Знаете где я нашел этот код? Вы наверное догадались :) Unity Engine 2014 года выпуска, а может он и до сих пор там.

class URendererOpengl : URendererBase {
  ...
  void commitDraw() {
    if (this == nullptr) {
      // WTF?
    }
  }
  ...
}

struct A {
    int x = 0;
    void bar() { std::cout << "bar" << std::endl; }
};

int  main() {
    A *a = nullptr;
    a->bar();
}
А где ошибка?

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

Next...

Какой из циклов выполнится быстрее? Ключи компилятора для релиза в студии /GS /W4 /Gm- /Ox /Ob2 /Zc:inline /fp:precise. Думаю на кланге будет тоже самое.

int steps = 32 * 1024 * 1024;
int* a = new int[2];

1. for (int i = 0; i < steps; i++) { a[0]++; a[0]++; }

2. for (int i = 0; i < steps; i++) { a[0]++; a[1]++; }
Так кто же быстрее?

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

Next...

У этой функции может быть UB с переполнением буфера. Часто это происходит для clang с оптимизацией -O3, и не происходит с -O0. На кланге 12.10 и выше уже исправлено для всех режимов оптимизации. Код не мой, всплыл на каком-то из собесов, когда просто беседовали по душам.

char destBuffer[16];
void serialize(bool x) {
	const char* whichString = x ? "true" : "false";
	const size_t len = strlen(whichString);
	memcpy(destBuffer, whichString, len);
}

int main() {
	bool x;
	serialize(x);
}
А где ошибка?

Компилятор перехитрил сам себя. Итак, что происходит? Агрессивная оптимизация преобразует это в len = 5 - x, bool это не 1/0, он зависит от компилятора. Clang определяет это 0 — ложь, другое — правда. X не инициализирован, поэтому в некоторых случаях у нас len = 5–7? И вылетает в memcpy по переполнению буфера.

Next...

// Порадуйте мир сами новыми видами баgoчек :)
int main() {
  printf("Hello, World! Where are your bugs?");
}

Надеюсь вам понравилась эта небольшая коллекция. А чтобы знать и понимать разные виды этих насекомых, можно иногда поглядывать в правила хорошего кода (https://wiki.sei.cmu.edu/confluence/display/cplusplus)

Зачем программисту два монитора?
Зачем программисту два монитора?

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


  1. Cheater
    30.09.2023 02:20
    +3

    Зачётно, далеко не всё смог разгадать.

    его можно интерпретировать как объявление анонимного объекта и вызов его конструктора преобразования с одним аргументом. Компиляторы почему-то предпочитают второе

    По стандарту так. https://en.wikipedia.org/wiki/Most_vexing_parse


  1. mayorovp
    30.09.2023 02:20

    Сравнивать плюсовые структуры и классы через memcmp плохая затея, ведь может прилететь в c2 отнаследованный класс, с другой vtbl.

    Не факт что это ошибка, обычно считается что несовпадение типов ведёт к неравенству объектов.


    Тут скорее проблема снова в выравнивании и прочем.


  1. Nick_Shl
    30.09.2023 02:20

    Знаете зачем программисту два монитора?

    Незачем. Окно IDE на два монитора ре развернешь и больше кода не увидишь...

    Хочу 32 дюйма 4к монитор - вот там ух! Но не дают. Говорят "Мы тебе дадим, а завтра у нас на пороге IT отдела будет целая очередь стоять из желающих требующих себе такие же!".


    1. kekekeks
      30.09.2023 02:20
      +9

      Открепите документ в отдельное окно, все IDE это позволяют.


    1. dalerank Автор
      30.09.2023 02:20
      +2

      А мне не понравилось, попробовал эту бандуру 34' Dell в повседневной работе. Начал быстро уставать, вечером побаливала голова и шея. Вернулся к двум монитора по 24дюйма после двух недель мучений


      1. McKinseyBA
        30.09.2023 02:20

        32" Dell (еще и кривой), 7 месяцев, полет нормальный. Вот эти 2 дюйма и играют разницу! :-)

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


    1. geher
      30.09.2023 02:20
      +1

      На одном мониторе одна IDE, на другом - вторая (разные).

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


    1. Nurked
      30.09.2023 02:20
      +3

      Для запуска того, что отлаживаешь. Особенно если пишешь фронт.


      1. Nick_Shl
        30.09.2023 02:20

        Я пока не научился запускать софт написанный под микроконтроллер на мониторе ????

        А вообще вполне валидный комментарий. Но запуск отлаживаемой программы на отдельном мониторе это не совсем то, что я имел ввиду. Что бы было эквивалентно, в ваш пример нужно добавить третий монитор.


        1. AlexanderS
          30.09.2023 02:20

          На одном IDE, на втором - отладка, на третьем - доки и всё прочее. И да - я хардварщик при этом)


  1. Sazonov
    30.09.2023 02:20
    +5

    Получается, что почти все ошибки из-за программирования на си++ в стиле си.


  1. 0serg
    30.09.2023 02:20

    Отличная подборка, спасибо!


  1. eao197
    30.09.2023 02:20
    +3

    Еще касательно вот этого кода:

      T& operator=(const T &rhs) {
        n = rhs.n;
        delete s1;
        s1 = new S(rhs.s1);
        return *this;
      }
    

    Если исключения в проекте не отключены, то потенциально new может бросить std::bad_alloc. В этом случае в this->s1 остается старое и, возможно, ненулевое значение. Которое затем будет передано в delete в деструкторе. И получим double free.


    1. dalerank Автор
      30.09.2023 02:20
      +2

      Специфика области такая, что про исключения я забыл лет 10 назад. 4% перфа слишком дорогая цена, когда мы тут за 15мс на фрейм бьемся насмерть ;)


  1. old_bear
    30.09.2023 02:20

    В данном случае, функция setDeltapPitch применяется для

    А это бонусная очепятка?


  1. vk6677
    30.09.2023 02:20
    +3

    Вот за это я люблю C++


  1. bel1k0v
    30.09.2023 02:20
    +8

    Вы случайно создаете дюжину копий объекта «вы» и всем им простреливаете
    ногу. Срочная медицинская помощь оказывается невозможной, так как вы не
    можете разобраться, где настоящие копии, а где — те, что только
    указывают на них и говорят: «А вот он я!»


  1. acordell
    30.09.2023 02:20

    Шикарная статья, спасибо! Все-таки С++, это С++.

    С выкидыванием memset оптимизатором только не очень понятно. По идее же это вызов функции, которой передается кусок данных... Мало ли что она там делает? Получается, что оптимизатор знает что такое memset?


    1. mayorovp
      30.09.2023 02:20
      +3

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


      1. dalerank Автор
        30.09.2023 02:20
        +2

        там оптимизации идут уже на уровне IR компилятора, с выкидыванием мертвых веток кода, почитать можно например тут (https://en.wikipedia.org/wiki/Dead-code_elimination). С мемсетом была похожая ошибка в md5, с точки зрения компилятора, это не влияет на производительность программы. Факт того, что личные данные останутся в памяти, не повлияет на работу программы.

        void MD5::finalize () {
          ...
          uint1 buffer[64];
          ...
          // Zeroize sensitive information
          memset (buffer, 0, sizeof(*buffer));
        }


    1. dalerank Автор
      30.09.2023 02:20

      но есть и обратная сторона, например на плойке все memcpy помечены как секьюрные и плоечный компилятор об этом знает, он их никогда не выкидывает и не оптимизирует, кроме того в самом memcpy сделаны проверки на пересечение с адресным пространством защищенных областей. В итоге сдкшный memcpy почти в два раза проигрывает наивной реализации копированию _m128 через регистры. Изза этой особенности все (с которыми я работал) игровые движки тащут свою имплементацию memcpy для ps5


    1. voldemar_d
      30.09.2023 02:20
      +1

      Про memset много материалов есть - здесь, например:

      https://habr.com/ru/companies/pvs-studio/articles/756872/

      https://pvs-studio.ru/ru/blog/posts/cpp/0360/


    1. lrrr11
      30.09.2023 02:20

      да. Чтобы такого не было, можно например пометить буфер как volatile (тогда компилятор не станет оптимизировать никакие обращения к нему. Этот режим был придуман специально для буферов памяти всяких устройств, когда запись данных в адрес означает их появление на экране, на диске и т.п.), либо воспользоваться специальной версией memset. https://en.cppreference.com/w/c/string/byte/memset


  1. aarreezz
    30.09.2023 02:20

    const char* whichString = x ? "true" : "false";
    const size_t len = strlen(whichString);

    Напоминает старинный индусский метод определения true/false:
    if( strlen(whichString) == 4 ) ... true

    "Знаете зачем программисту два монитора?" - а ведь хорошо сказано :)


  1. boldape
    30.09.2023 02:20
    +3

    Компиляторы почему-то предпочитают второе, поэтому объект мьютекса никогда не блокируется.

    Ну это не так. Тот код эквивалентен

    { auto _ = std::unique_lock(m);}

    Мьютекс блокируется и сразу разблокируется и это не то же самое, что он вообще не блокируется.

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

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

     if (this == null) {...}


    1. dalerank Автор
      30.09.2023 02:20
      -1

      1. Нет, не удалит если внутри логика, этот код ничем не отличается от любого другого и корректен

      2. Попробуйте скомпилить и запустить пример. (https://onlinegdb.com/YetgglcV-)


      1. mayorovp
        30.09.2023 02:20
        -1

        Нет, это вы попробуйте: https://godbolt.org/z/hr7j83a1r


        1. dalerank Автор
          30.09.2023 02:20
          -1

          1. mayorovp
            30.09.2023 02:20

            Что "всё же нет"? У вас во всех трёх примерах в baz нет вызова bar, а main так и вовсе пуст: DCE удалил единственный путь исполнения.


      1. boldape
        30.09.2023 02:20
        +1

        1. Рекомендую ознакомиться с историей вопроса и пройти по всем ссылкам https://stackoverflow.com/questions/48067323/c-why-cant-this-be-a-nullptr в дополнение к стэку так же рекомендую заглянуть в современный стандарт https://en.cppreference.com/w/c/language/operator_member_access смотрите секцию dereferencing, а ещё вы можете найти здесь же на Хабре статьи про УБ + ещё здесь есть серия статей от компилятора строителя, можете написать в личку этим людям, а ещё можете написать вопрос в комитет для разъяснения, вот прям тут вызывайте российского представителя к сожалению не могу правильно написать имя юзера из мобильного Фокса, antoshka из Яндекса

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


        1. dalerank Автор
          30.09.2023 02:20
          -1

          Я знаком с вопросом более чем, и с Антоном мы уже обсуждали этот вопрос на с++russia 2019. Программа может быть написана так, что желание или нежелание избежать UB на это не влияет


          1. boldape
            30.09.2023 02:20
            +1

            Программа может быть написана так, что желание или нежелание избежать UB на это не влияет

            Извините, я не понимаю, что вы хотите сказать.

            Я знаком с вопросом более чем

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

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

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


            1. dalerank Автор
              30.09.2023 02:20

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

              A *a = nullptr;
              a->bar();

              Студия ветку не выкинула даже с оптимизациями, кланг выкинул, но с O0 оставил


              1. mayorovp
                30.09.2023 02:20

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


  1. funny_falcon
    30.09.2023 02:20
    +1

    Вот только визуализация не от heapsort, а от insertion sort.


    1. dalerank Автор
      30.09.2023 02:20

      Ваша правда, спасибо, исправил


  1. av-86
    30.09.2023 02:20
    +1

    Вот здесь, судя по всему, знающие люди собрались. Может кто-нибудь пояснить, а можно ли компилятор заставить ругаться каждый раз, когда он видит UB?


    1. dalerank Автор
      30.09.2023 02:20
      +3

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

      1. -Wall/extra - больше предупреждений от встроеных анализаторов, включая некоторые для UB. Это не сделает UB ошибкой, но поможет выявить потенциально проблемные места.

      2. -Werror - сделает все предупреждения ошибками, можено включить в паре с -Wall для более строгой проверки.

      3. -fsanitize=undefined - включает AddressSanitizer и UndefinedBehaviorSanitizer в рантайме

      4. -pedantic-errors - злой флаг, включает параноик режим у кланга и все несоответствия стандарту выводит как ошибку.

      5. пройтись pvs-studio по проекту, отличный анализатор, ловит много чего, что пропускает компилятор


    1. boldape
      30.09.2023 02:20
      +2

      В дополнение к тому что выше, частично можно, пишите код в функциях/лямбдах с пометкой constexpr.

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


  1. Andrey2008
    30.09.2023 02:20
    +1

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

    Пара мыслей про описанные баги.

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

    И наоборот, ошибка с исчезновением memset кажется чем-то экзотическим и редким. Но это очень распространённая потенциальная уязвимость. Мы их обнаружили 100500 штук и продолжает обнаруживать. Хотя, казалось бы на эту тему тоже уже много написано: CWE-14, Zero and forget -- caveats of zeroing memory in C, Безопасная очистка приватных данных.


    1. dalerank Автор
      30.09.2023 02:20
      +1

      Видимо моя будет четвертой. У вас вышла мега отличная статья с антисоветами, благодарю. Тогда Всем отделом обсуждали несколько дней, но к счастью ничего словить не смогли. PVS стоит на страже. Слежу за Вашим блогом еще с По колено в Си++ г... коде.


  1. allcreater
    30.09.2023 02:20
    +1

    Компилятор перехитрил сам себя. Итак, что происходит?

    Тут-то UB происходит: нельзя читать из неинициализированной переменной. Поскольку недопущение таких вещей в зоне ответственности программиста - компилятор справедливо считает что на входе будет bool с конвенционным значением, и оптимизирует как умеет.

    Кстати, кто-нибудь знает, может ли сгенерироваться подобный код функции, если компилятор увидит вместо неинициализированной переменной какое-нибудь memcpy(&x, src, sizeof(x))?


    1. dalerank Автор
      30.09.2023 02:20
      -1

      Конечно нельзя, я к тому что bool это не 0, 1 только, что и привело к такому.