Мы мигрировали наш бэкенд с 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 и оценивали влияние на производительность. Результат: код стал чище, шаблонные ошибки — понятнее, а разработчики — счастливее.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье