Если вам надоело возиться с iostream‑манипуляторами и опасными printf, самое время освоить std::format в C++20. Он даёт привычный форматный синтаксис, типобезопасность и производительность. Разберёмся на примерах и типичных ошибках, чтобы вы смогли сразу применять std::format в рабочих проектах.

std::format в C++20: современное форматирование строк с примерами и лучшими практиками

Если вам надоело возиться с iostream‑манипуляторами и опасными printf, самое время освоить std::format в C++20. Он даёт привычный форматный синтаксис, типобезопасность и производительность. Разберёмся на примерах и типичных ошибках, чтобы вы смогли сразу применять std::format в рабочих проектах.

Быстрый старт: как подключить и скомпилировать

Поддержка std::format есть в современных компиляторах (GCC 13+, Clang 16+, MSVC 19.3x+). Минимальный пример:

#include <format>
#include <iostream>

int main() {
    std::string name = "Алиса";
    auto s = std::format("Привет, {}!", name);
    std::cout << s << '\n';
}

Компиляция (GCC/Clang):
g++ -std=c++20 main.cpp -O2 -o app

В MSVC достаточно включить стандарт C++20 в настройках проекта.

Синтаксис std::format: место для каждого аргумента

  • Плейсхолдер {} — вставка значения по порядку аргументов.
  • После двоеточия задаётся спецификатор формата: {<spec>}.
  • Внутри можно указывать выравнивание, ширину, заполнитель, систему счисления, точность и знак.
#include <format>
#include <iostream>

int main() {
    // Выравнивание и ширина
    std::cout << std::format("[{:<8}] [:{:^8}] [:{:>8}]\n", 42, 42, 42);
    // Заполнитель (fill)
    std::cout << std::format("{:*^10}\n", "C++"); // ***C++***

    // Числа: ведущие нули, знак, системы счисления
    std::cout << std::format("{:08d}\n", 123);   // 00000123
    std::cout << std::format("{:+d}\n", 5);      // +5
    std::cout << std::format("{:#x}\n", 255);    // 0xff
    std::cout << std::format("{:#b}\n", 10);     // 0b1010

    // Вещественные числа и точность
    std::cout << std::format("{:.2f}\n", 3.14159);  // 3.14
    std::cout << std::format("{:.3e}\n", 12345.0); // 1.235e+04
}

std::format vs printf и iostream

  • Типобезопасность: несовпадение типов ловится на этапе компиляции, если формат — строковый литерал (используется форматный тип format_string).
  • Удобство: один стиль форматирования вместо десятков манипуляторов iostream.
  • Производительность: сравнима с printf, при этом без опасных UB из‑за неверных спецификаторов.

Форматирование времени (chrono)

В C++20 можно красиво форматировать время. Чаще всего достаточно округлить до секунд и задать шаблон:

#include <format>
#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;
    auto now = system_clock::now();
    auto s = std::format("{:%Y-%m-%d %H:%M:%S}", floor<seconds>(now));
    std::cout << s << '\n';
}

Шаблоны формата схожи с strftime: %Y — год, %m — месяц, %d — день, %H:%M:%S — время.

Печать без cout: std::print из C++23

Если у вас C++23, можно печатать напрямую:

#include <print>

int main() {
    std::print("Hello, {}!\n", "world");
}

В C++20 это ещё недоступно — пользуйтесь std::format + std::cout.

Собственный тип: пишем форматтер за 2 минуты

std::format умеет форматировать ваши структуры, если вы напишете специализацию std::formatter. Пример для точки:

#include <format>
#include <iostream>

struct Point { int x; int y; };

template<>
struct std::formatter<Point> {
    // Простой форматтер: игнорируем формат-спеки
    constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }

    template<class FormatContext>
    auto format(const Point& p, FormatContext& ctx) const {
        return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
    }
};

int main() {
    Point p{3, 4};
    std::cout << std::format("P = {}\n", p);
}

Так вы можете описывать читаемый вывод для доменных сущностей и логов, не засоряя код ручной сборкой строк.

Типичные ошибки и как их избежать

  • Несовпадение типов и спецификаторов. Со строковым литералом компилятор укажет на ошибку; с динамической строкой будет выброшено исключение std::format_error в рантайме.
  • Забыли подключить заголовок: нужен <format> (и иногда <chrono> для времени).
  • Старая версия компилятора. Обновитесь до GCC 13+/Clang 16+/актуального MSVC или временно используйте {fmt}.

Если компилятор ещё не поддерживает std::format

Можно подключить библиотеку {fmt}, на которой основан стандартный std::format. API практически совпадает, а переход позже займёт минуты:

#include <fmt/core.h>
#include <fmt/chrono.h>

int main() {
    fmt::print("Привет, {}!\n", "мир");
}

Небольшая памятка по спецификаторам

  • Выравнивание: ":<width" (влево), ":^width" (по центру), ":>width" (вправо). Заполнитель указывается перед выравниванием: ":*^10".
  • Числа: d (10‑я), x/X (16‑я), b (2‑я), префикс # добавляет 0x, 0b. Ведущие нули: 08d. Знак: + для положительных тоже.
  • Вещественные: f (фикс), e (экспонента), g (умно выбирает), точность: .N.
  • Строки и char форматируются без танцев с бубном: просто {} или с выравниванием.

Практические советы

  • Выносите форматные строки в константы — проще переиспользовать и тестировать.
  • Для логирования определяйте форматтеры доменных типов — логи станут короче и понятнее.
  • Не смешивайте старые printf с std::format: держите проект в едином стиле форматирования.
  • Проверяйте поддержку на CI разными компиляторами, чтобы не поймать сюрпризы.

Что дальше изучать

Итоги

std::format в C++20 — удобный, безопасный и мощный инструмент для форматирования строк. Он заменяет связку iostream+манипуляторы и устаревший printf, даёт понятный синтаксис, расширяется под ваши типы и открывает путь к ещё более лаконичному std::print в C++23. Применяйте его в логировании, отчётах, диагностике и пользовательских сообщениях — код станет чище и надёжнее уже сегодня.