Comments 35
А если писать код на голых
mallocиfree, вылавливать утечки будет еще одним повседенным занятием. А вот найти, какой именно из пятисот вызовов выделения памяти не получил свойfreeэто задача со звездочкой. Если думаете, что умный GC или умные указатели решают эту проблему, то нет... не решают. Они просто меняют природу утечек и вместо утечки памяти получаем утечку ссылок и какой‑нибудь забытый менеджер сцены продолжает держать ссылку на невидимый объект, а тот по цепочке держит текстуры и звуки.
как мне кажется на С++ зависит от архитектуры... (+ если модули, если удобно настроено иде отловить можно я всё еще думаю), просто, чтобы легаси 10к-100к loc покидать по модулям и сохранить архитектуру времени уйдёт конечно, это понятно конечно....
В идеальном мире правильного и красивого кода - разработка этого самого кода стоила ровно ноль центов за тыщу лет работы, а дедлайн был через сто тыщ (световых) лет... соответственно, было комфортно работать со скоростью "одна отлаженная строка в час". И в те стародавние времена, когда этот мир, по некоторым слухам, существовал, бывали в самом деле программы без единого бага. Слухи упорно сватают на эту роль LaTeX, например. Так это или нет - не знаю.
В мире реальном и современном, увы, писать приходится чуток побыстрее. А скоропись, точнопись и красивопись почему-то не очень хорошо уживаются вместе. Не только в программировании. В обычном рисовании тоже. Даже когда рисует или пишет какой-нибудь дядюшка Клод.
Проблема сия едва ли устранима. Но - это "фичебаг", который, собственно, и дает рабочие места тем, кто занимается переработкой хитина...
Плата за производительность - вещь универсальная, у нее есть лишь два измерения: цена и качество. Если при этом цена фиксирована, то что страдает - увы, вопрос риторический. С тем и живём
Многопоточные гонки, кстати, отчасти диагностируются thread sanitizer'ом.
Иногда тсан даёт ложноположительные срабатывания, но каждое из них нужно убедительно доказывать, что оно именно ложно положительное.
Не везде он доступен, под msvc его так до сих пор нет, а если пробовать компилировать другим компиляторов, то можно получить ещё 1 ряд весёлых багов
Имхо, компилировать код разными компиляторами не помешает как раз для того, чтобы побольше разных багов выловить. Иной раз под Windows всё собирается и работает, а под Linux компилятор либо вообще код не собирает, либо показывает предупреждения, которых под Windows не было, это порой это наводит на мысли, что код надо бы исправить.
Чем дальше от начала координат, тем... дело даже не в ошибках конечной разрядности плавающей арифметики, а в ошибках вообще.
Преобразования систем координат делаются на матрицах, в одну сторону - на прямых, в обратную - на обратных. Если матрица плохо обусловлена, оператор преобразования становится люто чувствителен к малым изменениям (в том числе, к погрешностям любой природы - хоть из младших разрядов, хоть из физики).
И хотя с позиций чистой математики любое аффинное преобразование можно представить как матричное умножение в N+1-мерном пространстве (вектор-точка аугментирован единичкой, вектор-длина - ноликом), или же, что то же самое, как последовательность из вращения-масштабирования - матричного умножения в N-мерном пространстве - и затем параллельного переноса,
вот специально для таких ситуаций лучше не умничать, а разложить преобразование как последовательность произвольных вращений и переносов, - а обратное преобразование сыграть ровно в обратном порядке: отрицательный перенос, обратное вращение, отрицательный перенос, обратное вращение...
Отдельный ад возникает при интер- и экстраполяции движения. Там получается набор интерполированных преобразований, которые надо применять к исходной точке. А интерполяция - это всегда те самые малые изменения. Которые с плохо обусловленными матрицами делают очень больно!
Поэтому интерполировать нужно в какой-то локальной системе координат.
Интерполяция, опять же, бывает разная. Самые очевидные схемы - это линейная (слерп кватернионов и лерп положения) или матричная экспонента (движение по дуге).
Но вот беда, матричная экспонента вычисляется через матричный ряд Тейлора. С ограниченной точностью. То есть, с заданной погрешностью. Поэтому те самые малые изменения у нас зашумлены по определению!
И это не только геймдева касается, но и моделирования движения в реальном географическом пространстве.
Хорошо промышленному роботу: прикручен к станине или, максимум, катается по цеху. А вот беспилотный автомобиль или летательный аппарат отхватывает все эти нюансы вычислительной математики полной ложкой!
В беспилотрых системах, насколько я могу судить по общедоступным источникам (другой инфы не имею), спасает смена, время от времени, локальной системы координат с привязкой к карте. Понятно, что на экране сие будет выглядеть как, опять же, хитиновый товарищ. Да и хранить всего надо будет побольше, и с кодом придется помучиться. Но - сие вопрос ситуативный. Если сильно надо - решение есть. Если не сильно - то "и так сойдет". Фичебаг превращается в багофичу..
Для матрицы вращения 3х3 экспоненту можно вычислить в замкнутой формуле (и для кватернионов и моторов аналогично). Из минусов - там вылазят вычисления sin, cos и atan2, но для малых углов можно разложить формулы в ряд Тейлора и получить и точность и скорость вычислений. Вдобавок у чисел с плавающей запятой есть замечательное свойство - они умеют хранить очень маленькие или очень большие числа типа 1е-50 или 1е50, и точность не страдает. Я в остальном согласен, но именно интерполяция не самая проблемная часть.
Точность там страдает дай боже, когда находишь обратное преобразование. Сам наблюдал такое и сам фиксил.
Дотошно не воспроизведу сейчас, но было вот такое.
Есть задача восстановления лидарного облака с движущегося лидара кругового обзора, находящегося на автомобиле.
Лидар каждую точку снимает в конкретный момент времени. Получаем облако точек в пространстве-времени в координатах лидара. Зная положение лидара на автомобиле и траекторию автомобиля в пространстве-времени в координатах неподвижного мира, пересчитываем точки в пространство-время мира. Приводим к единому времени (для простоты, считаем объекты неподвижными). Восстанавливаем в пространство автомобиля.
Если ноль координат мира очень далеко (сотня километров) от положения автомобиля, то траектория - это не только и не столько прямолинейное движение на расстояние 1-2 метра, но и поворот на очень маленький угол (стотысячные доли радиана).
Соответственно, поворот всех этих точек облака - это тоже стотысячные радиана на сотню километров, плюс поворот лидарного луча на сотые радиана и десяток метров. Сначала в одну сторону, потом в другую.
И вот это шараханье на сотню километров туда и обратно на разные микрорадианы - радикально умножает.
У меня не сохранился юпитер-ноутбук с иллюстрацией, - если будет час та натхнення, попробую воспроизвести, но не обещаю.
Простая фигура (окружность равноудалённых от лидара точек) превращалась в расходящуюся спираль.
А, про такое я знаю, но в идеале не надо на сотню километров от центра уходить. Тогда да, любая погрешность с углом умножается на расстояние и точность падает на пять порядков (если 1 это один метр, а если один сантиметр или миллиметр то ещё хуже). А вам даже точности double не хватало? Там точность 15 знаков и как будто их достаточно.
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. А у вас-то какие баги? Так, прохиндейство…» Обижаются!
#define UPDATE_POS(AXIS) if (update_##AXIS) pos.AXIS = new_pos.AXIS
Ага, в дефайнах и том, как это разворачивается потом нельзя же ошибиться.
Если вы ошибётесь в дефайне, это будет совсем другой тип ошибки. Объект будет, например, просто стоять на месте, если условие по ошибке всегда будет ложным. В оригинале у вас происходит смешение компонентов, а это гораздо хуже. Поэтому есть смысл избавиться только от плохого типа ошибок, если есть такая возможность. Полностью избавиться от ошибок всех типов нельзя, пока есть буквы.
Вот пример из реального проекта, который был написан с копипастом и ошибку ловили долго, потому что на практике некоторые разные поля почти всегда совпадают, отличаются они в особенных ситуациях, и тогда это баг:
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);
}
Код очень старый (циклы, понимаешь!), я бы сейчас, конечно, декларативно это всё написал. Но более свежего кода нет: с тех пор, как я научился избавляться от копипасты препроцессором (тогда и научился), на эти грабли я больше ни разу не наступал. Чего и всем желаю.
Тут проблема другого плана. Что не получается выразить средствами языка, напрашивается на метапрограммирование. Если это метапрограммирование есть, оно обычно слишком громоздкое. В результате человек берет меньшее из зол и копипастит.
Для приведенных примеров хватит более простого синтаксиса: поддержка макроса 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. Это настолько частый случай, странно, что до сих пор не придумали частного решения этой проблемы.
Копипаста не может быть меньшим из зол, в этом мой поинт. Автор привёл один пример, я другой, и поверьте, ловить редкую разницу в путях… А там, знаете, есть нормализация 8.3 (до сих пор надо учитывать!), есть резолвинг (всякие . и ..)… Короче, это было больно.
Метапрограммирование макросами, как раз, не громоздкое. Писать можно тот же самый код. Что там громоздкого? Склеивание ##? В VS 2026 есть классная штука: ПКМ по файлу → Preprocess, и вы видите то, во что развернулись макросы. Я часто пользуюсь. Вот этот пример с pos.##AXIS, прежде, чем запостить комментарий, я отрендерил и проверил в Студии. Добавили, не прошло и 50 лет после изобретения макросов. Или прошло? Сейчас посчитал, прошло 54 года. Непонятно, почему отладчик не умеет переключаться в режим раскрытия локальных макросов (всё подряд, как в Preprocess, слишком вербозно). Видимо, ещё через полвека добавят. Сейчас в МС сильно заняты проверкой возраста и ИИ.
Ваш синтаксис мне нравится больше, чем имеющийся. В нём меньше копипасты, а то, что имеет семантику списка, оформлено как список.
Периодически я пытаюсь найти альтернативы макросам, но каждый раз оказывается, что их нет. По этой причине, и потому, что мне надоело писать const вместо mut (и ещё по десятку причин) я серьёзно думаю, не переписать ли для начала десяток файлов на C++2. Тамошние генераторы хочу попробовать.
SET_ITEM(DisplayName);
Это работает только пока у вас и там и там DisplayName. Как только вам придется сделать item.set_item("IsFolder", entry.is_folder), все становится немного сложнее и гораздо более багогенерирующее.
Так в этом же суть.
Когда я задал вопрос про стрингизацию в C++23, то ждал, пока кто-нибудь спросит, а зачем мне вообще маппинг имён 1:1. Ответ (на незаданный вопрос): во-первых, в C++ очень строгие требования к идентификаторам. Я это знаю, потому что пытался их обойти, соорудив новый синтаксис лямбд из символа → (сделать из простой клавиатуры типографическую в любом случае полезно). Такой выразительности позавидовали бы и C#, и JS. Но не вышло, не фартануло. Как оказалось, C++ хорошо знает, где буква, а где нет. Вот так можно:
const int π = 3;
А вот так — нельзя:
#define →(expr) -> decltype(expr) { return expr; }
Во-вторых, при маппинге обеспечивается уникальность пары «категория + настройка». Иначе код просто не скомпилируется. В чистом итоге, чего мы добились, гарантировав (пришлось это сделать макросами, хотя я бы предпочёл рефлексию) маппинг 1:1? Добились мы того, что компилятор проверяет валидность имён (нет спецсимволов, которые могут озадачить сериализатор и парсер) и проверяет непротиворечивость формата файла. Что может быть прекраснее, чем нагрузить компилятор ещё одной проверкой, да ещё и данных?
Подумать, какое это отношение имеет к item.set_item("IsFolder", entry.is_folder) остаётся в качестве самостоятельного упражнения.
Хорошие грабли вы заготовили для себя будущего и следующего, кто будет поддерживать этот код. Вызов UPDATE_POS кажется безопасным и выглядит прямо как вызов функции, но написан плохо. Вот тут будет oops.
if (flag)
UPDATE_POS(x);
else
UPDATE_POS(y);Раз любите макросы, то хотя бы пишите их грамотно. В этом случае нужно использовать трюк с do { ... } while(0)
Не надо. Объявление потому и написано одной строчкой выше использования, чтобы было видно, что это такое, и что это НЕ функция. И регистр как бы намекает.
А по поводу грамотности добавления лишних «трюков с do { ... } while(0)» поговорите с коллегой из подветки, который считает что они и так слишком громоздки. Я же придерживался и придерживаюсь золотой середины. Использовать буду, но без лишней писанины, которая сама — источник багов.
Сишные макросы это кувалда, которая легко проламывает систему типов и имеет подобные весёлые side-эффекты. Громоздкость, это цена, чтобы сделать их хоть чуть более безопасными.
В большом проекте писать потенциально опасные макросы - непрофессиональная небрежность. Завтра их кто-то вынесет в общий хедер, потому-что там нужна такая же логика, которая уже проверенно работает, и тоже хочется. Послезавтра кто-то другой использует не так как вы хотели и т.п.
Для личных поделок такая небрежность в написании макросов, безусловно, простительна.
У последнего зарелизенного проекта с внутренней логикой на C++, над которым я работал (с ещё одним коллегой), было немногим менее миллиона пользователей. Пойдёт такая поделка?
«В большом проекте писать потенциально опасные макросы» — если вы это про глобальные макросы, которые реально потенциально опасны, то не надо приплетать к ним меня. Я практикую другой препроцессинг: написал, тут же использовал, тут же уничтожил. Как бы, я привёл два разных примера на эту тему, можно было увидеть паттерн (если есть желание понять).
А вообще, любой желающий отлить свой негатив к макросам во что-то общественно-полезное приглашается вот сюда: Как сделать простую рефлексию (стрингизацию имён типов) на C++23?. Вот куда я бы точно не хотел совать макросы, так это туда, но как всегда, когда коснулось, оказалось, что завезли один magic_enum.
У последнего зарелизенного проекта с внутренней логикой на C++, над которым я работал (с ещё одним коллегой), было немногим менее миллиона пользователей. Пойдёт такая поделка?
Есть люди, который на браинфаке пишут. Означает ли это, что это хороший подход к разработке ПО? Нет.
То, что у вас хорошо работает ваш подход вовсе не означает, что он best. Просто означает что вы с ним умеете работать. А качество кода определяется немного другим.
Это надо говорить не мне, а коллеге, который перевёл тему на поделки. Если качество кода — самостоятельное понятие и не зависит от того, где код использован, в домашней поделке или огромном проекте, зачем тогда было упоминать домашние поделки? Понятно, что это было сделано, чтобы унасекомить. Так я тоже так умею 8-P
Если возразить по сути, без личных нападок, типа, что будет при несовпадении "IsFolder" и entry.is_folder, так я и отвечу по сути. Какая разница, где я это использовал. Хотя, конечно, приятно вспомнить, что этот код не сломался у (без малого) миллиона моих юзеров. Во многом благодаря тому, что я фанатично уничтожаю копипасту.
Макросы - тоже не всегда хорошо. А подобные опечатки вылавливаются статическими анализаторами вроде PVS Studio (не реклама - я просто их не особо много знаю).
Завели фиксированный массив на 1024 элемента, потому что динамическая память в игре это грех, и написали простую функцию спавна
не, ну писать в статический массив без проверки переполнения - это грех, как бывший энергетик-противоаварийщик подтверждаю. помнится, я для использования в обработчиках прерываний (QNX4) и сигналов (QNX6) был вынужден пойти поперек принципа упрощения и обернуть это дело в класс, который перегружал скобки и контролировал индекс (ибо этот код предназначался для упаковывания в либу и использования в разработке суровыми дядьками, пришедшими с Фортрана)...
Интересный случай был в практике, когда один баг своими эффектами закрывал другой баг и всё штатно работало до рефакторинга :)
А ещё бывали баги компилятора. Не знаю как с этим сейчас, я получил море удовольствия из-за того что msvc решил оптимизировать хвостовой вызов функции через jmp, но промахнулся со смещением параметров. В результате false превратился в ссылку null !
Теоретически я слышал, что такое бывает, и даже "друг говорил", что у его знакомого такое встретилось. Но на поверку всегда оказывалось, что там был UB или логическая ошибка. Наверняка у компилятора бывают баги, но на практике так и не удалось с этим столкнуться
Это был второй из серьезных багов msvc в моей практике. Первый был связан с кривой развёрткой циклов в режиме полной оптимизации.
На самом деле встречается, особенно с проприетарными компиляторами для специфичных чипов. У меня чип есть в проде для телекома с архитектурой starcore. Сам чип от nxp, я когда там работал оглаживал кучу багов компилятора. Сейчас я юзер, для России поддержки нет, но вот у меня линкер иногда крашится при определённом размере секции. Нашли два ворэраунда, либо секции добивать, либо задизасмили линкер и точечно убрали jsr стой проверкой
Не совсем компилятор. PL/SQL, встраиваемый язык Oracle DB.
В триггере после определённого количества (больше 4 или 5) начинаются игнорироваться return. Компилируется без ошибок и предупреждений, даже построчная отладка показывает, что return словно не существует.
Ворэраунд: не писать больше одного-двух return и использовать вложенные if.
class Foo {
int value;
public:
Foo(int value) { // параметр затеняет поле class
value = value; // ничего не делает, присваивает параметр самому себе
}
};От таких вещей спасают соглашения об именовании переменных и членов класса. Например, переменные-члены класса всегда должны начинаться с m_ или просто _, а прочие переменные - не должны. Да и вообще много рекомендаций по написанию кода существует, чтобы поменьше на грабли наступать.
Анатомия граблей