Comments 57
Насколько я знаю, нет возможности превратить строковой литерал в параметр типа чтобы распарсить и проверить его в компайл-тайме. Точнее, вроде как можно — но на практике оказывается что нельзя.
Ничего, скоро constexpr разовьются до такой степени, что можно будет и такой код писать. Впрочем, я в этом смысла не вижу, т.к. парсинг в рантайме не должен хоть сколько-нибудь значимо замедлить форматирование строки, а вот геморроя с разбором параметров форматирования это добавит немало.
Пробовал, кстати, сам написать велосипед — чисто для практики с variadic templates, только форматирование хотел сделать в стиле C#: my::string::Format("Hello, {0}! You are {1:0.00}", "Vasja", 12.0f);
Если вкратце: Args...
преобразовывался в std::tuple<StringWriter<Args...>>, по которому затем строился массив (StringWriterBase*)[N]
, к которому и происходило обращение по индексу аргумента в стиле arr[index]->Write(target_stringbuilder, format_string_slice);
Велосипед, как ни странно, работал, но вот реализовывать поддержку всех опций форматирования строк как в .NET не было ни малейшего желания.
На сколько я вижу, подобной функциональностью обладает только вариант «Обертка над vsnprintf». Но в нём нет контроля соответствия спецификации формата и типа аргумента.
Добавляем __attribute__ ((format (printf, N, M)))
после прототипа функции.
Зачем использовать boost.format, если достаточно давно есть fmt, ранее известная как cppformat.
И у любителей compile-time недавно появилось поле для экспериментов: pprintpp
При написании своего велосипеда полезно понимать, чем он лучше остальных. Для сравнения производительности с другими можно сравнить результаты из format-benchmark.
И у любителей compile-time недавно появилось поле для экспериментов: pprintppА это прикольно. Но взглянув на этот фрагмент, можно понять что, если в примере использования:
#include <pprintpp.hpp>
#include <cstdio>
int main()
{
pprintf("{} hello {s}! {}\n", 1, "world", 2.0) ;
}
заменить хотя бы на: #include <pprintpp.hpp>
#include <cstdio>
int main()
{
const char *f = "{} hello {s}! {}\n";
pprintf(f, 1, "world", 2.0) ;
}
то все посыпется… Зачем использовать boost.format, если достаточно давно есть fmt, ранее известная как cppformat.
Как у fmt сейчас с локалями? Поддерживает? Я не слежу, но совсем недавно была печаль.
Я тоже не слежу, кажется толком не поддерживает. Но вроде бы форматирование чисел с группировкой по разрядам использует глобальную локаль для получения разделителя и количества цифр в группах.
Вообще, локали одно из самых тормозных мест в стандартной библиотеке, и в плане компиляции, и времени выполнения. Так что автора cppformat можно понять, что он не хочет с ними связываться.
Локали поддерживаются как в питоне через спецификатор форматирования n
(http://fmtlib.net/3.0.0/syntax.html#format-specification-mini-language).
Форматирование по умолчанию не зависит от локали по причинам указанным в http://fmtlib.net/Text%20Formatting.html#Locale
А чем плох snprintf?
Быстрее и компактней ничего наверно нет, компиляторы выдают предупреждения на несоответствие типов. Нельзя определить точно размер буфера? Да и ненадо, на стеке выделить 256/512/..., а организацию вызовов (интерфейсы) можно организовать так что свободно летающие std::string не требуются. Причем на стеке память выделяется за константное (бесплатное) время а не поиск в хипе свободного блока по хитрому алгоритму с синхронизацией тредов.
Т.е. можно вообще не тратить время на изобретение новых классов, использовать vsnprintf, и выпилить в дизайне необходимость в свободно летающих std::string (часто форматирование строк сугубо задача UI модуля, вот в нем и можно скрыть эти заморочки прокинув стековые строки прямо в native API UI библиотеки). Т.е. тот же printf печатает прямо в стандартный вывод без копирования строк, а некоторые библиотеки дают возможность делать sprintf прямо в UI контрол. Тогда зачем нам временный std::string? Так, построить код ради кода.
Лишь первое, что пришло в голову:
- Нельзя использовать нестандартные типы. Особенно раздражает с std::string и string_view;
- Если мне не изменяет память — сишный formatstring непортабелен (%d, %ld, %lld). Отсюда вырвиглазные макросы аля PRIi64;
- Назначение — далеко не только UI. Еще логирование и отладка как минимум.
Учитывая что воз и ныне там, кажется, на эту фичу слегка подзабили. Правда я в С++14/17 не вчитывался на эту тему…
string string_format(const char* format, ...)
{
va_list args;
va_start(args, format);
int buf_len = vsnprintf(nullptr, 0, format, args);
unique_ptr<char[]> strBuf(new char[buf_len + 1]);
vsnprintf(strBuf.get(), buf_len + 1, format, args);
return string(strBuf.get());
}
auto strBuf = std::make_unique<char[]>(buf_len + 1);
Но на возврат -1 действительно, лучше проверять.
По сути она решает все указанные проблемы, имея производительность близкую к printf.
Наследие C
Строковое форматирование в C осуществляется с помощью семейства функций Xprintf.
…
Но, конечно, не обошлось и без недостатков:
нужно знать заранее сколько памяти потребуется для результирующей строки, что не всегда возможно определить
Можно вызвать snprintf без указания выходного буфера:
int res = snprintf(NULL, 0, "I have %d apples and %d oranges, so I have %d fruits", apples, oranges, apples + oranges);
Тогда res будет содержать необходимое количество байт (без нуль-байта). https://linux.die.net/man/3/snprintf:
Conforming To
The fprintf(), printf(), sprintf(), vprintf(), vfprintf(), and vsprintf() functions conform to C89 and C99. The snprintf() and vsnprintf() functions conform to C99.
Concerning the return value of snprintf(), SUSv2 and C99 contradict each other: when snprintf() is called with size=0 then SUSv2 stipulates an unspecified return value less than 1, while C99 allows str to be NULL in this case, and gives the return value (as always) as the number of characters that would have been written in case the output string has been large enough.
Да, это два прохода, но знать заранее ничего не нужно, а нужно следить за тем, что остальные аргументы идентичны.
соответствие количества и типа аргументов и местозаполнителей не проверяется при передаче параметров извне (как в обертке над vsnprintf, реализованной ниже), что может привести к ошибкам при выполнении программы
Комплятор gcc точно выдает предупреждение, если выставлена опция -Wformat (включена в -Wall). https://linux.die.net/man/1/gcc:
-Wformat
Check calls to "printf" and "scanf", etc., to make sure that the arguments supplied have types appropriate to the format string specified, and that the conversions specified in the format string make sense. This includes standard functions, and others specified by format attributes, in the "printf", "scanf", "strftime"...
YouCompleteMe, работающий через libclang, также подсвечивает такие места. Но там есть пара оговорок, посмотрите, пожалуйста.
Да, это два прохода, но знать заранее ничего не нужно, а нужно следить за тем, что остальные аргументы идентичны.На некоторых платформах Xprintf() всегда возвращают -1, поэтому к сожалению, двумя вызовами в общем случае не обойтись
Комплятор gcc точно выдает предупреждение, если выставлена опция -Wformat (включена в -Wall).Да, но только при непосредственном вызове. При передаче параметров извне предупреждения не будет:
printf("%d %d %d\n", 1, 2); // здесь есть warning
std::cout << format("%d %d %d", 1, 2) << std::endl; // а здесь уже нет
На некоторых платформах Xprintf() всегда возвращают -1, поэтому к сожалению, двумя вызовами в общем случае не обойтись
Ткните носом, пожалуйста, на каких именно.
Там же, насколько я помню, vsnprintf портил arglist
Деталей по некоторым причинам полностью раскрыть не могу, но платформа очень экзотическая
Но думаю она не единственная, где такое поведение
del
Я пользуюсь такой оберткой:
template<typename ... Args>
std::string format(const std::string &fmt, Args ... args)
{
// C++11 specify that string store elements continously
std::string ret;
auto sz = std::snprintf(nullptr, 0, fmt.c_str(), args...);
ret.reserve(sz + 1); ret.resize(sz); // to be sure there have room for \0
std::snprintf(&ret.front(), ret.capacity() + 1, fmt.c_str(), args...);
return ret;
}
Она конечно хуже буста (нет проверки типов, хотя это делает компилятор), и не умеет работать со строками, но все же удобна.
Я не знаю, этот код работает только на линуксе, и вроде кто-то собирал под макось (но я не знаю результат).
http://ru.cppreference.com/w/cpp/io/c/fprintf — но на платформе, где printf() не по стандарту нет надежды, что stdc++ будет соответствовать стандарту.
Еще нужно бы добавить assert на sz >= 0, но если sz = -1, то все равно должно будет упасть на resize().
Да с форматированием в С++ почему то до сих пор, как в статусе из соц.сетей — "все сложно") Даже странно, не ужели никому реально не надо.
Более подробно о поведении функций Xprintf на различных платформах можно почитать здесь.
Упомянутое различие для случая с Windows задокументировано в MSDN:
The snprintf function always stores a terminating null character, truncating the output if necessary. The _snprintf family of functions only appends a terminating null character if the formatted string length is strictly less than count characters.
Честно говоря я никогда не понимал и не разбирался зачем майкрософт ввел ещё серию функций _snprintf_s. Что в них более безопасного?
У вас незначительная логическая ошибка в функции from_string. Для неё стоит сделать явную специализацию для типа std::string. Код, который не будет работать:
auto x = from_string("1 2"); // x == "1";
auto x = from_string<int>("1 2");
так должно работать
Проверка (раскомментируйте код, чтобы заработало как надо)
#include <iostream>
#include <string>
#include <sstream>
template<typename T>
T from_string(const std::string &str)
{
std::stringstream ss(str);
T t;
ss >> t;
return t;
}
/*
template<>
std::string from_string(const std::string &str)
{
return str;
}
*/
int main()
{
auto x = from_string<std::string>("1 2");
std::cout << x; // Вывод "1" вместо "1 2"
return 0;
}
и по скорости и по надежности прям то что надо и c#/python стиль и старый добрый C стиль
Любопытно, что при разработке подобных штук люди всегда ограничиваются форматированием самых простейших типов и никогда не пытаются расширить этот тривиальный набор. Например, добавить плейсхолдер для форматирования IP-адреса, для первого (или второго) элемента std::pair, для автоматической подстановки текста ошибки по её коду. Или сделать крутой навороченный плейсхолдер для удобного вывода массивов. Точнее, для общего случая любых контейнеров/ranges. Или же можно было бы вообще выйти за флажки и добавить плейсхолдер для подстановки слова в указанном падеже и числе. Ну, типа, вот так:
format("счёт пополнен %d %s", 2, ablative("рубль")); // => "счёт пополнен 2 рублями"
template<typename T> std::string to_string(const T &t)
{
std::stringstream ss;
ss << t;
return ss.str();
}
… правда я покороче записал:
template<typename T> std::string to_string(const T &t)
{
return (std::stringstream()<<t).str();
}
мне кажется, читаемость/сопровождаемость никак не пострадала… или нет?
О строковом форматировании в современном C++