ядро языка — синтаксисcore language — syntax

if/switch с инициализациейif/switch with initializer

Переменная, объявленная прямо в условии if/switch, видна в теле и во всех ветках, но не утекает за пределы конструкции — if (auto it = m.find(key); it != m.end()).A variable declared right in the condition of if/switch is visible in the body and every branch, but doesn't leak past the construct — if (auto it = m.find(key); it != m.end()).

часто используетсяcommonly used простой синтаксисsimple syntax

Что этоWhat it is

C++17 разрешает вставить инициализирующий стейтмент перед условием в if и перед селектором в switch: if (init; cond) и switch (init; cond). Переменная, объявленная в init, попадает в собственный скоуп, который охватывает и cond, и тело if, и ветку else (или все case-метки в switch) — но перестаёт существовать сразу после конструкции целиком.C++17 allows inserting an init-statement before the condition in if and before the selector in switch: if (init; cond) and switch (init; cond). A variable declared in init gets a scope of its own that covers cond, the if body, and the else branch (or all case labels in a switch) — but stops existing right after the whole construct.

до C++17 — переменная объявляется строкой раньшеbefore C++17 — the variable is declared a line earlier
auto it = m.find("key");  // остаётся видна после if// stays visible after the if
if (it != m.end()) {
    use(it);
}
// it всё ещё в скоупе тут — мусорит пространство имён// it is still in scope here — clutters the namespace
do_something_unrelated(it);  // случайно доступно, хотя не должно быть// accidentally accessible, though it shouldn't be
C++17 — init прямо в ifC++17 — init right in the if
if (auto it = m.find("key"); it != m.end()) {
    use(it);
}
// it здесь уже не существует — компилятор не даст обратиться// it no longer exists here — the compiler won't allow access

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

Проблема была не в отсутствии возможности — { auto it = ...; if (...) {...} } работал всегда. Проблема в эргономике и в самой частой ошибке C++-кода: скоупе, который живёт дольше, чем нужно.The problem wasn't a missing capability — { auto it = ...; if (...) {...} } always worked. The problem was ergonomics, and one of the most common mistakes in C++ code: a scope that lives longer than it should.

01

Временные переменные засоряли окружающий скоупTemporary variables cluttered the surrounding scope

Переменная вроде it, нужная только для одной проверки, объявленная строкой раньше, оставалась доступна во всём остальном теле функции — читатель кода не мог по объявлению понять, что она предназначена только для одной проверки и её нельзя переиспользовать дальше.A variable like it, needed only for one check but declared a line earlier, stayed accessible throughout the rest of the function body — a reader couldn't tell from the declaration that it was meant for a single check and shouldn't be reused later.

02

Идиома «if с find» была многословной и легко ошибочнойThe "if with find" idiom was verbose and error-prone

Паттерн «получить значение, проверить успешность, использовать значение» — find, try_lock, парсинг — встречается постоянно. До C++17 он всегда требовал минимум двух строк и отдельного объявления переменной вне if, что провоцировало забыть про очистку/переиспользование этой переменной ниже по коду.The "get a value, check success, use the value" pattern — find, try_lock, parsing — comes up constantly. Before C++17 it always required at least two lines and a separate variable declaration outside the if, which invited forgetting to clean up or reuse that variable further down.

03

Требовался лишний блок { } ради ограничения времени жизниAn extra { } block was needed just to bound the lifetime

Единственный способ по-настоящему ограничить видимость переменной старым синтаксисом — обернуть весь код в дополнительные фигурные скобки, что добавляло уровень вложенности ради чисто косметической цели.The only way to genuinely limit a variable's visibility with the old syntax was to wrap everything in an extra pair of braces, which added a level of nesting for a purely cosmetic goal.

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

Формально if (init; cond) body1 else body2 — это просто синтаксический сахар, эквивалентный оборачиванию всей конструкции в блок:Formally, if (init; cond) body1 else body2 is just syntactic sugar equivalent to wrapping the whole construct in a block:

if (auto it = m.find(key); it != m.end()) {
    use(it);
} else {
    log_missing(it);  // it виден и здесь, в else// it is visible here too, in else
}
// концептуально эквивалентно:// conceptually equivalent to:
{
    auto it = m.find(key);
    if (it != m.end()) {
        use(it);
    } else {
        log_missing(it);
    }
}  // it выходит из скоупа здесь// it goes out of scope here

Для switch действует та же схема — init исполняется один раз перед вычислением селектора, и объявленная в нём переменная видна во всех case-метках:The same scheme applies to switch — init executes once before the selector is evaluated, and any variable declared in it is visible across all case labels:

switch (auto code = compute_status(); code) {
    case Status::ok:
        handle_ok(code);
        break;
    case Status::retry:
        handle_retry(code);
        break;
    default:
        handle_error(code);
}
// code вышел из скоупа сразу после switch// code went out of scope right after the switch

Можно также сочетать init со structured binding, что даёт особенно компактную идиому для контейнеров с find:The init can also be combined with a structured binding, giving a particularly compact idiom for containers with find:

if (auto [it, inserted] = m.insert({key, value}); inserted) {
    log_new_entry(it);
}

ГраблиGotchas

Переменная из init видна и внутри else — это иногда неожиданноThe init variable is visible inside else too — sometimes unexpectedly

Легко подумать, что init относится только к телу if, но на самом деле его скоуп охватывает всю конструкцию целиком, включая else. Это обычно удобно (см. пример с log_missing выше), но при быстром чтении кода можно не заметить, что переменная из «успешной» ветки на самом деле доступна и в «неуспешной».It's easy to assume init belongs only to the if body, but its scope actually covers the entire construct, including else. This is usually convenient (see the log_missing example above), but a quick read of the code can miss that a variable from the "success" branch is in fact available in the "failure" branch too.

switch-init и constexpr case-метки — независимые вещиswitch-init and constexpr case labels are unrelated concerns

Частый вопрос — можно ли использовать переменную из init в качестве значения case-метки. Нет: метки case в любом случае обязаны быть константными выражениями времени компиляции, и это требование ничего не меняет с появлением switch-init — init лишь готовит значение для селектора, а не для меток.A common question is whether a variable from init can be used as a case label's value. It can't: case labels are always required to be compile-time constant expressions, and that requirement is unchanged by switch-init — init only prepares the value for the selector, not for the labels.

Не заменяет обычную инициализацию переменных, которые нужны после конструкцииDoesn't replace ordinary initialization for variables needed after the construct

Если значение нужно и после if/switch, синтаксис с init не подходит — переменная всё равно исчезнет в конце конструкции. В этом случае по-прежнему верно объявлять переменную обычным способом до if.If the value is still needed after the if/switch, the init syntax doesn't fit — the variable will still disappear at the end of the construct. In that case it's still correct to declare the variable the ordinary way before the if.