си++
PVS-Studio и «Гопник-2»
Много читал об инструменте статического анализ
Нашлось несколько интересных ошибок. На скриншоте одна из них. Как видно, str_enemy_amount проверяется два раза, тогда как после первого условия там гарантировано ноль. Очевидно автор этого куска кода его скопипастил, но забыл во втором куске исправить enemy на hero.
Я бегло посмотрел все ошибки, которые он мне выдал, надо будет внимательно посмотреть каждую, но как будто бы всё по делу, ноль ложных срабатываний.
«Гопник-2»: убрал printf
Закончил убирать функцию printf, сегодня вколотил последний гвоздь в крышку её гроба. Я
В процессе замены встретилось вот такое. Загадка, всё ли хорошо в следующем коде?
std::cout << fmt::format(" {}(+{})", main_hero->inv[i].name, main_hero->inv[i].armo)Мой компилятор считает, что всё, но сборка на «Гитхабе» падает с ошибкой «Binding packed field error».
Получается, я по неведомой причине не могу обратиться по ссылке на поле внутри упакованной структуры (fmt::format принимает параметры по ссылке).
Структуру я пакую, чтобы нивелировать различия на бинарном уровне между компиляторами, — так как я её сохраняю и восстанавливаю как есть. В общем, от упаковки не отказаться, пришлось присвоить значения промежуточным переменным:
const auto name = main_hero->inv[i].name;
const auto armo = main_hero->inv[i].armo;
std::cout << fmt::format(" {}(+{})", name, armo);Выглядит, как костыль, но самое главное — почему один компилятор требует промежуточные переменные, а другой умеет и без них?
Я бы понял, если бы «Гитхаб» собирал мне бинарники
«Гопник-2»: что нового?
Можно подумать, что я совсем забросил заниматься игрой «
Недавно появившиеся контрибьюторы добавили мне в проект библиотеку fmt, что упростило работу, —
Осталось примерно 260 замен, не так много, было около 400 или около того.
Ещё ребята постарались добавить мне тесты. Я ещё не смотрел, но планирую ознакомиться с тем как это делает. Так как на Си++ я раньше не писал, то совершенно без понятия как они тут выглядят, любопытно.
Си++: форматированный вывод (продолжение)
Очень раздражает, что вместо того, чтобы писать прикладной код, я вынужден бороться с компиляторами, да не с одним, а с тремя, под разные операционные системы. Я как будто пытаюсь писать не на одном языке, а сразу на трёх, похожих, но не полностью.
Мои приключения c форматированным выводом в Си++ до сих пор не закончились, оказалось, что последний вариант обёртки над std::snprintf в одном из компиляторов не собирается с одним из видов аргументов, пришлось его обернуть в std::cref.
Умные люди в комментариях советуют перейти на внешнюю библиотеку fmt и я уже сделал подход к снаряду, но под mingw у меня сборка пока не заработала. Завтра попытаюсь ещё раз — хочу подключить библиотеку подмодулем к своему репозиторию встроить компиляцию в процесс сборки своего проекта. Мне кажется, что я делаю всё правильно, а вот mingw так не кажется, попытаюсь завтра побороть.
Си++: форматированный вывод
Си++, похоже, язык для воспитания героев — создаём трудности на ровном месте и начинаем их преодолевать. В моём случае началось всё с разумной, как мне до сих пор кажется, идеи — заменить сишные безобразные строки char * на прекрасный класс std::string из Си++.
В процессе в комментариях мне написали, что вместо него надо использовать std::string_view, чтобы избежать копирования. Так я узнал, что за строки в Си++ отвечают не один класс, а четыре — есть ещё базовые классы, с приставкой base.
Заодно пришлось перейти на стандарт 2017 года, так как до строк с семантикой перемещения додумались только недавно.
Всё шло хорошо, пока я не дошёл до форматирования текста. В Си этим занимается функция printf, а Си++ есть метод std::format, но вот беда — он появился только в стандарте 2020 года и его ещё нет ни в одном из компиляторов, которые есть у меня под рукой.
Пошёл на поклон к ЧатГПТ, который выдал мне такую обёртку:
template<typename... Args>
std::string string_format(const std::string format, Args... args) {
int size_s = std::snprintf(nullptr, 0, format.data(), args ...) + 1; // Extra space for '\0'
if (size_s <= 0) {
throw std::runtime_error("Error during formatting.");
}
auto size = static_cast<size_t>( size_s );
std::unique_ptr<char[]> buf(new char[size]);
std::snprintf(buf.get(), size, format.data(), args ...);
return {buf.get(), buf.get() + size - 1}; // We don't want the '\0' inside
}Это так называемый «шаблонная функция». Насколько я пока понимаю принцип, компилятор смотрит с каким типами параметров вызывается эта функция, и делает их столько штук, сколько есть комбинаций вызовов.
Так как файлы с кодом друг друга не видят, чтобы шаблонная функция «знала» с какими параметрами её вызывают, её надо помещать в заголовочный файл (🤦🏻♂️), либо указать в коде все необходимые прототипы вручную (🤦🏻♂️🤦🏻♂️🤦🏻♂️).
И хотя я осознаю, что со своим уставом в чужой монастырь не лезут, первое мне кажется противоестественным, а второе — неудобным.
На этом этапе я выяснил, что мой компилятор под «Виндоуз» ничего не знает про std::string_view, а компилятор под «Мак» не может скомпилировать код выше вот с такой прекрасной ошибкой:
comm/../utils.hpp:30:60: error: cannot pass object of non-trivial type 'std::string' through variadic function; call will abort at runtime [-Wnon-pod-varargs]
int size_s = std::snprintf(nullptr, 0, format.c_str(), std::forward<Args>(args)...) + 1;Мне бы хотелось, чтобы код собирался под оба компилятора, поэтому я снова пошёл к ЧатГПТ и вернулся вот с таким прекрасным кодом:
template<typename... Args>
std::string string_format(const std::string& format, Args&&... args) {
auto size_s = std::snprintf(nullptr, 0, format.c_str(),
std::decay_t<Args>(args).c_str()...) + 1; // Extra space for '\0'
if (size_s <= 0) {
throw std::runtime_error("Error during formatting.");
}
auto size = static_cast<size_t>( size_s );
std::unique_ptr<char[]> buf(new char[size]);
std::snprintf(buf.get(), size, format.c_str(),
std::decay_t<Args>(args).c_str()...);
return {buf.get(), buf.get() + size - 1}; // We don't want the '\0' inside
}Тут происходит
comm/../utils.hpp:32:58: error: request for member 'c_str' in 'args#0', which is of non-class type 'std::decay_t<char*>' {aka 'char*'}
std::decay_t<Args>(args).c_str()...) + 1;К счастью, решилось это просто — надо было всего лишь убрать .c_str().
По пути там пришлось решить ещё пару забавных мелочей, но они недостаточно отложились в памяти, чтобы я их мог воспроизвести без опасения
Теперь я понимаю за что программистам на Си++ платят такие деньги — это доплата за нервную работу.
Бесплатный CLion?
Два дня до момента, когда моя среда разработки, где я мучаю Си++, превратится в тыкву. С преподавательской лицензией меня обломали, попробую другой путь — податься на бесплатную лицензию как бесплатный проект.
Казалось бы дело в шляпе, но там надо показать регулярные коммиты за три месяца, а у меня пока ещё и месяц не прошёл. Но я придумал план, надёжный, как швейцарские часы, — два месяца рефакторинга. Буду перетаскивать очевидные сишные части на Си++. Начал со строк — переписываю сишные указатели на нормальный std::string.
Придётся, видимо, пока взять в качестве редактора VSCode, вот только он не IDE. Ну а какой выбор? В процессе знакомлюсь с тем, что умеет Си++. Смотрите какая прелесть:
std::ostream &operator<<(std::ostream &os, const Colors &color) {
static const std::map <Colors, std::string> colorNames = {
{RESET, "39;49m"},
{BLUE, "01;34m"},
{GREEN, "01;32m"},
{CYAN, "01;36m"},
{RED, "01;31m"},
{MAGENTA, "01;35m"},
{YELLOW, "01;33m"},
{WHITE, "01;37m"},
{BLACK, "01;30m"},
};
os << "\033[" << colorNames.at(color);
return os;
}В Си это была большая функция с оператором switch, которая выводила на экран последовательности для переключения цвета, её надо было вызывать отдельно каждый раз. На Си++ всё удобнее:
std::cout << YELLOW << mess[2] << std::flush;Рраз и строка жёлтая. YELLOW тут у меня — значение перечисляемого типа Colors.
«Гопник-2»: переход на нормальный Си++11
Как я писал прежде, когда я адаптировал
Тут у нас в Татарстане выдались длинные выходные, — в праздник
Вызов _fileno у меня использовался для получения номера дескриптора входного потока, так что тут всё решилось просто, — для этого есть специальная константа STDIN_FILENO. А вот с strdup пришлось повозиться.
Сначала я, разумеется, сделал свою реализацию — там
Первым делом пошёл на поклон к «ЧатГПТ», спросил совета. Советы не заработали, но дали направление мысли.
Первая мысль была забахать макрос, который заменял бы собой strdup в проблемном компиляторе, а остальные не трогал. Тут надо сказать, что под «Виндой» далеко не во всех наборах утилит компиляторы не содержат функцию strdup, например в msys2 она есть.
Мне хотелось
Пришлось пошарить по исходникам. Оказалось, есть ряд макросов, которые присутствуют в msys2, но отсутствуют в msys. Из них я выбрал __MINGW64_VERSION_MAJOR:
#if defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR)
#define strdup(s) (__extension__({ const char* __s = (s); size_t __l = strlen(__s) + 1; char* __d = (char*)malloc(__l);\
__d ? (char*)memcpy(__d, __s, __l) : nullptr; }))
#endifТут используется расширение, которое позволяет писать многострочные макросы. Хотя этот вариант мне и нравится, но опять используется не нормальный Си++, а расширение компилятора.
Вторая мысль, на которую меня навела нейросеть — использовать слабые символы языка Си++. Это совершенно новая для меня штука, поэтому нужно небольшое пояснение. Прежде всего для меня же, чтобы я в будущем не забыл что это вообще такое.
Если упрощать, то символы — это экспортируемые именованные сущности. Те, что
Слабые символы используются для библиотек, чтобы пользователь мог заменить некоторые сущности, если они его не устраивают.
Если перевернуть ситуацию, и считать мой код библиотекой, а тот код, где должна быть определена strdup основным, то всё должно получиться.
«ЧатГПТ», к сожалению, помочь не смог, — сначала подсовывал нерабочий код, а потом зациклился в своих советах. Поэтому пришлось разбираться самостоятельно.
Всё оказалось не так уж и сложно. Сначала в заголовочном файле определяем прототип функции:
#ifdef __MINGW32__
char *strdup(const char *s1);
#endifЯ его спрятал под условие, чтобы мой редактор перестал ругаться на избыточное определение, но всё работает и без этого.
Потом в самом коде создаём слабый символ для strdup:
#ifdef __MINGW32__
#pragma weak my_strdup
char *strdup(const char *src) {
size_t len = strlen(src) + 1;
char *s = (char *) malloc(len);
return s == nullptr ? nullptr : (char *) memcpy(s, src, len);
}
#endifСлабый символ я определяю через директиву #pragma weak, у которой надо указать имя слабого символа, причём оно должно отличаться от имени функции. Я пока не до конца понял зачем это нужно делать, но без этого не работает.
Кстати, #pragma weak тоже расширение, но оно легко заменяется на атрибут weak, надо только будет изучить его синтаксис, я не успел этого сделать.
На имя my_strdup у меня ругается редактор — говорит, что слабый символ нигде не определён, поэтому я опять же засунул всё это в условную компиляцию.
В выходные, как всегда, есть куча дел, кроме хобби, поэтому до конца я разобраться не успел и буду рад, если
Дополнено: в итоге все ситуации я побороть не смог и откатился на самое простое решение со своей собственной реализацией strdup.
auto в Си и Си++
Несмотря на неприязнь к Си++,
Одна из таких мелочей — ключевое слово auto. Оно есть и в Си, но значение у него совершенно другое, в современном коде, как будто бы, его использовать негде.
В Си++ оно означает автоматический вывод типа, вместо его явного указания. Клёвая штука, позволяет делать например такое (пример из кода
auto old_mode = set_tty_special_mode();
// тут какой-то код
restore_tty_mode(old_mode);Фишка тут в том, что у set_tty_special_mode и restore_tty_mode две реализации — под «Виндоуз» своя. Эти две реализации работают с разными типами, под «Виндоуз» там DWORD, в остальных случаях — структура termios.
В Си было бы
#ifdef __MINGW32__
#typedef DWORD terminal_mode
#else
#typedef struct termios terminal_mode
#endif
terminal_mode old_mode = set_tty_special_mode();
// тут какой-то код
restore_tty_mode(old_mode);А в Си++ можно просто поставить auto и обойтись без шаблонного кода. Очень хочется иметь такую клёвую примочку и в Си.
Программируем с GPT
Мне
Нужно было переписать вот такой код с «Пайтона» на «Си++»:
{72: 65, 80: 66}.get(value, 0)Что тут происходит? Если value равно 72, вернуть 65, если 80, то 66, иначе ноль. Нужно мне это, если вкратце, для преобразования виндовых кодов в линуксовые.
Так как Си++ я не знаю, описал на Пайтоне — так быстрее и проще показать, что код мне нужен компактный — хочется не городить switch, а добавлять новое правило как можно проще.
Вот что мне сгенерировал
int value = 72;
std::map<int, int> mymap = {{72, 65}, {80, 66}};
int result = mymap.count(value) ? mymap[value] : 0;После консультации с живыми людьми, выяснилось, что плохо тут примерно всё.
В итоге, код стал выглядеть следующим образом:
const static std::unordered_map<int, int> win2lin = {{72, 65}, {80, 66}};
auto it = win2lin.find(_getch());
ch = it == win2lin.end() ? 0 : it->second;
«Гопник-2»: разные mingw
Надо
Сильнее всего меня удивило в процессе, что поведение mingw, — инструментария для компиляции приложений под «Виндоуз», различается под разными операционными системами.
Поскольку мне не хотелось заморачиваться с установкой софта на виртуальную машину, я развернул mingw у себя под «Маком» и внёс правки, чтобы всё благополучно компилировалось именно на этой ОС.
Запустив получившееся на первой попавшейся виндовой виртуалке, я успокоился. Но оказалось этого недостаточно — под самой «Виндоуз» приложение не собиралось!
После этих правок игра начала собираться под «Виндой» тоже.
Недавно прочитал в одной из статей на Хабре, что «язык Си++ достаточно сложен, чтобы позволить нам писать на нём просто», как же это верно! Неожиданно много времени занимает борьба
«Гопник-2»: UTF-8
Продолжаю писать про портирование
Я уже очень давно не работаю под этой операционной системой, поэтому некоторые обычные, возможно, для многих вещи для меня являются открытием. Когда я начинал процесс портирования игры под эту платформу, я был уверен, что столкнусь в трудностями совершенно в других частях кода. Одной из неожиданностей была плохая работа «Виндоуз» с кодировкой
Если вывод в консоль удалось победить сравнительно легко, то со вводом возникли проблемы — вызов SetConsoleCP(CP_UTF8), который должен был помочь, не помог. В интернете пишут, что он, по всей видимости, не реализован — просто не делает ничего.
Если с латинскими буквами проблем не возникает, они кодируются одним байтом, это ничем не отличается от того как программы до этого работали десятилетиями, а вот с русскими буквами беда — для них требуется два байта и в таком виде Виндоуз их не понимает.
Рекомендуемый специалистами способ — переключить консоль в режим ввода кодировки
#ifdef __MINGW32__
int wlen = 100;
int save = _setmode(_fileno(stdin), _O_U16TEXT);
wchar_t *wstr = (wchar_t *) malloc(wlen * sizeof(wchar_t));
fgetws(wstr, wlen, stdin);
int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, 0, 0, 0, 0);
user_name = (char *) malloc(len);
WideCharToMultiByte(CP_UTF8, 0, wstr, -1, user_name, len, 0, 0);
_setmode(_fileno(stdin), save);
free(wstr);
#endifМне, к счастью, ввод русских букв нужен только в одном месте — там где игрок вводит своё имя. Я убрал используемую там функцию fgets под условную компиляцию и вставил кусок, который выше. С таким исправлением всё заработало, как ожидается.
«Гопник-2»: адаптация под Windows
В итоге, вчерашняя мысль адаптировать
Несмотря на то, что «Чат» у меня последней, четвёртой версии, всё равно, даже на простых примерах он допускает совершенно детские ошибки, — не подключает нужные заголовочные файлы (мы программировали на Си), упускает важные части.
На скриншоте у меня как раз такой пример — он не скомпилируется
Вот как в итоге стал выглядеть этот кусок после нескольких уточняющих вопросов и гуглежа:
#ifdef __MINGW32__
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode = 0;
GetConsoleMode(hStdin, &mode);
if (echo) {
mode &= ~ENABLE_ECHO_INPUT;
} else {
mode |= ENABLE_ECHO_INPUT;
}
SetConsoleMode(hStdin, mode);
int ch = _getch();
if (ch == 0x03) // Ctrl+C
{
printf("Ctrl+C hit, exiting...\n");
gracefulexit();
}
return ch;
#endifКак бы то ни было, с помощью нейросетей
#ifdef __MINGW32__
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
setvbuf(stdout, nullptr, _IONBF, 0);
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD mode = 0;
GetConsoleMode(handle, &mode);
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
#endifПришлось полностью выключить буферизацию, чтобы нормально отрабатывал printf с
Насколько я понял то, что написано в интернете, у консоли Виндоуз до сих пор большие проблемы с кодировкой
Из заметных багов — игра падает при попытке загрузки сохранения и совсем не работает ввод русских букв. Второе мне нужно только одном месте (где запрашивается ввод имени игрока), так что есть надежда это починить. Остальное как будто работает.
Блокировка экрана на «Маке» из Си
CGSCreateLoginSession(NULL);
// теперь выдаёт
// Assertion failed: (sid != NULL), function CGSCreateLoginSession, file Services/Connection/CGSSession.c, line 418.Возможно
В общем, покапался я в заголовочных файлах, подбором и интуицией родил код, который блокирует экран
// получаем словарик с атрибутами текущей сессии
CFDictionaryRef dict = CGSCopyCurrentSessionDictionary();
// выбираем из него идентификатор текущей сессии, получаем его в виде int
CFNumberRef number = (CFNumberRef) CFDictionaryGetValue(dict, CFSTR("kCGSSessionIDKey"));
CGSSessionID currentSession;
CFNumberGetValue(number, kCFNumberIntType, ¤tSession);
// блокируем текущую сессию
CGSCreateLoginSession(¤tSession);Обновлённая версия лежит там же — на «Гитхабе».
_Noreturn в C11
Си — язык на котором я программирую редко, но всё же такое случается. Соответственно слежу что там появляется нового (кстати, есть ребята, которые понятия не имеют, что этот язык вообще развивается).
В относительно недавно вышедшей редакции
Смысла этого я не понимал,
Я noreturn никогда не пользовалась, но насколько я знаю он нужен, когда функция вообще не возвращает значение. В void функции можно сказать ’return;’
Noreturn-функции либо бросают исключения, либо говорят exitв какой-то момент. Если компилятор знает, что из функции возврата нет, то может сделать соответствующую оптимизацию.За Си не слежу, мне здесь C# полезнее, чем С.
Чуть подробнее об этом написано в описании атрибута «noreturn» компилятора gcc (там есть аналог в виде расширения языка) на примере функции fatal:
The noreturn keyword tells the compiler to assume that fatal cannot return. It can then optimize without regard to what would happen if fatal ever did return. This makes slightly better code. More importantly, it helps avoid spurious warnings of uninitialized variables.
The noreturn keyword does not affect the exceptional path when that applies:
a noreturn-marked function may still return to the caller by throwing an exception or calling longjmp.Do not assume that registers saved by the calling function are restored before calling the noreturn function.
В общем, примерно понятно, хотя, на мой взгляд, всё равно выглядит как микрооптимизация.
Клуб нелюбителей C++
В своё время я посмотрел массу языков программирования (и продожаю смотреть), на некоторых —
Триграфы в Си
Век живи, век учись, больше заработаешь. Язык C я недолюбливаю, преимущественно
Вообще триграфы были придуманы для терминалов, в которых некоторых символов не хватает. В итоге вместо #define можно написать ??=define. Триграфы подменяются на нужные символы в самом начале, поэтому эти записи эквивалентны. Вместо { можно написать ??< , вместо } использовать ??>
Остальные триграфы можно найти в статье Елены Сагалаевой «Триграфы и альтернативные символы» на её персональном блоге. Кстати, очень рекомендую сам блог — отличное чтиво на темы связанные с программированием.