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

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,741
Реакции
1,448
Credits
25,126
фронт.jpg
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?
Давайте посмотрим.

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

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****

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

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