
std::format это большое и мощное дополнение в C ++ 20, которое позволяет нам эффективно форматировать текст в строки. Он добавляет форматирование в стиле Python с безопасностью и простотой использования.
В этой статье будет показано, как реализовать пользовательские форматеры, которые вписываются в эту новую std::formatархитектуру.
Краткое введение в std::format Для просмотра ссылки Войди или Зарегистрируйся
Вот пример Hello World:
Код:
#include <format>
#include <iostream>
#include <chrono>
int main() {
auto ym = std::chrono::year { 2022 } / std::chrono::July;
std::string msg = std::format("{:*^10}\n{:*>10}\nin{}!", "hello", "world", ym);
std::cout << msg;
}
Поиграйте в Для просмотра ссылки Войди
Вывод:
Код:
**hello***
*****world
in2022/Jul!
Как вы можете видеть, у нас есть заполнители аргументов, которые расширяются и форматируются в std::stringобъект. Более того, у нас есть различные Для просмотра ссылки Войди
Существующие форматеры Для просмотра ссылки Войди или Зарегистрируйся
По умолчанию std::formatподдерживаются следующие типы:- char, wchar_t
- строковые типы - включая std::basic_string, std::basic_string_view, массивы символов, строковые литералы
- арифметические типы
- и указатели: void*, const void*и nullptr_t
Когда вы вызываете:
Код:
std::cout << std::format("10 = {}, 42 = {:10}\n", 10, 42);
Вызов создаст два форматера, по одному для каждого аргумента. Они отвечают за синтаксический анализ спецификатора формата и форматирование значения в выходных данных.
Специализации для форматировщиков:
Код:
template<> struct formatter<char, char>;
template<> struct formatter<char, wchar_t>;
template<> struct formatter<wchar_t, wchar_t>;
Для каждого charT- специализации строкового типа.
Код:
template<> struct formatter<charT*, charT>;
template<> struct formatter<const charT*, charT>;
template<size_t N> struct formatter<const charT[N], charT>;
template<class traits, class Allocator>
struct formatter<basic_string<charT, traits, Allocator>, charT>;
template<class traits>
struct formatter<basic_string_view<charT, traits>, charT>;
Для каждого charT, для каждого cv-неквалифицированного арифметического типа ArithmeticT , отличного от char, wchar_t, char8_t, char16_t, или char32_t, специализации:
Код:
template<> struct formatter<ArithmeticT, charT>;
Для каждого charTтипа специализации типа указателя:
Код:
template<> struct formatter<nullptr_t, charT>;
template<> struct formatter<void*, charT>;
template<> struct formatter<const void*, charT>;
Например, если вы хотите напечатать указатель:
Код:
int val = 10;
std::cout << std::format("val = {}, &val = {}\n", val, &val);
Это не сработает, и вы получите ошибку компилятора (не короткую, но, по крайней мере, описательную), которая:
Код:
auto std::make_format_args<std::format_context,int,int*>(const int &,int *const &)'
was being compiled and failed to find the required specializations...
Это потому, что мы пытались печататьint*, но библиотека поддерживает только void*. Мы можем исправить это, написав:
Код:
int val = 10;
std::cout << std::format("val = {}, &val = {}\n", val, static_cast<void*>(&val));
И вывод может быть (MSVC, x64, Debug):
Код:
val = 10, &val = 0xf5e64ff2c4
В {fmt}библиотеке есть даже утилита, но ее нет в стандарте.
Код:
template<typename T> auto fmt::ptr(T p) -> const void*
Хорошо, но как тогда насчет пользовательских типов?
Для потоков вы могли переопределитьoperator <<, и это сработало. Это тоже так простоstd::format?
Давайте посмотрим.
Пользовательские форматеры Для просмотра ссылки Войди или Зарегистрируйся
std::format Основная идея состоит в том, чтобы предоставить пользовательскую специализацию formatterдля вашего типа.Чтобы создать средство форматирования, мы можем использовать следующий код:
Код:
template <>
struct std::formatter<MyType> {
constexpr auto parse(std::format_parse_context& ctx) {
return /* */;
}
auto format(const MyType& obj, std::format_context& ctx) {
return std::format_to(ctx.out(), /* */);
}
};
Выражение | Возвращаемый тип | Требование |
---|---|---|
f.parse(pc) | PC::iterator | Анализирует спецификацию формата ([format.string]) для типа T в диапазоне [pc.begin(), pc.end()) до первого непревзойденного символа. Выдаетformat_error, если не проанализирован весь диапазон или непревзойденный символ не равен }. Примечание: Это позволяет форматировщикам выдавать значимые сообщения об ошибках. Сохраняет проанализированные спецификаторы формата *thisи возвращает итератор после конца проанализированного диапазона. |
f.format(t, fc) | FC::iterator | Форматирует tв соответствии с сохраненными спецификаторами*this, записывает выходные данные fc.out()и возвращает итератор после конца выходного диапазона. Вывод должен зависеть только от t, fc.locale(), и диапазона [pc.begin(), pc.end())от последнего вызова до f.parse(pc). |
Отдельные значения Для просмотра ссылки Войди или Зарегистрируйся
Для начала давайте возьмем простой тип оболочки с одним значением:
Код:
struct Index {
unsigned int id_{ 0 };
};
И тогда мы можем написать следующий форматировщик:
Код:
template <>
struct std::formatter<Index> {
// for debugging only
formatter() { std::cout << "formatter<Index>()\n"; }
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin();
}
auto format(const Index& id, std::format_context& ctx) {
return std::format_to(ctx.out(), "{}", id.id_);
}
};
Пример использования:
Код:
Index id{ 100 };
std::cout << std::format("id {}\n", id);
std::cout << std::format("id duplicated {0} {0}\n", id);
У нас есть следующий вывод:
Код:
formatter<Index>()
id 100
formatter<Index>()
formatter<Index>()
id duplicated 100 100
Как вы можете видеть, даже для дублированного аргумента {0}создаются два форматера, а не один.
parse()Функция принимает контекст и получает спецификацию формата для заданного аргумента.
Например:
Код:
"{0}" // ctx.begin() points to `}`
"{0:d}" // ctx.begin() points to `d`, begin-end is "d}"
"{:hello}" // ctx.begin points to 'h' and begin-end is "hello}"
parse()Функция должна возвращать итератор в закрывающую скобку, поэтому нам нужно найти его или предположить, что он находится в позиции ctx.begin().
В случае {:hello}возврата begin()не будет указывать на }и, таким образом, вы получите некоторую ошибку во время выполнения - будет выдано исключение. Так что будьте осторожны!
Для простого случая с одним значением мы можем положиться на стандартную реализацию и повторно использовать ее:
Код:
template <>
struct std::formatter<Index> : std::formatter<int> {
auto format(const Index& id, std::format_context& ctx) {
return std::formatter<int>::format(id.id_, ctx);
}
};
Теперь наш код будет работать и анализировать стандартные спецификаторы:
Код:
Index id{ 100 };
std::cout << std::format("id {:*<11d}\n", id);
std::cout << std::format("id {:*^11d}\n", id);
вывод:
Код:
id 100********
id ****100****
Несколько значений Для просмотра ссылки Войди или Зарегистрируйся
Как насчет случаев, когда мы хотели бы показать несколько значений:
Код:
struct Color {
uint8_t r{ 0 };
uint8_t g{ 0 };
uint8_t b{ 0 };
};
Чтобы создать средство форматирования, мы можем использовать следующий код:
Код:
template <>
struct std::formatter<Color> {
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin();
}
auto format(const Color& col, std::format_context& ctx) {
return std::format_to(ctx.out(), "({}, {}, {})", col.r, col.g, col.b);
}
};
Это поддерживает только фиксированный формат вывода и никаких дополнительных спецификаторов формата.
Однако мы можем полагаться на предопределенное string_viewсредство форматирования:
Код:
template <>
struct std::formatter<Color> : std::formatter<string_view> {
auto format(const Color& col, std::format_context& ctx) {
std::string temp;
std::format_to(std::back_inserter(temp), "({}, {}, {})",
col.r, col.g, col.b);
return std::formatter<string_view>::format(temp, ctx);
}
};
Нам не нужно реализовывать parse()функцию с помощью приведенного выше кода. Внутри format()мы выводим значения цвета во временный буфер, а затем повторно используем базовый форматировщик для вывода конечной строки.
Аналогично, если ваш объект содержит контейнер значений, вы можете написать следующий код:
Код:
template <>
struct std::formatter<YourType> : std::formatter<string_view> {
auto format(const YourType& obj, std::format_context& ctx) {
std::string temp;
std::format_to(std::back_inserter(temp), "{} - ", obj.GetName());
for (const auto& elem : obj.GetValues())
std::format_to(std::back_inserter(temp), "{}, ", elem);
return std::formatter<string_view>::format(temp, ctx);
}
};
Приведенный выше форматировщик будет печатать obj.GetName(), а затем за ним последуют элементы из obj.GetValues()контейнера. Поскольку мы наследуем от string_view класса formatter, здесь также применяются стандартные спецификаторы формата.
Расширение средства форматирования с parse()помощью функции Для просмотра ссылки Войди или Зарегистрируйся
Но как насчет пользовательской функции синтаксического анализа?Основная идея заключается в том, что мы можем проанализировать строку формата, а затем сохранить в ней некоторое состояние*this, после чего мы можем использовать информацию в вызове format .
Давайте попробуем:
Код:
template <>
struct std::formatter<Color> {
constexpr auto parse(std::format_parse_context& ctx){
auto pos = ctx.begin();
while (pos != ctx.end() && *pos != '}') {
if (*pos == 'h' || *pos == 'H')
isHex_ = true;
++pos;
}
return pos; // expect `}` at this position, otherwise,
// it's error! exception!
}
auto format(const Color& col, std::format_context& ctx) {
if (isHex_) {
uint32_t val = col.r << 16 | col.g << 8 | col.b;
return std::format_to(ctx.out(), "#{:x}", val);
}
return std::format_to(ctx.out(), "({}, {}, {})", col.r, col.g, col.b);
}
bool isHex_{ false };
};
И тест:
Код:
std::cout << std::format("col {}\n", Color{ 100, 200, 255 });
std::cout << std::format("col {:h}\n", Color{ 100, 200, 255 });
вывод:
Код:
col (100, 200, 255)
col #64c8ff
Краткие сведения Для просмотра ссылки Войди или Зарегистрируйся
Чтобы обеспечить поддержку пользовательских типов, и std::format мы должны реализовать специализацию для std::formatter. Этот класс должен предоставлять parse()функции и format(). Первый отвечает за синтаксический анализ спецификатора формата и сохранение дополнительных данных, *thisесли это необходимо. Последняя функция выводит значения в out буфер, предоставляемый контекстом форматирования.Хотя внедрение средства форматирования может быть сложнее operator <<, оно предоставляет множество возможностей и гибкости. Для простых случаев мы также можем полагаться на наследование и повторное использование функциональности из существующих форматеров.
Поиграйте с кодом для этой статьи в Для просмотра ссылки Войди
В Visual Studio 2022 версии 17.2 и Visual Studio 2019 версии 16.11.14 вы можете использовать std:c++20flag, но до этих версий используйте /std:latest(поскольку он все еще находился в стадии разработки). По состоянию на июль 2022 года GCC не реализует эту функцию. Clang 14 имеет экспериментальную внутреннюю реализацию, но она еще не раскрыта.