C++ Пользовательские типы и std::format из C ++ 20

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,771
Реакции
1,451
Credits
25,339
format_custom.png
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::chrono типы!). Позже мы сможем вывести эту строку в объект stream.

Существующие форматеры Для просмотра ссылки Войди или Зарегистрируйся

По умолчанию std::formatподдерживаются следующие типы:
  • char, wchar_t
  • строковые типы - включая std::basic_string, std::basic_string_view, массивы символов, строковые литералы
  • арифметические типы
  • и указатели: void*, const void*и nullptr_t
Это определено в стандарте by formatter, см. В спецификации Для просмотра ссылки Войди или Зарегистрируйся:

Когда вы вызываете:
Код:
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?
 

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,771
Реакции
1,451
Credits
25,339

Пользовательские форматеры Для просмотра ссылки Войди или Зарегистрируйся

При 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).
Это больше кода, для которого мы привыкли писатьoperator <<, и звучит более сложно, поэтому давайте попробуем расшифровать Стандарт.

Отдельные значения Для просмотра ссылки Войди или Зарегистрируйся

Для начала давайте возьмем простой тип оболочки с одним значением:
Код:
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****
 

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,771
Реакции
1,451
Credits
25,339

Несколько значений Для просмотра ссылки Войди или Зарегистрируйся

Как насчет случаев, когда мы хотели бы показать несколько значений:
Код:
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 имеет экспериментальную внутреннюю реализацию, но она еще не раскрыта.

Ссылки: