Загрузка
Все статьи

C++17/20: structured bindings, concepts и ranges — что реально используется в продакшене

Честный разбор фич C++17 и C++20: что мы взяли в проект, что пропустили и почему. Before/after примеры и компромиссы миграции.

Мы мигрировали наш бэкенд с C++14 на C++20 за четыре месяца. Не скажу, что это было легко — но результат того стоил. При этом далеко не все фичи оказались полезны в реальном коде. Рассказываю, что прижилось, а что осталось на бумаге.

Structured Bindings — лучшая мелочь C++17

Structured bindings — это, на мой взгляд, самая практичная мелкая фича за последние годы. Она не меняет архитектуру, но делает код чище. Мы используем её постоянно — при возврате нескольких значений, при итерации по мапам, при распаковке tuple'ов.

До C++17 мы писали так:

// BEFORE: C++14 — грязно и многословно
auto result = cache_.insert({key, value});
bool inserted = result.second;
auto& stored_value = result.first->second;

// Итерация по map — что за first и second?
for (const auto& pair : config_) {
    process(pair.first, pair.second); // какой first, какой second?
}

После:

// AFTER: C++17 — сразу понятно, что есть что
auto [iter, inserted] = cache_.insert({key, value});
auto& stored_value = iter->second;

for (const auto& [section, params] : config_) {
    process(section, params); // читается как английский
}

// Распаковка возвращаемого значения
auto [status, body] = http_client.get("/api/health");
if (status == 200) {
    parse(body);
}

Мы приняли structured bindings с первого дня миграции и ни разу не пожалели. Единственный нюанс: нельзя захватить structured binding по ссылке в лямбде напрямую — нужно захватывать весь объект. Это нас пару раз кусало.

Concepts — SFINAE больше не нужен

Concepts заменили SFINAE во всех наших шаблонных функциях. Разница в читаемости — колоссальная. Раньше шаблонные ошибки были длиной в страницу, теперь компилятор прямо говорит: «требуется Container, а ты передал int».

// BEFORE: C++14 SFINAE — что здесь происходит?
template >>
T safe_divide(T a, T b) {
    if (b == 0) throw std::invalid_argument("division by zero");
    return a / b;
}

// AFTER: C++20 concepts — понятно даже джуниору
template 
T safe_divide(T a, T b) {
    if (b == 0) throw std::invalid_argument("division by zero");
    return a / b;
}

// Определяем свои concept'ы
template
concept Hashable = requires(T t) {
    { std::hash{}(t) } -> std::convertible_to;
};

template
size_t compute_hash(const T& value) {
    return std::hash{}(value);
}

Мы определили около 15 concept'ов для нашего домена: Serializable, RequestHandler, MessageProducer. Это не только улучшает сообщения об ошибках, но и служит документацией — по сигнатуре шаблона сразу понятно, какие требования к типу.

Ranges — красиво, но с оговорками

Ranges — самая «хипстерская» фича C++20, и мы приняли её далеко не сразу. Причина проста: отладка lazy views в проде — это боль. Вы не можете просто поставить breakpoint внутри цепочки | transform | filter | take и увидеть промежуточные значения.

Но для read-only преобразований данных ranges оказались отличными:

#include 
#include 
#include 
#include 

// BEFORE: императивный стиль
std::vector active_emails(const std::vector& users) {
    std::vector result;
    for (const auto& u : users) {
        if (u.is_active && !u.email.empty()) {
            result.push_back(u.email);
        }
    }
    return result;
}

// AFTER: declarative с ranges
std::vector active_emails(const std::vector& users) {
    return users
        | std::views::filter([](const User& u) { return u.is_active; })
        | std::views::filter([](const User& u) { return !u.email.empty(); })
        | std::views::transform([](const User& u) { return u.email; })
        | std::ranges::to>();
}

Мы используем ranges для логики преобразования данных, но избегаем их в hot path — из-за невозможности легко профилировать отдельные этапы цепочки. Также std::ranges::to появился только в C++23, и нам пришлось написать свой аналог для C++20.

Что мы пропустили

Modules. Звучит круто, но поддержка компиляторов и сборочных систем в 2026 году всё ещё сырая. CMake с modules — это боль. Мы подождём.

std::format. Отличная замена printf и stringstream, но мы уже использовали fmtlib, и миграция на std::format не даёт практической выгоды.

Coroutines. Я писал про них отдельно — мы используем, но через фреймворк, а не голый стандартный механизм.

Советы по миграции

Мигрируйте поэтапно. Сначала включите C++17, замените auto + .first/.second на structured bindings — это безопасно и не ломает ABI. Потом включайте C++20 и добавляйте concepts в новые шаблонные функции, не трогая старый код. Ranges вводите постепенно, только там, где читаемость реально улучшается. И обязательно обновите компилятор — GCC 13 и Clang 17 имеют хорошую поддержку C++20, а GCC 14 и Clang 18 уже покрывают большую часть C++23.

Наша миграция заняла четыре месяца, но мы не спешили — каждую фичу тестировали на CI и оценивали влияние на производительность. Результат: код стал чище, шаблонные ошибки — понятнее, а разработчики — счастливее.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

CMake: модульная архитектура для большого C++ проекта — без ad-hoc include путей

Как мы реорганизовали CMake-структуру проекта на 15+ модулей, избавились от хаотичных include_directories и перешли на современный CMake с target-based зависимостями.

20 июня 2026 10 мин

Kotlin + C++: реальный кейс — видеопроцессинг на сервере через JNI

Как мы построили сервер видеотранскодинга на Kotlin REST API + C++ FFmpeg через JNI. Прокачка JNI_OnLoad, передача фреймов через DirectByteBuffer, обработка ошибок и бенчмарки.

19 июня 2026 13 мин

C++: потоки и потокобезопасность — мьютексы, atomics и deadlock-ы на практике

Разбираем многопоточность в C++ на реальных примерах из продакшена: мьютексы, lock_guard, unique_lock, std::atomic, deadlock-ы и как их избежать. Сравнение с корутинами Kotlin.

18 июня 2026 11 мин