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

Отладка багов, пожалуй, самый недооцененный инженерный навык. Ему толком не учат, его очень редко просят «осветить» на собеседовании, и еще ни одного синьора‑отладчика я в жизни не встречал. Все обучение сводится к тому, что ты просто сидишь три часа и смотришь краш‑дамп, пытаясь понять, почему игра упала. Рецептов поимки и починки хитиновых товарищей тыща и один, и у каждого обязательно будет свой, поэтому смысла рассказывать о них я не вижу, но попробую рассказать о том какие собственно товарищи бывают.


Обычная очепятка

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

if (update_x) pos.x = new_pos.x;
if (update_y) pos.x = new_pos.y; // Дрогнула рука
if (update_z) pos.z = new_pos.z;

Самое мерзкое в опечатках это наш мозг, который работает как встроенный автокорректор. Ты смотришь на этот кусок кода десять раз, и глаз просто «пролистывает» это ошибку, потому что ты знаешь, что там должно быть написано, но оно там не написано. В какой‑то момент мне надоело вылавливать такие вещи, и мы включили в проекте clang‑tidy с bugprone-* и -Wshadow на clang сборке, которые спасают от большинства багов с перекрытием имен переменных, и дрогнувшей руки.

class Foo {
    int value;
public:
    Foo(int value) { // параметр затеняет поле class
        value = value; // ничего не делает, присваивает параметр самому себе
    }
};

Логический баг

А еще код делает то, что ты написал... буквально делает, но можно написать глупость, например ошибиться на единицу (off‑by‑one), когда обновляешь кольцевой буфер событий или пытаешься удалить элемент из массива:

memmove(events + i, events + i + 1, (num_events - i) * sizeof(*events));
--num_events;

Но с такими багами хотя бы приятно работать и если есть стабильный сценарий воспроизведения, то они будут детерминированы на 100%. Ты просто садишься и шагаешь отладчиком, но у разработчиков есть пагубная привычка плодить логические баги своими руками, оптимизируя код раньше времени. Начинается всё как обычно с благих намерений: «О, давай я напишу отдельный код для фастпас или редкого случая, когда мы удаляем самый последний элемент». В итоге у тебя появляется ветка if/else, которая исполняется раз в неделю при особом положении луны и она естественно толком не тестируется. И конечно, именно она взрывается у игрока. Чем линейнее код и чем меньше в нем редких изолированных веток, тем меньше мест, где логика может тихо хранить хитинового товарища.

Запись за границу массива

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

#define MAX_PARTICLES 1024
Particle particles[MAX_PARTICLES];
uint32_t num_particles;

void spawn_particle(Particle p) {
    particles[num_particles++] = p;
}

Кто‑то обязательно заспавнит 1025-ю частицу и такой код пойдет затирать соседнюю память, и начнется ад странных крашей. Вы можете сказать, что надо срочно переписывать всё на динамические массивы? Но фиксированные пулы это предсказуемость и защита от фрагментации, в геймдеве они нужны, так что фиг вам, а не динамика.

Утечки ресурсов

Дружок предыдущего товарища, тоже сколько уж лет, а воз и ныне там, точнее баги все те же. В игровом движке утекать может что угодно: текстуры, меши, дескрипторы файлов, захваченные мьютексы, на nintendo switch в первых ревизиях рекурсивный мьютекс не удалялся, если заходил сам в себя больше пяти раз, а их всего можно было создать 1024 штуки на процесс.

А если писать код на голых malloc и free, вылавливать утечки будет еще одним повседевным занятием. А вот найти, какой именно из пятисот вызовов выделения памяти не получил свой free это задача со звездочкой. Если думаете, что умный GC или умные указатели решают эту проблему, то нет... не решают. Они просто меняют природу утечек и вместо утечки памяти получаем утечку ссылок и какой‑нибудь забытый менеджер сцены продолжает держать ссылку на невидимый объект, а тот по цепочке держит текстуры и звуки.

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

Повреждение памяти

Если бы у дебаггинга был пьедестал, то повреждение памяти точно надо ставить на верхнюю ступеньку: use‑after‑free, вылет за пределы массива, запись по «дикому» указателю, повреждение заголовка блока аллокации, повторное удаление и повторное создание в размеченой области, что я еще забыл?

Ужас этих багов что причина и симптом разорваны во времени и пространстве. Функция A где‑то в модуле физики слегка затерла край соседнего массива, но физика продолжает работать, но где-то неправильно оторазился виджет UI при попытке показать имя игрока, и ладно если кракозябры покажет, а может и вылетать с Access Violation. И вот ты сидишь над краш‑дампом UI и не понимаешь, при чем тут вообще интерфейс.

Единственное, что спасает жизнь в таких ситуациях это AddressSanitizer (ASan) и специальные границы блоков памяти (канарейки). ASan обкладывает все аллокации защитными «красными зонами» и падает в момент невалидной записи, не давая испортить соседние структуры. А запись паттернов вроде 0xDEADBEEF в освобожденные блоки сразу дает понять в дебаггере, что ты пытаешься прочитать «труп» объекта.

Состояние гонки

Современный движок параллелит всё, что может. Физика на одном ядре, подготовка кадра на другом, стриминг ресурсов и AI на третьем, но когда два потока одновременно пытаются работать с одними данными без синхронизации, появляется он — Race Condition.

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

Пятничный фикс

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

// Пятница, 17:55. Фиксим редкий мигающий иконку здоровья
void UIHealthBar::Update(float DeltaTime)
{
    // "Заодно почистим каст, а то валидатор ругался..."
    PlayerCharacter* Player = Cast<PlayerCharacter>(GetOwningPawn());
    
    // Раньше тут была проверка if (Player), но разработчик уверен, 
    // что UI HealthBar существует ТОЛЬКО когда игрок жив и валиден.
    HealthPercent = Player->GetHealth() / Player->GetMaxHealth();
}

Код отправляется в репозиторий, билдится и уходит тестировщикам. Но в субботу утром выясняется, что когда игрок погибает, респавнится или загружает уровень через меню, UI отрисовывается на один кадр раньше, чем создается объект персонажа и указатель Player оказывается нулевым. Пятничный баг процветает не один десяток лет, пролазит через ревью и тесты и дожидается своего часа.

Гейзенбаги

Термин родился из квантовой физики и принципа неопределенности Гейзенберга, когда сам факт наблюдения за системой меняет ее состояние. В мире C++ это баги, которые живут только когда на них никто не смотрит. Прилетает от QA таска на краш при заходе в пещеру, запускаешь проект под отладчиком, доходишь до пещеры... и ничего не происходит. Все работает... делаешь это десять, двадцать раз... выключаешь отладчик, запускаешь релизный билд и ловишь падение. Сводится это обычно к неиниченной памяти и оптимизациям компилятора:

struct AttackParams
{
    float Damage;
    bool IsCritical; // Забыли инициализировать в конструкторе
};

void ApplyDamage()
{
    AttackParams Params;
    Params.Damage = 100.0f;
    // Params.IsCritical содержит случайный мусор из стека
    
    if (Params.IsCritical) {
        // В Debug-сборке здесь всегда false из-за зануления стека.
        // В Release-сборке здесь может оказаться true, и крит сработает неожиданно.
    }
}

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

void MeshLoader::OnAsyncLoadComplete(Mesh* LoadedMesh)
{
    log("Mesh loaded: %s\n", LoadedMesh->GetName()); // Этот лог не удалять!!!
    RenderQueue::Enqueue(LoadedMesh);
}

Подключение отладчика или включение специальных дебаг‑флагов меняет размер заголовочных файлов и структур данных (например, добавляются дебажные итераторы в std::vector или дополнительные поля валидации в аллокаторе), в результате сдвигаются адреса в памяти, и повреждение памяти, которое раньше затирало важные данные, начинает затирать безвредную «подушку безопасности» (guard bytes), маскируя проблему.

Фичебаг («It's not a bug, it's a feature»)

А еще есть ситуации, когда сбой в математике или логике создает настолько гениальный геймплейный опыт, что разработчики решают ничего не чинить, а просто переименовывают баг в «механику».

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

Агрессивный Ганди в Civilization. Из‑за переполнения 8-битного целого (unsigned integer) агрессия Ганди, снижаясь ниже нуля при принятии демократии, сбрасывалась в 255. Миролюбивый лидер превращался в ядерного психопата. Это на самом деле байка, и ничего такого не было, но миф этот стал настолько известным, что разработчики пятой части внесли его логику игры и сделав миф реальностью.

Распрыжка (Bunny Hopping) в Quake. Ошибка в векторе сложения скоростей при прыжке и повороте камеры позволяла разгоняться до скоростей истребителя, что позволяло вытворять очень веселые трюки на уровнях. В принципе если баг делает игру веселее, то его не фиксят, а полируют и отдают маркетологам, на этом построена целая серия игр Saints Row, где QA получали премии за экплуатацию разных багов, которые потом делали элементами механик самой игры.

Эффект Бабочки (The Butterfly Effect / Floating‑Point Math)

Баги, возникающие из‑за ограниченной точности чисел с плавающей запятой, когда игрок уходит далеко от центра координат. Чем дальше игрок от центра, тем меньше точности остается на дробную часть и на удалении порядка 100км, точность шага составляет несколько миллиметров, в итоге моделька начинает непрерывно «вибрировать» и эпилептически дергаться при ходьбе, пока её не выплюнет за пределы мира.

Или приводит к артефактам генерации, как в Minecraft (Far Lands), где на расстоянии в 12.5 миллионов блоков от спавна погрешность float при генерации шума Перлина становилась настолько огромной, что ландшафт превращался в гигантские дырявые сырные стены.

Спагетти‑Зависимости

Забавные баги, когда игра падает только при условии, что вы открыли дверь, удерживая в руках определенный предмет, и обязательно под углом в 45 градусов. В TF2 есть знаменитый городской миф про текстуру кокоса внутри файлов игры, без которой движок Valve Source просто отказывается запускаться. В файлах игры реально лежит текстура coconut.vtf, это VTF‑файл с реалистичным изображением кокоса, и его происхождение тянется к апдейту Love&War 2014 года, и судя по всему, остался как неиспользуемый ассет с тех времён. Легенда про то, что удаление файла ломает запуск игры, разрослась в основном благодаря шуточному комментарию под оригинальным постом в духе «без понятия, кто это сюда положил, но когда я удалил файл, игра перестала запускаться», который многие приняли за настоящий комментарий разработчика из исходников.

Или кейс из Lineage 2, когда игроки не могли зайти на локацию, потому что в его инвентаре лежал квестовый предмет из 2009 года, у которого идентификатор совпадал с новым типажом анимации для дракона.

Инверсия приоритетов

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

Баги плавающего шага времени (Variable Delta Time)

Плата за стремление к разблокированному фреймрейту. Вы связываете физику или перемещение с тем dt, который прошел между кадрами.

// Если кадр просел с 60 FPS (16мс) до 2 FPS (500мс) из-за загрузки
position += velocity * dt; // dt стал гигантским

И за один длинный кадр (например, когда игра «лагнула») персонаж пролетает насквозь через трехметровую стену, потому что физический коллайдер просто перескочил ее в один шаг.

Что я забыл?

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

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


  1. Jijiki
    03.09.2026 23:33

    А если писать код на голых malloc и free, вылавливать утечки будет еще одним повседенным занятием. А вот найти, какой именно из пятисот вызовов выделения памяти не получил свой free это задача со звездочкой. Если думаете, что умный GC или умные указатели решают эту проблему, то нет... не решают. Они просто меняют природу утечек и вместо утечки памяти получаем утечку ссылок и какой‑нибудь забытый менеджер сцены продолжает держать ссылку на невидимый объект, а тот по цепочке держит текстуры и звуки.

    как мне кажется на С++ зависит от архитектуры... (+ если модули, если удобно настроено иде отловить можно я всё еще думаю), просто, чтобы легаси 10к-100к loc покидать по модулям и сохранить архитектуру времени уйдёт конечно, это понятно конечно....


  1. Chemist_modeler
    03.09.2026 23:33

    В идеальном мире правильного и красивого кода - разработка этого самого кода стоила ровно ноль центов за тыщу лет работы, а дедлайн был через сто тыщ (световых) лет... соответственно, было комфортно работать со скоростью "одна отлаженная строка в час". И в те стародавние времена, когда этот мир, по некоторым слухам, существовал, бывали в самом деле программы без единого бага. Слухи упорно сватают на эту роль LaTeX, например. Так это или нет - не знаю.

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

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

    Плата за производительность - вещь универсальная, у нее есть лишь два измерения: цена и качество. Если при этом цена фиксирована, то что страдает - увы, вопрос риторический. С тем и живём


  1. nickolaym
    03.09.2026 23:33

    Многопоточные гонки, кстати, отчасти диагностируются thread sanitizer'ом.

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


    1. Melpomenna
      03.09.2026 23:33

      Не везде он доступен, под msvc его так до сих пор нет, а если пробовать компилировать другим компиляторов, то можно получить ещё 1 ряд весёлых багов


  1. nickolaym
    03.09.2026 23:33

    Чем дальше от начала координат, тем... дело даже не в ошибках конечной разрядности плавающей арифметики, а в ошибках вообще.

    Преобразования систем координат делаются на матрицах, в одну сторону - на прямых, в обратную - на обратных. Если матрица плохо обусловлена, оператор преобразования становится люто чувствителен к малым изменениям (в том числе, к погрешностям любой природы - хоть из младших разрядов, хоть из физики).

    И хотя с позиций чистой математики любое аффинное преобразование можно представить как матричное умножение в N+1-мерном пространстве (вектор-точка аугментирован единичкой, вектор-длина - ноликом), или же, что то же самое, как последовательность из вращения-масштабирования - матричного умножения в N-мерном пространстве - и затем параллельного переноса,

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

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

    Поэтому интерполировать нужно в какой-то локальной системе координат.

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

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

    И это не только геймдева касается, но и моделирования движения в реальном географическом пространстве.

    Хорошо промышленному роботу: прикручен к станине или, максимум, катается по цеху. А вот беспилотный автомобиль или летательный аппарат отхватывает все эти нюансы вычислительной математики полной ложкой!


    1. Chemist_modeler
      03.09.2026 23:33

      В беспилотрых системах, насколько я могу судить по общедоступным источникам (другой инфы не имею), спасает смена, время от времени, локальной системы координат с привязкой к карте. Понятно, что на экране сие будет выглядеть как, опять же, хитиновый товарищ. Да и хранить всего надо будет побольше, и с кодом придется помучиться. Но - сие вопрос ситуативный. Если сильно надо - решение есть. Если не сильно - то "и так сойдет". Фичебаг превращается в багофичу..


    1. lgorSL
      03.09.2026 23:33

      Для матрицы вращения 3х3 экспоненту можно вычислить в замкнутой формуле (и для кватернионов и моторов аналогично). Из минусов - там вылазят вычисления sin, cos и atan2, но для малых углов можно разложить формулы в ряд Тейлора и получить и точность и скорость вычислений. Вдобавок у чисел с плавающей запятой есть замечательное свойство - они умеют хранить очень маленькие или очень большие числа типа 1е-50 или 1е50, и точность не страдает. Я в остальном согласен, но именно интерполяция не самая проблемная часть.


  1. ImagineTables
    03.09.2026 23:33

    if (update_y) pos.x = new_pos.y; // Дрогнула рука

    Потому что копипаста / DRY violation. Хотя бы так надо было:

    #define UPDATE_POS(AXIS) if (update_##AXIS) pos.AXIS = new_pos.AXIS
    
    UPDATE_POS(x);
    UPDATE_POS(y);
    UPDATE_POS(z);
    
    #undef UPDATE_POS
    

    Это лучше, чем «clang‑tidy с bugprone-* и -Wshadow», потому что уничтожает саму возможность совершить описанную ошибку.

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

    Я иногда говорю разрабам на языках без адресной арифметики и с виртуальной машиной, где всё отслеживается средой и заворачивается в подарочную упаковку exception’ов (C#, Java, ES): «Баги бывают в C/C++/Delphi. А у вас-то какие баги? Так, прохиндейство…» Обижаются!


    1. vvzvlad
      03.09.2026 23:33

      #define UPDATE_POS(AXIS) if (update_##AXIS) pos.AXIS = new_pos.AXIS

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


      1. ImagineTables
        03.09.2026 23:33

        Если вы ошибётесь в дефайне, это будет совсем другой тип ошибки. Объект будет, например, просто стоять на месте, если условие по ошибке всегда будет ложным. В оригинале у вас происходит смешение компонентов, а это гораздо хуже. Поэтому есть смысл избавиться только от плохого типа ошибок, если есть такая возможность. Полностью избавиться от ошибок всех типов нельзя, пока есть буквы.

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

        for each (auto entry in entries)
        {
                html::value item;
                item.set_item("DisplayName",    entry.DisplayName);
                item.set_item("IsFolder",       entry.IsFolder);
                item.set_item("IconPath",       entry.IconPath);
                item.set_item("FilePath",       entry.FilePath);
                item.set_item("LocalName",      entry.DisplayName);
                items.append(item);
            }
        

        Исправленная версия:

        for each (auto entry in entries)
        {
        #define STR_VALUE(arg) #arg
        #define SET_ITEM(field) item.set_item(STR_VALUE(field), entry.field)
                html::value item;
                SET_ITEM(DisplayName);
                SET_ITEM(IsFolder);
                SET_ITEM(IconPath);
                SET_ITEM(FilePath);
                SET_ITEM(LocalName);
        #undef SET_ITEM
        #undef STR_VALUE
                items.append(item);
        }
        

        Код очень старый (циклы, понимаешь!), я бы сейчас, конечно, декларативно это всё написал. Но более свежего кода нет: с тех пор, как я научился избавляться от копипасты препроцессором (тогда и научился), на эти грабли я больше ни разу не наступал. Чего и всем желаю.


        1. VADemon
          03.09.2026 23:33

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

          Для приведенных примеров хватит более простого синтаксиса: поддержка макроса for со вставкой заданных буквальных значений. Но такого нет.

          for each (auto entry in entries)
          {
                  html::value item;
          #for n /DisplayName/IsFolder/IconPath/FilePath/LocalName/
                  item.set_item("$n",    entry.$n);
          #endfor
                  items.append(item);
          }

          Синтаксис набросал по подобию Shell/sed. Это настолько частый случай, странно, что до сих пор не придумали частного решения этой проблемы.


          1. ImagineTables
            03.09.2026 23:33

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

            Метапрограммирование макросами, как раз, не громоздкое. Писать можно тот же самый код. Что там громоздкого? Склеивание ##? В VS 2026 есть классная штука: ПКМ по файлу → Preprocess, и вы видите то, во что развернулись макросы. Я часто пользуюсь. Вот этот пример с pos.##AXIS, прежде, чем запостить комментарий, я отрендерил и проверил в Студии. Добавили, не прошло и 50 лет после изобретения макросов. Или прошло? Сейчас посчитал, прошло 54 года. Непонятно, почему отладчик не умеет переключаться в режим раскрытия локальных макросов (всё подряд, как в Preprocess, слишком вербозно). Видимо, ещё через полвека добавят. Сейчас в МС сильно заняты проверкой возраста и ИИ.

            Ваш синтаксис мне нравится больше, чем имеющийся. В нём меньше копипасты, а то, что имеет семантику списка, оформлено как список.

            Периодически я пытаюсь найти альтернативы макросам, но каждый раз оказывается, что их нет. По этой причине, и потому, что мне надоело писать const вместо mut (и ещё по десятку причин) я серьёзно думаю, не переписать ли для начала десяток файлов на C++2. Тамошние генераторы хочу попробовать.


        1. vvzvlad
          03.09.2026 23:33

          SET_ITEM(DisplayName);

          Это работает только пока у вас и там и там DisplayName. Как только вам придется сделать item.set_item("IsFolder", entry.is_folder), все становится немного сложнее и гораздо более багогенерирующее.


    1. tenzink
      03.09.2026 23:33

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

      if (flag)
          UPDATE_POS(x);
      else
          UPDATE_POS(y);

      Раз любите макросы, то хотя бы пишите их грамотно. В этом случае нужно использовать трюк с do { ... } while(0)


      1. ImagineTables
        03.09.2026 23:33

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

        А по поводу грамотности добавления лишних «трюков с do { ... } while(0)» поговорите с коллегой из подветки, который считает что они и так слишком громоздки. Я же придерживался и придерживаюсь золотой середины. Использовать буду, но без лишней писанины, которая сама — источник багов.


        1. tenzink
          03.09.2026 23:33

          Сишные макросы это кувалда, которая легко проламывает систему типов и имеет подобные весёлые side-эффекты. Громоздкость, это цена, чтобы сделать их хоть чуть более безопасными.

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

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


          1. ImagineTables
            03.09.2026 23:33

            У последнего зарелизенного проекта с внутренней логикой на C++, над которым я работал (с ещё одним коллегой), было немногим менее миллиона пользователей. Пойдёт такая поделка?

            «В большом проекте писать потенциально опасные макросы» — если вы это про глобальные макросы, которые реально потенциально опасны, то не надо приплетать к ним меня. Я практикую другой препроцессинг: написал, тут же использовал, тут же уничтожил. Как бы, я привёл два разных примера на эту тему, можно было увидеть паттерн (если есть желание понять).

            А вообще, любой желающий отлить свой негатив к макросам во что-то общественно-полезное приглашается вот сюда: Как сделать простую рефлексию (стрингизацию имён типов) на C++23?. Вот куда я бы точно не хотел совать макросы, так это туда, но как всегда, когда коснулось, оказалось, что завезли один magic_enum.


            1. vvzvlad
              03.09.2026 23:33

              У последнего зарелизенного проекта с внутренней логикой на C++, над которым я работал (с ещё одним коллегой), было немногим менее миллиона пользователей. Пойдёт такая поделка?

              Есть люди, который на браинфаке пишут. Означает ли это, что это хороший подход к разработке ПО? Нет.

              То, что у вас хорошо работает ваш подход вовсе не означает, что он best. Просто означает что вы с ним умеете работать. А качество кода определяется немного другим.


  1. sacai
    03.09.2026 23:33

    Завели фиксированный массив на 1024 элемента, потому что динамическая память в игре это грех, и написали простую функцию спавна

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


  1. verls
    03.09.2026 23:33

    Интересный случай был в практике, когда один баг своими эффектами закрывал другой баг и всё штатно работало до рефакторинга :)


    1. dalerank Автор
      03.09.2026 23:33

      Да, есть такое. Похоже на еще одиг вид граблей


  1. rutenis
    03.09.2026 23:33

    А ещё бывали баги компилятора. Не знаю как с этим сейчас, я получил море удовольствия из-за того что msvc решил оптимизировать хвостовой вызов функции через jmp, но промахнулся со смещением параметров. В результате false превратился в ссылку null !


    1. tenzink
      03.09.2026 23:33

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


      1. rutenis
        03.09.2026 23:33

        Это был второй из серьезных багов msvc в моей практике. Первый был связан с кривой развёрткой циклов в режиме полной оптимизации.


      1. DoHelloWorld
        03.09.2026 23:33

        На самом деле встречается, особенно с проприетарными компиляторами для специфичных чипов. У меня чип есть в проде для телекома с архитектурой starcore. Сам чип от nxp, я когда там работал оглаживал кучу багов компилятора. Сейчас я юзер, для России поддержки нет, но вот у меня линкер иногда крашится при определённом размере секции. Нашли два ворэраунда, либо секции добивать, либо задизасмили линкер и точечно убрали jsr стой проверкой


      1. redfox0
        03.09.2026 23:33

        Не совсем компилятор. PL/SQL, встраиваемый язык Oracle DB.

        В триггере после определённого количества (больше 4 или 5) начинаются игнорироваться return. Компилируется без ошибок и предупреждений, даже построчная отладка показывает, что return словно не существует.

        Ворэраунд: не писать больше одного-двух return и использовать вложенные if.