Сравнительно недавно в stdlib плюсов появилось форматирование строк «как в питоне», а точнее, как в библиотеке fmt. И я, как смелый и отчаянный, решил этим воспользоваться. Возможно, аксакалы и настоящие разработчики скажут, что я всё делаю не так, и вообще не то, но я буду рад такой критике, если она поможет легче жить ;)
Итак, приступим. Вам понадобится свежий компилятор и стдлиб. На cppreference зеленеют красивые надписи (since C++20)
, круто! 2020-й уже давно прошёл, да и в мейкфайлах/vcxproj мы везде давно ставим --std=c++20
, кто посмелее — даже больше. Статьи про std::format
выходят уже несколько лет, и даже переводы на хабре есть [1], [2]. Значит, сейчас зафигачим маленький инклюдик, пройдёмся sed -e '...' по исходникам, и всё будет в шоколаде.
Не так быстро! Если, внезапно, микрософт впереди планеты всей, и format доступен как в свежей студии (2022), так в и в уже не свежей (2019), то если мы соберёмся собрать нашу программку, например, на убунту, тогда для разогрева нас сначала ждёт сайд-квест по установке самого нового gcc из ppa (или вообще из сорцов, если ваша система не самая модная и молодёжная, вы успеете сварить кофе, а если у вас нет кофеварки, то тоже успеете и заказать её, и получить, и сварить). Потому что фактически в GCC std::format появился совсем недавно. В шланге он есть якобы с 14й версии, но его даже не пробуйте, берите сразу 15+, потому что баги...
Ну что же, самым свежим компилятором и самым свежим stl мы обзавелись. (Кто-то скажет, надо было брать fmt и не выпендриваться, но мы же верим в светлое будущее!). Открываем, компилируем и запускаем Hello world! Работает! Но чувствуется привкус обмана... Во многих статьях упоминается, что это форматирование «почти как в питоне». Только данное «почти» слишком «почти». Возможно, такое сравнение прокатит для тех, кто никогда не видел питон, или какой-нибудь другой современный язык. Это совсем не f-strings из питона, и не interpolated string как в C#, и не template literals в JS/TS, и даже не костыль-макрос в Rust-е. C++ хоть и меняется семимильными шагами, но всё же, где ему угнаться до хипстерских языков. Ну штош, будем есть, что дают. К тому же, похожий формат применяется и в Zig, так что дальшейшие сравнения будут разве что с ним, а не с JS/C#/Rust (куда нам...).
Я что-то замечтался и отвлёкся, вернёмся к нашим баранам. У нас есть MSVC 14.36 и GCC 13.2, на них и будем пробовать. Встречайте, Hello world:
std::cout << std::format("Hello {}!\n", "world");
Так работает, отлично! Нам обещали какие-то проверки валидности, давайте попробуем:
std::cout << std::format("Hello {}!\n");
↓↓↓
/usr/local/gcc-13.2/include/c++/13.2.0/format:230:56: error: call to non-‘constexpr’ function ‘void std::__format::__invalid_arg_id_in_format_string()’
230 | __format::__invalid_arg_id_in_format_string();
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~
/usr/local/gcc-13.2/include/c++/13.2.0/format:180:3: note: ‘void std::__format::__invalid_arg_id_in_format_string()’ declared here
180 | __invalid_arg_id_in_format_string()
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
— Вжик, — сказала японская бензопила.
— Хм, — сказали суровые сибирские мужики.
std::cout << std::format("Hello {!\n", "world");
↓↓↓
/usr/local/gcc-13.2/include/c++/13.2.0/format:3468:68: error: call to non-‘constexpr’ function ‘void std::__format::__unmatched_left_brace_in_fomat_string()’
3468 | __format::__unmatched_left_brace_in_format_string();
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~
/usr/local/gcc-13.2/include/c++/13.2.0/format:165:3: note: ‘void std::__format::__unmatched_left_brace_in_format_string()’ declared here
165 | __unmatched_left_brace_in_format_string()
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
— Вж-жик, — сказала японская бензопила.
— Хммм... — сказали суровые сибирские мужики.
std::cout << std::format("Hello {}!\n", "cruel", "world");
↓↓↓
# ./a.out
Hello cruel!
— Дзинь, — сказала японская бензопила.
— Агааа! — сказали суровые сибирские мужики.
Да, какие-то проверки есть, и даже форматную строку оно проверяет! Но такой простой проверки, как количество аргументов нет. (Голосом Михалкова) Вас ки-ну-ли. Придётся всё самим, всё самим. Вот, например, как с этим справляются в одном из логгеров: binlog/create_source_and_event.hpp.
Конечно, форматную строку придётся распарсить заново. А ещё вопрос, почему во времена c++20 мы всё ещё используем макросы? Ага, это отличный вопрос, сейчас не будем углубляться, но хорошо, что заметили ;) Очевидно, что count_placeholders
по ссылке не выдерживает никакой критики, поэтому я напишу свой лунап^w count_placeholders:
constexpr std::size_t count_placeholders(const char* str) {
std::size_t result = 0;
for (std::size_t i = 0; str[i] != 0; ++i) {
assert(str[i] != '%');
if (str[i] == '{' && str[i + 1] != '{') {
++i;
for (; str[i] != 0; ++i) {
if (str[i] == '}' && str[i + 1] != '}') {
++result;
break;
}
}
}
}
return result;
}
Вот такого крокодила я написал для дополнительной проверки форматной строки. Эта конструкция не поддерживает пронумерованные аргументы вида {2} {3} {100500}, но в нашем случае (портировании printf) это и не требуется. Когда потребуется — можно будет расширить (например, выдавая в результат максимальный найденный индекс). На 4-й строке можно заметить ещё одну фишку — этот ассерт временный, он нужен на время миграции старого (супер-старого) кода на базе xxprintf, чтобы точно быть уверенным, что все старые форматы из кода выкосили и ничего не пропустили. В продакшене так делать, конечно, не стоит.
О боги ц++, спасибо, что есть constexpr и даже можно писать в нём сколько-то нетривиальные функции. Теперь обернём нашу проверку и std::format вместе:
template <class... Args>
void Log(int level, const char* fmt, Args&&... args) {
static_assert(count_placeholders(fmt) == sizeof...(Args));
std::cout << std::format(fmt, args...);
}
Хорошо я придумал, а, а?
error C2131: expression did not evaluate to a constant
ну или
error: non-constant condition for static assertion
Низзя! Нельзя так делать: constexpr может потреблять строки (в виде const char* / std::string / std::string_view), но передавать ни в какие вызовы не может. В стандартах что-то обсуждается на этот счёт, и даже есть какие-то костыльные библиотеки. Но самый простой способ — зафигачить старый добрый макрос! Вот, — скажете вы, — причина присутствия макросов в логгере. Но, я вас огорчу, это не единственная причина их использования. Если в данном примере закомментить static_assert, компилятор нас всё равно обломает :(
Хорошо, хорошо, я понял, макросы так макросы. Не жили хорошо, нечего и начинать:
#define Log(level, fmt, ...) \
do { \
static_assert(count_placeholders(fmt) == std::tuple_size<decltype(std::make_tuple(__VA_ARGS__))>::value,\
"Number of {} placeholders in format string must match number of arugments"); \
/* тут всякая логика логирования, проверка уровня, буферов, формат заголовка итд */ \
std::cout << std::format(fmt, __VA_ARGS__) << std::endl; \
/* конечно, никто в продакшене в stdout не пишет, но мы же только учимся... */ \
} while (false)
Считать количество аргументов в макросе не так прикольно, как в темплейте, но это самое не-вырвиглазное, что я смог найти. Ура, оно наконец-то компилируется! И даже логи выводит, и проверки работают. Можно приступать к конвертированию всех вызовов, а потом уже как-нибудь разберёмся с выводом в правильное время и правильное место. Борьбу с особо креативными форматами вида "value = %3.14lgbtq++"
я оставлю за скобками, всё же это скорее проблема printf, от которой мы хотим поскорее избавиться. Тут меня ждало несколько приятных моментов: благодаря compile-time проверкам были пойманы строчки с кривыми форматами, и даже одно место редко случающегося segfault-а. Так что переходить на std::format однозначно полезно ????????????
Все места вызова портировали, отлично. Пора нарастить мясо на скелет логгера. Для начала надо выяснить, как форматировать в предоставленный буфер. К счастью, о нас подумали, и сделали сразу две функции format_to и format_to_n, последняя из которых тут же уютно обосновалась в нашем макросе:
#define Log(level, fmt, ...) \
/* ... */ \
auto _l_count = std::format_to_n(_l_buff, _l_bufSize, fmt, __VA_ARGS__).size; \
/* ... */ \
Обратите внимание на префикс локальных переменных, он нужен чтобы избежать конфликта с вызывающим кодом. Функция format_to_n ещё очень удобна тем, что в её возвращаемом значении есть желаемый размер буфера, на случай если с первого раза не хватило. Вот теперь всё хорошо и статья на этом могла бы закончиться (но вы же видите, где скролл?), если бы я не посмотрел на размер получившегося бинарника: O_o упс! Файл из 1.2 МБайта распух до 6! (шести, Карл!) Хотя в наше время чат-клиентов, занимающих сотни мегабайт на диске (и не меньше — в ram), негоже стесняться каких-то 6МБ, я решил, что так не пойдёт. Мы должны бороться со злом, а не примкнуть к нему! К тому же, это плохо скажется на icache.
К гадалке не ходи, ясно что код распух из-за инлайна самого макроса и мелких методов, в нём вызывающихся. В старом printf-based логгере я решил этот вопрос вынесением основного тела логгера в функцию, помеченную __declspec(noinline)
(или __attribute__ ((noinline))
, каждому компилятору своё). Давайте попробуем провернуть то же самое и с нашим новым кодом:
template <class... Args>
FIX_NOINLINE void LogWorker(int level, const char* fmt, Args&&... args) {
// ...
auto _l_count = std::format_to_n(_l_buff, _l_bufSize, fmt, args...).size;
// ...
}
#define Log(level, fmt, ...) \
do { \
static_assert(count_placeholders(fmt) == std::tuple_size<decltype(std::make_tuple(__VA_ARGS__))>::value,\
"Number of {} placeholders in format string must match number of arugments"); \
LogWorker(level, fmt, __VA_ARGS__); \
} while (false)
↓↓↓
1>Logger.hpp(193,71): error C7595: 'std::basic_format_string<char>::basic_format_string': call to immediate function is not a constant expression
1>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.36.32532\include\format(3384,63): message : failure was caused by a read of a variable outside its lifetime
1>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.36.32532\include\format(3384,63): message : see usage of 'fmt'
... ещё пара сотен строк шаблонных ругательств от компилятора ...
В смысле?! Ах да, это же та самая проблема, где нельзя передать дальше константную форматную строку, из-за которой мы и городили макрос. Но как же оно внутри работает и не держит при этом 4 копии одного и того же кода? Тут есть 2 пути: нажать F12 и провалиться в метод, или пойти читать документацию. И тут и там мы увидим методы vformat***, братьев-близнецов тех, которые мы уже используем. Плюсы: им можно передавать любой формат, минусы: они его не валидируют, им пофиг и нет функции vformat_to_n
, хотя в оригинале её таки сделали. Где логика?™
Для статической проверки форматной строки требуется const std::format_string<Args...> fmt
. И вот его нет в несвежей студии, потому что в стандарте он появился не сразу, так что снова придётся городить макрос:
#define fmt_get get()
#ifdef _MSC_VER
# if _MSC_VER <= 1935
# define format_string _Fmt_string
# undef fmt_get
# define fmt_get _Str
# endif
#endif
Отлично, почти всё что требуется мы уже закостылили! Но продолжаем ныть дальше: если вы пользуетесь только MS Visual Studio, то готовьтесь к абсолютно невменяемым текстам ошибок. Начиная с call to immediate function is not a constant expression
по любому поводу (и без), и заканчивая километровыми портянками, в которых STL срыгивает на вас своё нутро. Я уверен, в аду припасён отдельный котёл для разработчиков STL, в котором они будут читать сообщения об ошибках в темплейтах до скончания веков... И ещё один по соседству — для boost-а. В gcc ошибки почти такие же длинные (шаблоны же), но чуть более адекватные.
Bonus track
Естественно, после решения всех насущных проблем захотелось большего: в частности, форматирования кастомных типов. Статей на эту тему тоже полно, поэтому тут подвоха я не ожидал. Первый попавшийся пример — полноценный форматтер из двух методов: parse и
format. На каждый мелкий енум городить такое не хочется, поэтому скроллим дальше и находим пример с унаследованным форматтером:
template<> struct std::formatter<MyEnum> : formatter<const char*> {
auto format(auto v, auto& ctx) { return formatter<const char*>::format(MyEnumToString(v), ctx); }
};
И он даже сразу заработал! Аллилуйя! ...Ха-ха, у gcc на этот счёт свои соображения:
/usr/local/gcc-13.2/include/c++/13.2.0/format:3270:38: error: no matching function for call to ‘std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(MyEnum&)’
3270 | basic_format_arg<_Context> __arg(__v);
| ^~~~~
... ещё 3 километра текста ошибок ...
(ノಠ益ಠ)ノ彡┻━┻ Что? Что ему опять надо??? К счастью, на этот раз ответ легко загуглить, gcc не хватает совсем чуть-чуть. Найдите 6 отличий:
template<> struct std::formatter<MyEnum> : formatter<const char*> {
auto format(auto v, auto& ctx) const { return formatter<const char*>::format(MyEnumToString(v), ctx); }
};
И последнее, про генерируемый код. Я, конечно, ожидал что будет что-то похожее на то, как делает Zig, ну или хотя-бы частично так. Ведь в этом весь прикол заранее известной форматной строки. Вот что генерирует зиг для Hello world:
И это не просто весь код main
, а почти весь код бинарника, который весит 5кб и не имеет импортов! Ладно, от C++ я такого не жду, но хотя-бы побить строку на части можно было?
А шиш там! Мы ещё и форматную строку будем каждый раз в рантайме парсить. Причём gcc (на скрине выше) генерирует самый приличный код из «большой троицы» (не считая сотен функций в бинарнике и ещё десятки импортированных).
Выводы
Несмотря на все эти неприятности, однозначно стоит переходить из каменного века (printf) и тёмных веков средневековья (iostreams). Даже в таком не самом оптимальном виде std::format
ни капли не медленнее printf и сильно быстрее iostream. Плюс мы получаем проверки формата во время компиляции, и даже можем дополнительно написать свои.
И в случае если вы не можете просто так обновить свой компилятор, советую попробовать библиотеку fmtlib, из которой собственно std::format и вырос. Она работает начиная с C++11 и используется во многих крупных проектах. Так что рекомендую!
А где-нибудь к C++30 мы получим форматирование как в питоне и других современных языках, я надеюсь ;)
Комментарии (26)
Kelbon
27.09.2023 09:45+2. Причём gcc (на скрине выше) генерирует самый приличный код из «большой троицы» (не считая сотен функций в бинарнике и ещё десятки импортированных).
кажется это типичная ошибка при работе с гцц - писать бенчмарк в мейне. Гцц по одной им известной причине по другому оптимизирует мейн нежели остальные функции
Нет, не хорошо, создаётся лишняя строка которая только потом выводится в cout, вместо этого лучше использовать format_to + ostreambuf_iterator
https://godbolt.org/z/xen6cT4h1Мы ещё и форматную строку будем каждый раз в рантайме парсить.
А вот это проблема, но решаемая, нужно чтобы реализации использовали факт проверенности строки на компиляции(кажется пока это закрывается спецификацией которая чётко говорит, что format должен вызывать vformat)
atd Автор
27.09.2023 09:45+3Спасибо, про main познавательно. Про последний пункт — да, надеемся.
Нет, не хорошо
У вас ссылка непосещённая ;)
rsashka
27.09.2023 09:45-1Плюс мы получаем проверки формата во время компиляции, и даже можем дополнительно написать свои.
Если это уже есть и в printf, то зачем городить огород? Чтобы строка формата как в Python, автоматический вывод типа или еще что-то?
Kelbon
27.09.2023 09:45+1этого нет в printf
printf это сишный вариадик который вносит рантайм оверхед(помимо прочих проблем)
не кастомизируем под свои типы(даже с расширениями), не переносим(нужно писать макросы в формат строке) и тд
rsashka
27.09.2023 09:45этого нет в printf
Нет чего, проверки форматов во время компиляции? А как же __attribute__ ((format(printf, 1, 2))); Если этого нет в компиляторе MS, то этого нет именно там, но не у printf, для которой проверка формата есть.
printf это сишный вариадик который вносит рантайм оверхед(помимо прочих проблем)
Который проверен десятилетиями и изучены всего подводные камни? И чем это плохо? А насчет оверхеда, я специально для embedded использую только printf, потому что с оверхедом при парсинге строки формата справляется самый простой микроконтроллер, а вот лишние 200-300 кБайт ПЗУ только для того, чтобы подключить форматированные ввод/вывод потоков это очень серьезный оверхед, который съедает в первую очередь деньги, т.к. заставляет использовать более старшие (и более дорогие) версии кристаллов.
не кастомизируем под свои типы(даже с расширениями), не переносим(нужно писать макросы в формат строке) и тд
Ни разу не возникало необходимости кастомизировать свои типы данных для форматного вывода, т.к. эта задача решается другим способом (конвертируешь свой тип данных в строку и выводишь как строку). И для этого не требуется новый форматный вывод.
А чтобы не писать макросы, можно определить собственную функцию с форматироваными аргументами как у printf, так же можно сделать и в предыдущем случае.Я не стараюсь вас убедить использовать старый и проверенный годами printf. Хотите использовать новомодный std::format_string, пожалуйста, это ваше право. Просто для себя я не увидел в нем ни одного ключевого преимущества, который бы заставил на него перейти.
Поэтому мой предыдущий комментарий про "зачем городить огород, есть printf" для меня актуален, так как я действительно хочу разобраться в вопросе. Но пока аргументы против printf притянуты за уши, но может быть я чего-то не знаю или не обращаю внимания?
Kelbon
27.09.2023 09:45А чтобы не писать макросы, можно определить собственную функцию с форматироваными аргументами как у printf, так же можно сделать и в предыдущем случае.
не поможет, если нужно вывести инт, а он на разных плафтормах разный, то ни шаблоны ни другие функции не помогут, придётся использовать макросы и пихать в формат строку
конвертируешь свой тип данных в строку и выводишь как строку
неэффективно
ввод/вывод потоков это очень серьезный оверхед
потоки это другое, не связано с std::format. Они действительно добавляют в бинарь своего, да и в целом неэффективны. Но формат к ним никак не относится, std::print, который внутри использует std::format, можно использовать вместе с FILE* и каким нибудь stdout
rsashka
27.09.2023 09:45-2не поможет, если нужно вывести инт, а он на разных плафтормах разный, то ни шаблоны ни другие функции не помогут, придётся использовать макросы и пихать в формат строку
Сделай
static_cast<int>
для аргумента и использую единую форматную строку для всех платформ. Я реально не понимаю, зачем тут макросы?неэффективно
Конечно, гораздо эффективнее подключить отдельную библиотеку, хотя свой формат в строку все равно придется конвертировать перед выводом.
потоки это другое, не связано с std::format....
Но это не ответ на вопрос, зачем прикручивать лишнюю библиотеку и менять работающее годами решение на другое, если у нового него нет серьезных преимуществ?
Kelbon
27.09.2023 09:45Сделай
static_cast<int>
дополнительное поле для ошибок, оверхеда, кривого кода. Интересно как выводить unsigned если кастовать всё к инту... А как выводить __int128? Ну и конечно любые свои типы, какая-нибудь точка своя в геометрии. Ну или ренж таких точек. В общем придётся на каждый случай писать кучу бойлерплейта с возможностью ошибиться оочень много раз
хотя свой формат в строку все равно придется конвертировать перед выводом.
не придётся, вместо того чтобы создавать строку и выводить просто выводишь поля друг за другом, например
если у нового него нет серьезных преимуществ?
они есть, если вам не очевидны минусы printf, то тут ничем не могу помочь
rsashka
27.09.2023 09:45Я ответил ответил на ваше возражение
не поможет, если нужно вывести инт, а он на разных плафтормах разный, то ни шаблоны ни другие функции не помогут, придётся использовать макросы и пихать в формат строку
простейшим примером, как это можно сделать без макросов и разных форматных строк. Поэтому ненужно накручивать то, чего не было в изначально озвученной проблеме.
просто выводишь поля друг за другом, например
С такой же легкостью можно и в printf, поэтому опять возражение мимо.
они есть, если вам не очевидны минусы printf, то тут ничем не могу помочь
Это не вопрос веры или суслика (ты его не видишь, а он есть), а реальных фактов, если вы конечно в теме, но вы так и не смогли привести ни одного (в отличии от @atd).
Kelbon
27.09.2023 09:45простейшим примером, как это можно сделать без макросов и разных форматных строк. Поэтому ненужно накручивать то, чего не было в изначально озвученной проблеме.
придётся выдумывать каждый раз как это сделать
С такой же легкостью можно и в printf, поэтому опять возражение мимо.
нет,
1. придётся много раз вызывать printf2. каждый раз придётся повторять код, нельзя вынести это в форматтер
если вы конечно в теме, но вы так и не смогли привести ни одного
я привёл, вы просто не считаете это проблемами по непонятной причине
вот, перепишите на printf
void log(auto&& x) { std::print("{}", x); }
rsashka
27.09.2023 09:45-2Не нужно выдумывать, достаточно решать текущие задачи не замахиваясь на глобальные.
Сделать свой форматтер, если не хочется повторяться.
va_list args; va_start(args, format); vsnprintf(buffer, LOG_MAX_BUFFER_SIZE, format, args); va_end(args);
Я вижу более простые решения для приведенных вами примеров, поэтому и не считаю их серьезными аргументами для замена проверенного и работающего решения на что-то новое.
вот, перепишите на printf
Не буду. Но не потому, что не хочу или не могу, а не вижу смысла в такой конструкции логирования. Я считаю, что разработчик должен понимать, что он выводит в лог и в каком виде (чтобы это не было 1Гб двоичных данных). И с этой точки зрения printf с проверкой типов аргументов для форматной строки является почти идеальным решением.
Kelbon
27.09.2023 09:45Не буду. Но не потому, что не хочу или не могу,
могли бы сразу сказать, что вам единственное что нужно это хоть как нибудь вывести через 3 костыля int(и не дай бог это int32_t), а остальное не нужно, не имеет смысла и так далее
rsashka
27.09.2023 09:45-1Ни в коем случае.
Наоборот, я очень активно пользуюсь отладочным выводом, причем реализовано это кроссплатформенно и единообразно сразу для всех платформ (Linux, Windows, FreeRTOS на x86, x64, Cortex, GigaDevice) без изменений исходников и возможностью переназвачения вывода на экран, в файл, UART или даже во Flash память микроконтроллера для посмертного CoreDump
atd Автор
27.09.2023 09:45А насчет оверхеда, я специально для embedded использую только printf
Ого, как времена изменились! Раньше сам printf в ембеде считался преступлением, и использовались отдельные форматтеры (иногда даже самописные), чтобы сэкономить 20кб принтфа (ещё и стэка неизвестно сколько он сожрёт). А теперь это «без оверхеда» уже считается? Oo
Но с тем, что в ембеде std::format пока не нужен — соглашусь. Хотя и принтф тоже там не нужен )
rsashka
27.09.2023 09:45Я не знаю, когда и у кого он считался преступлением, так как это один из самых простых и всегда работающих выводов при отладке, который работает даже без программатора. Тем более, можно отключать не нужные форматы вывода (например числа с плавающей запятой).
И более чем странно читать ваши саркастические замечания, но так и не увидеть ответов на действительно важные вопросы.
atd Автор
27.09.2023 09:45+1Я не знаю, когда и у кого он считался преступлением
Во времена pic16f84a.
Ваша основная (и справедливая) претензия — к iostream, но его использовать совсем не обязательно, вы можете сделать (как показано в статье):
char buff[BUFSIZE]; std::format_to_n(buff, BUFSIZE...)
Или вообще взять `format_to` и писать сразу в UARTn->DR.
можно отключать не нужные форматы вывода (например числа с плавающей запятой).
Весь прикол
std::format
в том, что не надо ничего отключать, ненужны форматы сами не вкомпилятся в результирующий бинарник.
kovserg
27.09.2023 09:45Я вот лично не очень понимаю все эти лишние проверки
std::cout << std::format( load_string(ID_HELLO,lang), arg1, arg2 );
И что в таком случае надо проверять?
И как вообще выравнивать строки влево в право, если ширина в юникоде/utf8 очень не очевидная величина.
А что насчет шаблонов текста то же ничего ни фильтров, ни именованных полей?
В общем очень прорывная технология, аж уши закладывает. /s
Kelbon
27.09.2023 09:45И что в таком случае надо проверять?
что строка известна на этапе компиляции для начала
crackedmind
27.09.2023 09:45+1Да, какие-то проверки есть, и даже форматную строку оно проверяет! Но такой простой проверки, как количество аргументов нет.
Это сделано сознательно, так как можно написать
std::format("{1} {1} {0} {0}", 9, 6)
Kelbon
Если вам хочется дополнительно проверить, что количество аргументов совпадает с количеством {}, то сделайте
atd Автор
Спасибо, дельный совет! К сожалению, наличие
std::format_string
я увидел только после обновления студии до самой свежей. А до этого у меня УХ как подгорало...Таким трюком можно полностью избавиться от макросов (почти, если не считать совместимость с несвежими VS и clang-ом)