библиотека — форматирование строкlibrary — string formatting

std::format

Типобезопасное форматирование строк в стиле Python — std::format("{} is {} years old", name, age) вместо спецификаторов printf или стека iostream-манипуляторов.Python-style type-safe string formatting — std::format("{} is {} years old", name, age) instead of printf specifiers or a stack of iostream manipulators.

часто используетсяcommonly used printf — UB при несовпадении типа и спецификатораprintf — UB on a type/specifier mismatch частый вопрос: почему не printf и не iostreamcommon question: why not printf or iostream

Что этоWhat it is

std::format (заголовок <format>) — функция форматирования строк с плейсхолдерами {}, заменяемыми аргументами по порядку, либо по явному индексу {0}/{1} (что позволяет переставлять порядок вывода без изменения порядка аргументов вызова). После двоеточия внутри плейсхолдера идёт спецификатор формата: {:.2f} — 2 знака после запятой, {:>10} — выравнивание по правому краю в поле шириной 10 символов, {:x} — шестнадцатеричное представление. Функция возвращает std::string, ничего не печатая напрямую.std::format (from <format>) is a string-formatting function with {} placeholders substituted by arguments in order, or by an explicit index {0}/{1} (which lets you reorder the output without changing the call's argument order). After a colon inside a placeholder comes a format specifier: {:.2f} — 2 digits after the decimal point, {:>10} — right-aligned in a 10-character field, {:x} — hexadecimal. The function returns a std::string, printing nothing directly.

до C++20 — printf или iostreambefore C++20 — printf or iostream
char buf[64];
snprintf(buf, sizeof(buf), "%s is %d years old", name.c_str(), age);
// name должен быть вручную сконвертирован в C-строку;// name must be manually converted to a C string;
// %d для double — UB в рантайме, компилятор может не поймать// %d for a double — runtime UB, the compiler may not catch it

std::ostringstream oss;
oss << name << " is " << std::setprecision(2) << age << " years old";
// типобезопасно, но многословно, манипуляторы стекируются и// type-safe, but verbose — manipulators stack up and
// не дают декларативно переставить позицию аргумента// don't let you declaratively reorder an argument's position
C++20 — std::formatC++20 — std::format
auto msg = std::format("{} is {} years old", name, age);
// типобезопасно: name — напрямую std::string, без .c_str()// type-safe: name is a std::string directly, no .c_str()

auto swapped = std::format("{1} {0}", "world", "hello");
// "hello world" — позиция вывода задана индексом, без// "hello world" — output position set by index, without
// изменения порядка аргументов вызова// changing the call's argument order

auto price = std::format("{:>10.2f}", 3.14159);
// "      3.14" — 2 знака после запятой, ширина поля 10, вправо// "      3.14" — 2 decimal digits, field width 10, right-aligned

Зачем ввелиWhy it was introduced

Ни один из двух существовавших подходов к форматированию строк не был одновременно типобезопасным, удобным и производительным:Neither of the two existing approaches to string formatting was simultaneously type-safe, convenient, and fast:

01

printf-семейство — не типобезопасно и не работает с пользовательскими типамиThe printf family isn't type-safe and doesn't work with user types

Несовпадение спецификатора формата и реального типа аргумента (например, %d для double) — undefined behavior в рантайме; компилятор ловит это лишь опционально, через -Wformat, и не всегда. Передать пользовательский тип напрямую в printf нельзя вовсе — нужна ручная конвертация в C-строку или число.A mismatch between the format specifier and the argument's actual type (say, %d for a double) is undefined behavior at runtime; the compiler catches this only optionally, via -Wformat, and not always. A user-defined type can't be passed to printf directly at all — it needs manual conversion to a C string or number first.

02

iostream — типобезопасно, но многословно для сложного форматированияiostream is type-safe, but verbose for complex formatting

operator<< сам по себе типобезопасен, но спецификаторы вроде ширины поля или точности задаются отдельными манипуляторами (std::setw, std::setprecision), которые накапливаются в состоянии потока и легко "утекают" на следующий вывод. Декларативно переставить позицию аргумента в шаблоне строки (вывести второй аргумент раньше первого) без изменения порядка вызова operator<< невозможно.operator<< itself is type-safe, but specifiers like field width or precision are set through separate manipulators (std::setw, std::setprecision) that accumulate in the stream's state and easily "leak" into the next output. Declaratively reordering an argument's position in a string template (printing the second argument before the first) without changing the order of operator<< calls is not possible.

03

std::format сочетает типобезопасность с производительностью, близкой к ручному кодуstd::format combines type safety with performance close to hand-written code

std::format даёт типобезопасность printf-подобного вызова (ошибка типа при constexpr-строке формата ловится на этапе компиляции) при производительности не хуже iostream, а зачастую и быстрее — без буферизации через виртуальные вызовы потока. Пользовательские типы поддерживаются через специализацию std::formatter<T>, без превращения в промежуточную C-строку.std::format gives the type safety of a printf-like call (a type mismatch against a constexpr format string is caught at compile time) with performance no worse than iostream, and often better — with no buffering through the stream's virtual calls. User-defined types are supported via a std::formatter<T> specialization, without going through an intermediate C string.

std::format — не изобретение с нуля, а стандартизация сторонней библиотеки {fmt} (Victor Zverovich), которая годами использовалась в продакшене как раз ради решения этой проблемы — та же история, что и у boost::filesystem, ставшего прототипом std::filesystem.std::format isn't invented from scratch — it standardizes the third-party {fmt} library (Victor Zverovich), which had already been used in production for years to solve exactly this problem — the same story as boost::filesystem becoming the prototype for std::filesystem.

Как это работает под капотомHow it works under the hood

Строка формата разбирается на этапе компиляции, если она передана как constexpr-значение (обычный строковый литерал квалифицируется автоматически) — компилятор проверяет число плейсхолдеров и типы аргументов относительно спецификаторов формата ещё до генерации кода. Каждый тип аргумента находит свою реализацию через специализацию шаблона std::formatter<T>, которую можно определить и для собственных типов.The format string is parsed at compile time when it's passed as a constexpr value (an ordinary string literal qualifies automatically) — the compiler checks the placeholder count and argument types against the format specifiers before any code is even generated. Each argument type finds its implementation through a std::formatter<T> template specialization, which can also be defined for user-defined types.

#include <format>

struct Point { int x, y; };

// специализация formatter для собственного типа// a formatter specialization for a user-defined type
template <>
struct std::formatter<Point> : std::formatter<std::string> {
    auto format(const Point& p, auto& ctx) const {
        return std::formatter<std::string>::format(
            std::format("({}, {})", p.x, p.y), ctx);
    }
};

std::string s = std::format("point = {}", Point{1, 2});
// "point = (1, 2)" — без ручной конвертации в C-строку// "point = (1, 2)" — no manual conversion to a C string

auto hex = std::format("{:x}", 255);   // "ff"// "ff"

Если строка формата — не compile-time константа (например, получена в рантайме из конфигурации или пользовательского ввода), std::format в такой вызов не компилируется — для этого случая есть отдельная функция std::vformat, принимающая уже упакованные аргументы через std::make_format_args.If the format string isn't a compile-time constant (say, it comes from configuration or user input at runtime), that call to std::format simply won't compile — for this case there's a separate function, std::vformat, which takes pre-packed arguments via std::make_format_args.

Специализация выше наследуется от std::formatter<std::string> ради краткости, но полноценный кастомный форматтер пишется как отдельная пара методов parse()/format() — parse() разбирает часть спецификатора после двоеточия (например, свои флаги вроде {:short}) и возвращает итератор на позицию, где разбор остановился, а format() пишет результат в выходной итератор контекста. Минимальный скелет, не поддерживающий собственных флагов, просто пропускает разбор спецификатора насквозь:The specialization above inherits from std::formatter<std::string> for brevity, but a full custom formatter is written as a separate pair of methods, parse()/format() — parse() parses the part of the specifier after the colon (say, your own flags like {:short}) and returns an iterator to where parsing stopped, while format() writes the result into the context's output iterator. A minimal skeleton that supports no flags of its own just passes the specifier parsing straight through:

struct Money { long long cents; };

template <>
struct std::formatter<Money> {
    // парсинг спецификатора формата — здесь без своих флагов,// format-specifier parsing — no custom flags here,
    // просто находим позицию закрывающей '}' и останавливаемся на ней// just find the closing '}' and stop right there
    constexpr auto parse(std::format_parse_context& ctx) {
        return ctx.begin();
    }

    // собственно форматирование — пишем в ctx.out(), возвращаем итератор// the actual formatting — write into ctx.out(), return the iterator
    auto format(const Money& m, std::format_context& ctx) const {
        return std::format_to(ctx.out(), "${}.{:02}", m.cents / 100, m.cents % 100);
    }
};

std::string s = std::format("total: {}", Money{4250});  // "total: $42.50"// "total: $42.50"

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Выравнивание и ширина поля для табличного выводаAlignment and field width for tabular output

Вывод таблицы в консоль или лог-файл, где колонки должны быть выровнены — имя по левому краю, число по правому, оба в фиксированной ширине.Printing a table to the console or a log file where columns must line up — the name left-aligned, the number right-aligned, both in a fixed width.

struct Row { std::string_view name; int qty; };
std::vector<Row> rows = {{"apples", 12}, {"bananas", 3}};

for (const auto& r : rows) {
    std::cout << std::format("{:<10}{:>5}\n", r.name, r.qty);
    // "apples         12"// "apples         12"
    // "bananas         3"// "bananas         3"
}
02

Фиксированная точность для денежных суммFixed precision for monetary amounts

Вывод цены или баланса всегда с двумя знаками после запятой, независимо от того, сколько значащих цифр в исходном double — типичное требование финансового или биржевого кода.Printing a price or balance with exactly two decimal digits, regardless of how many significant digits the source double has — a typical requirement in financial or trading code.

double balance = 1234.5;
std::string out = std::format("balance: {:.2f}", balance);
// "balance: 1234.50" — ровно 2 знака, а не "1234.5"// "balance: 1234.50" — exactly 2 digits, not "1234.5"
03

Индексированные аргументы для переиспользования значения в строкеIndexed arguments to reuse a value within the string

Шаблон строки, где один и тот же аргумент должен появиться в нескольких местах — например, повторяющееся имя переменной в сообщении об ошибке — без передачи его в вызов дважды.A string template where the same argument must appear in multiple places — say, a repeated variable name in an error message — without passing it into the call twice.

std::string field = "email";
std::string msg = std::format("field '{0}' is invalid: '{0}' must not be empty", field);
// "field 'email' is invalid: 'email' must not be empty"// "field 'email' is invalid: 'email' must not be empty"
04

Строка лога с несколькими разнотипными значениями за один вызовA log line with several differently-typed values in one call

Типичный вызов логирования события — число (id), строка (статус) и bool (флаг) собираются в одну читаемую строку без ручной конкатенации и без std::to_string на каждое значение.A typical event-logging call — a number (an id), a string (a status), and a bool (a flag) assembled into one readable line without manual concatenation or a std::to_string per value.

int order_id = 4821;
std::string_view status = "filled";
bool is_retry = false;

log.info(std::format("order {} status={} retry={}", order_id, status, is_retry));
// "order 4821 status=filled retry=false" — bool печатается как текст// "order 4821 status=filled retry=false" — bool prints as text

ГраблиGotchas

Строка формата по умолчанию должна быть compile-time константойThe format string must be a compile-time constant by default

std::format ожидает строку формата как std::format_string<Args...> — тип, конструируемый только из constexpr-значения (строкового литерала). Передать напрямую std::string, полученную в рантайме (из файла конфигурации, из аргументов командной строки), тем же способом нельзя — это ошибка компиляции, а не рантайм-проблема. Для динамической строки формата нужен std::vformat(fmt_str, std::make_format_args(args...)) — другая сигнатура, другой путь.std::format expects the format string as a std::format_string<Args...> — a type constructible only from a constexpr value (a string literal). Passing a std::string obtained at runtime (from a config file, from command-line arguments) the same way doesn't work — it's a compile error, not a runtime problem. A dynamic format string needs std::vformat(fmt_str, std::make_format_args(args...)) — a different signature, a different path.

Поддержка компиляторами была неполной первое время после стандартаCompiler support was incomplete for a while after the standard shipped

<format> — одна из тех фич C++20, чью реализацию стандартные библиотеки долго доводили до конца: GCC (libstdc++) получил полноценную поддержку заметно позже релиза стандарта, MSVC и Clang (libc++) двигались быстрее, но с разной степенью полноты по конкретным версиям. Практическая заметка — проверять наличие и полноту <format> в конкретном тулчейне перед тем, как закладывать его в проект; на переходный период многие использовали напрямую библиотеку {fmt}, благо API почти идентичен.<format> is one of those C++20 features whose implementation standard libraries took a while to finish: GCC (libstdc++) got full support noticeably later than the standard's release, MSVC and Clang (libc++) moved faster but with varying completeness across specific versions. The practical note is to check the presence and completeness of <format> in your specific toolchain before relying on it; during the transition period many projects used the {fmt} library directly instead, since its API is nearly identical.

std::format не печатает — он возвращает строкуstd::format doesn't print — it returns a string

std::format сам по себе создаёт и возвращает std::string, не выводя ничего на экран — для прямого вывода в C++20 есть паттерн std::cout << std::format(...), а начиная с C++23 — отдельная функция std::print, форматирующая и выводящая за один вызов без промежуточной строки.std::format by itself builds and returns a std::string, printing nothing to the screen — for direct output in C++20 the pattern is std::cout << std::format(...), and starting with C++23 there's a dedicated std::print function that formats and prints in a single call with no intermediate string.