C++20 и C++23: что реально дошло до продакшена
Concepts, ranges, std::format, std::expected: разбираем, какие возможности C++20 и C++23 действительно используются в рабочем коде, чем платишь за каждую и где компиляторы ещё догоняют стандарт.

За три года после ратификации C++20 (ISO/IEC 14882:2020) большая часть команд включила -std=c++20 и обнаружила, что реально пишут иначе от силы в пяти местах: концепты вместо SFINAE, ranges вместо голых итераторов, std::format вместо iostream, корутины в асинхронном коде и std::span как замена паре «указатель + длина». Остальное — модули, constexpr-контейнеры, <source_location> — либо ждёт поддержки инструментов, либо не оправдывает переписывания. C++23 (ISO/IEC 14882:2024) добавил в этот короткий список std::expected и std::print. Разберём, что из этого стоит внедрять сейчас, а что подождёт.
Проверяйте поддержку по вашей связке компилятора и стандартной библиотеки: GCC 13/14, Clang 17/18, MSVC 19.3x поддерживают разный объём. Опорная таблица — на cppreference, ниже речь о том, что уже применимо в бою.
Concepts: SFINAE наконец читается
Главная практическая выгода концептов — не новые возможности, а сообщения об ошибках. Раньше шаблон, которому подсунули неподходящий тип, разворачивался в стену текста на сотни строк. С концептом ошибка звучит по-человечески: «ограничение Hashable не удовлетворено».
#include <concepts>
#include <cstddef>
// Тип годится, если для него определён operator< и результат конвертируется в bool
template <typename T>
concept Comparable = requires(const T& a, const T& b) {
{ a < b } -> std::convertible_to<bool>;
};
template <Comparable T>
const T& max_of(const T& a, const T& b) {
return a < b ? b : a;
}
Компилируется на GCC 12+, Clang 15+, MSVC 19.30+. Цена перехода низкая: концепты не ломают ABI и вставляются в существующие шаблоны точечно. Не пытайтесь переписать все шаблоны разом — начните с публичных API библиотек, где понятная ошибка экономит время вызывающему.
Ranges: меньше циклов, но следите за временем жизни
Диапазоны и вьюхи из <ranges> убирают ручные циклы фильтрации-преобразования. Ленивые адаптеры выстраиваются в конвейер:
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums{1, 2, 3, 4, 5, 6};
// Ленивый конвейер: чётные, возведённые в квадрат
auto pipeline = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int value : pipeline)
std::cout << value << ' '; // 4 16 36
}
Тут прячется главная ловушка. Вьюха не владеет данными — она держит ссылку на исходный контейнер. Если контейнер временный, вьюха повиснет на висячей ссылке:
// ОПАСНО: get_data() возвращает временный vector,
// он умрёт до итерации по вьюхе
auto bad = get_data() | std::views::filter(pred);
C++20 частично закрыл это через owning_view для rvalue-контейнеров, но не для всех адаптеров. Правило простое: не сохраняйте вьюху дольше, чем живёт её источник, и не возвращайте вьюху на локальный контейнер из функции. Для владеющей коллекции материализуйте результат — в C++23 для этого есть std::ranges::to<std::vector>() (GCC 14, Clang 17+), в C++20 придётся собирать вектор вручную циклом или через std::ranges::copy.
Ranges делают код короче ровно там, где раньше был явный цикл преобразования. Они не делают его быстрее — накладные расходы на адаптеры компилятор чаще всего срезает, но проверяйте это профилировщиком, а не на веру.
std::format и std::print: forматирование без потоков
std::format (C++20) заменяет и printf с его небезопасностью типов, и iostream с его многословием. Синтаксис — как у Python:
#include <format>
#include <string>
std::string user = "admin";
int attempts = 3;
// Проверка типов на этапе компиляции: если {} не совпадёт с аргументом — ошибка сборки
std::string msg = std::format("Пользователь {} исчерпал {} попытки", user, attempts);
Практическая деталь: реализация std::format появилась в libstdc++ с GCC 13, в libc++ с Clang 17, в MSVC — с 19.29. Если целитесь в старые компиляторы, поведение недоступно, и внешняя библиотека fmt остаётся рабочим вариантом (у неё совместимый API). В C++23 добавили std::print и std::println, которые выводят сразу в поток без промежуточной строки — доступны в GCC 14 и MSVC 19.37+.
std::expected: ошибки без исключений
Самое востребованное новшество C++23 для тех, кто отказывается от исключений в горячем пути или работает в кодовой базе с -fno-exceptions. std::expected<T, E> хранит либо результат, либо ошибку — тип, который годы кочевал по проектам как самописный Result.
#include <expected>
#include <string>
#include <charconv>
enum class ParseError { empty, not_a_number };
std::expected<int, ParseError> parse_int(std::string_view text) {
if (text.empty())
return std::unexpected(ParseError::empty);
int result{};
auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), result);
if (ec != std::errc{})
return std::unexpected(ParseError::not_a_number);
return result;
}
// Вызов: проверяем has_value() или используем value_or
auto value = parse_int("42").value_or(-1);
Доступен с GCC 12, Clang 16, MSVC 19.33. В отличие от исключений, стоимость ошибки предсказуема — это ветка, а не раскрутка стека. Платите за это тем, что ошибку нельзя проигнорировать молча: её надо проверять на каждом уровне или явно пробрасывать через and_then / transform.
Что пока держите на паузе
Не всё из стандарта готово к продакшену одинаково.
| Возможность | Стандарт | Статус на практике |
|---|---|---|
| Concepts | C++20 | Внедряйте: стабильно во всех трёх компиляторах |
| Ranges (базовые вьюхи) | C++20 | Внедряйте, следите за временем жизни |
| std::format | C++20 | Внедряйте с GCC 13 / Clang 17, иначе fmt |
| std::expected | C++23 | Внедряйте, если компилятор свежий |
| Модули (import) | C++20 | Осторожно: сборка и IDE догоняют, тулчейн хрупкий |
| Корутины | C++20 | Мощно, но без готового рантайма писать больно |
| std::mdspan | C++23 | Нишевое, для численных задач |
Модули обещают убрать боль препроцессора и ускорить сборку, но на практике CMake, системы кеширования и IDE поддерживают их неравномерно. Корутины дают синтаксис co_await, но стандарт не поставляет готовых генераторов и задач-планировщиков — в C++23 появился только std::generator (GCC 14). Без внешней библиотеки корутинного рантайма писать асинхронный код на голом стандарте — это собирать промисы вручную.
Что делать дальше
Порядок внедрения, который окупается быстрее всего:
- Включите
-std=c++20и соберите проект — сначала просто убедитесь, что ничего не сломалось. - Замените ручное форматирование на
std::format(или fmt на старых компиляторах) — это даёт безопасность типов почти бесплатно. - Добавьте концепты в публичные шаблонные API — ради читаемых ошибок у тех, кто вызывает ваш код.
- Переведите функции с кодами ошибок на
std::expected, если ваш компилятор поддерживает C++23. - Ranges вводите точечно, в местах с явными циклами фильтрации, и обязательно проверяйте время жизни источников.
Мерьте время сборки до и после: концепты и ranges нагружают компилятор, и на крупной кодовой базе это заметно. Модули и корутины откладывайте до момента, когда ваш тулчейн (компилятор, система сборки, кеш) поддержит их без костылей — проверяйте по релиз-нотам GCC и Clang, а не по дате выхода стандарта.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: C++ compiler support — cppreference, C++23 library features — cppreference
Частые вопросы
У меня GCC 11, часть примеров не собирается. Что доступно?
std::expected заметно медленнее исключений?
Стоит ли переходить на модули ради ускорения сборки?
Почему вьюха из ranges падает или выдаёт мусор?
Можно смешивать код на C++17 и C++20 в одном проекте?
std::print уже можно использовать вместо std::cout?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.