Pull to refresh

Comments 57

UFO just landed and posted this here

Насколько я знаю, нет возможности превратить строковой литерал в параметр типа чтобы распарсить и проверить его в компайл-тайме. Точнее, вроде как можно — но на практике оказывается что нельзя.

Ничего, скоро 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 не было ни малейшего желания.

там немного в другом проблема. constexpr не умеет работать с кучей, а сделать constexpr string конструктор через small string optimization невозможно т.к. в языке нет возможности перегружать функции по длине и/или статичности строкового литерала
В наследии C самая крутая фишка не в том, что значения аргументов вставляются в указанные места строки формата (хотя это тоже, как вы правильно отметили, немаловажно). А в том, что эти значения форматируются в соответствии с указанными спецификациями.

На сколько я вижу, подобной функциональностью обладает только вариант «Обертка над vsnprintf». Но в нём нет контроля соответствия спецификации формата и типа аргумента.
Насколько помню, boost поддерживает спецификацию типа. По крайней мере, когда я писал аналог буста, то мне удалось сделать и спецификаторы типа и порядковые номера одновременно. Делал ради фана, ни в одном проекте так и не использовал библиотеку. Про производительность тоже ничего сказать не могу, скорее всего не сильно быстро.
По поводу контроля над спецификациями формата и типа аргумента:
Добавляем __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) ;
}
то все посыпется…

Похоже маловато константности, может constexpr поможет.

Зачем использовать boost.format, если достаточно давно есть fmt, ранее известная как cppformat.

Как у fmt сейчас с локалями? Поддерживает? Я не слежу, но совсем недавно была печаль.

Я тоже не слежу, кажется толком не поддерживает. Но вроде бы форматирование чисел с группировкой по разрядам использует глобальную локаль для получения разделителя и количества цифр в группах.


Вообще, локали одно из самых тормозных мест в стандартной библиотеке, и в плане компиляции, и времени выполнения. Так что автора cppformat можно понять, что он не хочет с ними связываться.

А чем плох snprintf?
Быстрее и компактней ничего наверно нет, компиляторы выдают предупреждения на несоответствие типов. Нельзя определить точно размер буфера? Да и ненадо, на стеке выделить 256/512/..., а организацию вызовов (интерфейсы) можно организовать так что свободно летающие std::string не требуются. Причем на стеке память выделяется за константное (бесплатное) время а не поиск в хипе свободного блока по хитрому алгоритму с синхронизацией тредов.
Т.е. можно вообще не тратить время на изобретение новых классов, использовать vsnprintf, и выпилить в дизайне необходимость в свободно летающих std::string (часто форматирование строк сугубо задача UI модуля, вот в нем и можно скрыть эти заморочки прокинув стековые строки прямо в native API UI библиотеки). Т.е. тот же printf печатает прямо в стандартный вывод без копирования строк, а некоторые библиотеки дают возможность делать sprintf прямо в UI контрол. Тогда зачем нам временный std::string? Так, построить код ради кода.

Лишь первое, что пришло в голову:


  1. Нельзя использовать нестандартные типы. Особенно раздражает с std::string и string_view;
  2. Если мне не изменяет память — сишный formatstring непортабелен (%d, %ld, %lld). Отсюда вырвиглазные макросы аля PRIi64;
  3. Назначение — далеко не только UI. Еще логирование и отладка как минимум.
UFO just landed and posted this here
Александреску давно возмущался несовершенством мира вообще и форматирования строк в С++ в частности — https://erdani.com/publications/cuj-2005-08.pdf (ссылки на код в статье битые 8-( )
Учитывая что воз и ныне там, кажется, на эту фичу слегка подзабили. Правда я в С++14/17 не вчитывался на эту тему…
Интересная статья, спасибо
В случае обёртки над vsnprintf вместо многочисленных попыток выделить память можно просто спросить у функции, сколько же ей нужно.

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());
}
Лучше так (14 стандарт):
auto strBuf = std::make_unique<char[]>(buf_len + 1);
К сожалению, она не всегда знает, сколько ей нужно. На некоторых платформах всегда возвращается -1.
Могу лишь предположить, что это старые версии ОС с поддержкой C89/C90, где vsnprintf стандартом вообще не определена, или нестандартные библиотеки. С99 и POSIX (понимаю, с 1003.1-2005) требуют возвращать значение. К сожалению, не нашёл требований к данной функции по стандарту C++; скорее всего, тоже требует.

Но на возврат -1 действительно, лучше проверять.
Можно пользоваться asprintf()/vasprintf(), которые есть в любой современной libc, и не заботиться о выделении памяти.
но тогда функция по факту отработает дважды
Рекомендую ознакомится с библиотекой fmt: https://github.com/fmtlib/fmt
По сути она решает все указанные проблемы, имея производительность близкую к 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, поэтому к сожалению, двумя вызовами в общем случае не обойтись

Ткните носом, пожалуйста, на каких именно.
Сейчас точно не помню, кажется на SPARCе со специфическим линуксом
Там же, насколько я помню, vsnprintf портил arglist
Деталей по некоторым причинам полностью раскрыть не могу, но платформа очень экзотическая
Но думаю она не единственная, где такое поведение
Я думаю, специфические линуксы и архитектуры потребуют особого внимания и без snprintf, но я понял, спасибо.
Еще судя по ссылке из статьи в винде с Visual Studio и HP-UX тоже всегда возвращается -1, но я не проверял
Сейчас посмотрел mingw в винде, все ок, -1 не возвращается. Значит такое только в студии
Компилятор Visual Studio корректно возвращает требуемую длину при
snprintf(NULL, 0, format, ...);
За YouCompleteMe спасибо
Спасибо Valloric! Отличный инструмент для С-семейства не только для подсветки ошибок, но и для перехода к объявлениям. Конечно, надо повозиться с настройкой и не забывать для каждого проекта делать отдельную конфигурацию.

Я пользуюсь такой оберткой:


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;
}

Она конечно хуже буста (нет проверки типов, хотя это делает компилятор), и не умеет работать со строками, но все же удобна.

Если std::snprintf из C++11 работает одинаково на всех платформах, то это неплохой вариант, спасибо.

Я не знаю, этот код работает только на линуксе, и вроде кто-то собирал под макось (но я не знаю результат).
http://ru.cppreference.com/w/cpp/io/c/fprintf — но на платформе, где printf() не по стандарту нет надежды, что stdc++ будет соответствовать стандарту.


Еще нужно бы добавить assert на sz >= 0, но если sz = -1, то все равно должно будет упасть на resize().

Да с форматированием в С++ почему то до сих пор, как в статусе из соц.сетей — "все сложно") Даже странно, не ужели никому реально не надо.

стандарт меняется довольно медленно. Сторонние библиотеки всегда можно использовать. Благо, что много годных либ header-only.

Хорошо, что в последнее время за стандарт взялись. Но всё равно, медленно всё проходит…
Более подробно о поведении функций 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");

так должно работать
Если «так должно работать», то зачем вам шаблоны? Пишите явную имплементацию для типа int. Если же делаете шаблонную функцию, то делайте так, чтобы она работала с любыми типами.

Проверка (раскомментируйте код, чтобы заработало как надо)

#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;
}
Если «так должно работать», то зачем вам шаблоны?

Как раз чтобы не писать миллион имплементаций для всех возможных типов
Понял, о чем Вы
Да, это специфика operator<< для строк
Исправлю, спасибо

В целом интересно. Но я чето не понял как С++11-шный вариант автора работает с std стрингами. Там же вылезет «error: no matching function for call to 'to_string(std::basic_string&)'»

Странно, но у меня все работает:


std::string str = "hello";
std::cout << to_string(str) << std::endl;
hello

Вероятно у Вас в коде using namespace std; и вместо шаблонного to_string вызывается std::to_string

Любопытно, что при разработке подобных штук люди всегда ограничиваются форматированием самых простейших типов и никогда не пытаются расширить этот тривиальный набор. Например, добавить плейсхолдер для форматирования IP-адреса, для первого (или второго) элемента std::pair, для автоматической подстановки текста ошибки по её коду. Или сделать крутой навороченный плейсхолдер для удобного вывода массивов. Точнее, для общего случая любых контейнеров/ranges. Или же можно было бы вообще выйти за флажки и добавить плейсхолдер для подстановки слова в указанном падеже и числе. Ну, типа, вот так:


format("счёт пополнен %d %s", 2, ablative("рубль")); // => "счёт пополнен 2 рублями"
Со строками в C++ традиционно всё плохо, к сожалению. Новые костыли оказываются не лучше предыдущих, стандарт распухает, и так снова и снова.
спасибо за этот вариант — пригодился:
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();
}

мне кажется, читаемость/сопровождаемость никак не пострадала… или нет?
UFO just landed and posted this here
спасибо. попробую boost на нашей системе :)

… и всё-таки мой фрагмент кода работает не на всех компиляторах, пришлось выражение усложнить
Sign up to leave a comment.

Articles