std::format — очень полезное (и серьезное) нововведение C++20, позволяющее нам форматировать текст в строки удобным и эффективным образом. Оно привносит в язык форматирование в стиле Python в сочетании с безопасностью и простотой использования. В этой статье я расскажу, как реализовать пользовательские средства форматирования (форматтеры) в соответствии с новой std::format архитектурой. Давайте взглянем на этот Hello World: Вы можете посмотреть этот код в Compiler Explorer. Вывод: Мы здесь видим местозаполнители для аргументов, которые разворачиваются и форматируются в объект std::string. Кроме того, у нас есть различные спецификаторы для управления выводом (тип, длина, точность, заполняющие символы и т. д.). Мы также можем использовать пустой местозаполнитель {}, который даст нам дефолтный вывод конкретного типа (к слову, поддерживается даже std::chrono!). Позже мы можем вывести эту строку в объект-поток. Подробнее о архитектуре и фичах вы можете узнать в этой статье.  По умолчанию std::format поддерживает следующие типы: char, wchar_t; строковые типы, включая std::basic_string, std::basic_string_view, массивы символов и строковые литералы; арифметические типы; указатели: void*, const void* и nullptr_t. Это определено в стандарте formatter, вы можете увидеть это в спецификации : Давайте вызовем: Вызов создаст два форматтера, по одному для каждого аргумента. На них лежит задача распарсить спецификатор формата и отформатировать значение в вывод. Специализации форматтеров: Все специализации для строковых типов и charT: Для каждого charT, а также для каждого cv-неквалифицированного арифметического типа ArithmeticT, не являющегося char, wchar_t, char8_t, char16_t или char32_t, есть специализация: Специализации для типов указателей и charT: Например, если вы хотите вывести указатель: Этот код не будет работать, и вы получите ошибку компилятора (не самую лаконичную, но зато понятную): Это потому, что мы пытались вывести int*, но библиотека поддерживает только void*. Мы можем это обойти, написав: И тогда вывод будет (MSVC, x64, Debug): В библиотеке {fmt} даже реализована такая вспомогательная функция, но в Стандарте, увы, нет. Хорошо, а что насчет пользовательских типов? Для потоков мы можем переопределить оператор <<, и такой код будет работать. Но так же ли это просто для std::format? Давайте разберемся. С std::format основная идея заключается в том, чтобы предоставить пользовательскую специализацию formatter‘а для вашего типа. Чтобы создать форматтер, можно использовать следующий код: Вот основные требования к такому типу функций (из Стандарта): Выражение Тип возврата Требование 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). Тут больше кода, чем нам нужно было написать для оператора <<, и все это звучит чуть более сложно, так что давайте попробуем разобраться с тем, что от нас требует Стандарт. Для начала возьмем простой тип-обертку с одним значением: И для него мы можем написать следующий форматтер: Пример использования: Мы получим следующий вывод: Как видите, даже для дублируемого аргумента {0} создаются два форматтера, а не один. Функция parse() принимает контекст и получает спецификатор формата для данного аргумента. Например: Функция parse() должна вернуть итератор на закрывающую скобку, поэтому нам нужно найти ее или предположить, что она находится в позиции ctx.begin(). В случае {:hello} возврат begin() не будет указывать на } и, таким образом, мы получите ошибку времени выполнения — будет сгенерировано исключение. Так что будьте внимательны! Для простого случая с одним значением мы можем положиться на стандартную реализацию и попросту задействуем этот код: Теперь наш код будет работать, распаршивая стандартные спецификаторы: Вывод: А что насчет случаев, когда мы хотели бы показать несколько значений: Чтобы создать создать форматтер, мы можем написать что-то вроде этого: Этот код поддерживает только фиксированный формат вывода и никаких дополнительных спецификаторов формата. Однако мы можем задействовать уже готовый форматтер string_view: Нам не нужно реализовывать функцию parse() с приведенным выше кодом. Внутри format() мы выводим значения цвета во временный буфер, а затем повторно используем исходный форматтер для вывода окончательной строки. Точно так же, если ваш объект содержит контейнер значений, вы можете написать следующий код: Приведенный выше форматтер выведет obj.GetName(), а затем элементы из obj.GetValues(). Поскольку мы наследуемся от класса string_view форматтера, здесь также применимы стандартные спецификаторы формата. Но как насчет пользовательской функции для парсинга? Основная идея заключается в том, что мы можем распарсить строку формата, а затем сохранить некоторое состояние в *this, после чего мы можем использовать информацию в вызове format. Давайте попробуем: И тест: Вывод: Чтобы обеспечить поддержку пользовательских типов в std::format, мы должны реализовать специализацию для std::formatter. Этот класс должен предоставлять функции parse() и format(). Первая отвечает за парсинг спецификатора формата и сохранение дополнительных данных в *this, если это необходимо, а вторая выводит значения в выходной буфер out, предоставленный контекстом форматирования. Хотя реализация форматтера может быть и сложнее, чем с оператором <<, он дает нам больше возможностей и гибкости. В самых простых случаях мы также можем полагаться на наследование и повторное использование функционала уже существующих форматтеров. Вы можете изучить код, используемый в этой статье в Compiler Explorer. В Visual Studio 2022 версии 17.2 и Visual Studio 2019 версии 16.11.14 вы можете использовать std:c++20, но в более ранних версиях используйте /std:latest (поскольку он все еще находился в разработке). По состоянию на июль 2022 года GCC еще не реализовал эту фичу. Clang 14 имеет экспериментальную внутреннюю реализацию, но она еще не раскрыта. Ссылки Справочное руководство по API — документация fmt 8.1.0 Форматирование определяемых пользователем типов с помощью библиотеки {fmt} — во многих случаях мы можем просто заменить fmt:: на std::, поэтому ознакомление с документацией по fmt не будет лишним. Внеземное руководство по форматированию текста в C++20 — C++ Stories C++20 — Полное руководство от Nicolai M. Josuttis — трюки с наследованием существующих типов и повторным использованием их функционала форматирования. MSVC’s STL Completes /std:c++20 - Блог команды разработчиков C++ Хоть и модно критиковать ООП-подход к разработке кода, он остаётся самым популярным во многих и многих сферах. Поэтому не знать и не уметь использовать данную парадигму разработки для настоящего профессионала просто не вежливо. Приглашаем всех желающих на открытое занятие «ООП глазами C++», на котором поговорим и посмотрим на примерах, как термины ООП реализуются в синтаксисе языка C++. Регистрация на занятие.  Редактор