Си++: форматированный вывод
Си++, похоже, язык для воспитания героев — создаём трудности на ровном месте и начинаем их преодолевать. В моём случае началось всё с разумной, как мне до сих пор кажется, идеи — заменить сишные безобразные строки 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().
По пути там пришлось решить ещё пару забавных мелочей, но они недостаточно отложились в памяти, чтобы я их мог воспроизвести без опасения
Теперь я понимаю за что программистам на Си++ платят такие деньги — это доплата за нервную работу.

Комментарии 12
Рекомендую просто использовать fmt. За основу format в C++20 взят именно он.
Можно использовать его как в компилируемом виде (предпочтительно), так и чисто как
https://fmt.dev
И присоединяюсь к комменту выше про fmt.
Еще есть boost, но он жирный, его интегрировать — будет из пушки по воробьям.
2. Классический способ форматировать данные в C++ — с использованием потоков
3. Современный способ — таки std::format. В 2023 году жаловаться на отсутствие компиляторов — грех: стандарту уже три года, а поддержка появилась задолго до выхода стандарта. К сожалению, проблему с локализацией std::format всё равно полностью не решает, и всякие «выбрано 1 файлов» неизбежны. Но уже жить можно.
Ну что у меня стоит, тем и собираю ) А раз там двадцатый стандарт поддерживается не полностью, то значит и у остальных пользователей будут проблемы.
А можно подробнее? Потому что именно так сейчас и работает — в формате %s, в аргументах std::string.
1.
2. Потом появилась потребность поддерживать другие языки. С большинством языков проблем не возникло: их буквы можно было запихать в верхнюю половину кодовой таблицы. Так появился зоопарк разных кодировок: только для русского языка были
3. Потом вспомнили про китайский язык, у которого «букв» намного больше 256. Возник Unicode, который решил собрать
4. В это время Microsoft решил, что уж 16 битов точно хватит всем. И добавил в Windows API поддержку
5. В марте 2001 года вышла версия Unicode 3.1, в которой было добавлено ещё 42 тысячи китайских иероглифов, а общее количество символов достигло 94205. 16 битов не хватило, «широкие строки» превратились в чемодан без ручки.
6. Юниксовый мир в это время шёл в совсем другом направлении, и в результате пришёл к кодировке
7. Сейчас и Microsoft уже пытается переходить на
Да, якак-то писал статью про Юникод, в курсе.
СUCS-2 и кривоватой (только на вывод) поддержкой UTF-8 в Виндоуз, я тоже уже успел столкнуться, мне не понравилось ))
Спасибо за развёрнутый ответ!
Возможно, без них уже не получиться обойтись, если строить все на готовых компонентах(и не только на Windows, а и на MacOS с iOS). Но в этом случае, нврн, будет выгоднее делать конвертацию в моменте. Каким бы злом широкие строчки ни были, с ними приходится рано или поздно столкнуться.
Вся хитрость в части «std::decay_t(args).c_str()…». Если её оставить, то всё будет работать корректно, поскольку метод c_str() преобразует std::string в const char *. Но… аргументами такой функции могут быть только строки, int в неё не запихнёшь, поскольку у int нет метода c_str(). Так не интересно.non-POD class types (until C++11)class types with an eligible non-trivial copy constructor, an eligible non-trivial move constructor, or a non-trivial destructor, together with scoped enumerations (since C++11), are conditionally-supported in potentially-evaluated calls with implementation-defined semantics (these types are always supported in unevaluated calls)». Если коротко, то передавать в variadic функции можно _некоторые_ C++ типы, и std::string к ним не относится.
как-то разбираться, что ей передали и как это выводить. У компилятора, когда он компилировал функцию snprintf, данных о типах аргументов не было: это не шаблонная функция, а variadic function, она скомпилирована несколько лет назад и лежит где-то в libc. Единственное, на что может полагаться snprintf при разборе своих аргументов — это строка формата. А двоичное представление std::string далеко не всегда совпадает с const char *.
Если же вызов метода c_str() убрать, то в функцию snprintf будет передан аргумент в его изначальном виде. Если пришёл const char *, значит, const char *. Если int, значит, int. Если float, то… Ан нет, float будет преобразован в double (см. https://en.cppreference.com/w/cpp/language … _arguments). Если std::string, то… «Only arithmetic, enumeration, pointer, pointer to member, and class type arguments (after conversion) are allowed. However,
После этого будет сделан call snprintf. А дальше функции snprintf придётся
Ну или придумать