Условия
Любая нетривиальная программа делает выбор. Загружать ли файл, если он существует. Вызывать ли метод, если указатель не нулевой. Применять ли алгоритм, если тип известен уже на этапе компиляции. За каждым из этих выборов стоит одна из форм условного ветвления.
В C++ их несколько, и у каждой своя роль:
if / else— универсальное ветвление по любому выражениюswitch— множественный выбор по целочисленному значению- тернарный оператор
?:— выбор значения в одном выражении if constexpr— ветвление, которое происходит не во время выполнения, а при компиляции
Понять, когда использовать каждую форму — это и есть суть темы.
if / else
Базовая конструкция ветвления. Условие может быть любым выражением, приводимым к bool:
int x = 42;
if (x > 0) {
std::cout << "положительное\n";
} else if (x < 0) {
std::cout << "отрицательное\n";
} else {
std::cout << "ноль\n";
}
Фигурные скобки при одном операторе необязательны, но рекомендуются. Без них один лишний оператор при редактировании — и вы получите «висящий else» или случайный логический баг:
// Опасно: добавление строки ломает логику
if (flag)
do_a();
do_b(); // выполняется всегда, несмотря на отступ
// Безопасно
if (flag) {
do_a();
do_b();
}
if с инициализатором (C++17)
Позволяет объявить переменную прямо в условии, ограничив её область видимости блоком if. Это уменьшает загрязнение окружающей области видимости:
if (int result = compute(); result > 0) {
use(result);
} else {
log_error(result);
}
// result недоступен здесь — это намерение, зафиксированное синтаксисом
Особенно удобно при поиске в контейнерах:
if (auto it = map.find(key); it != map.end()) {
process(it->second);
}
То же работает для switch:
switch (auto status = getStatus(); status) {
case Status::Ok: handle_ok(); break;
case Status::Error: handle_error(); break;
default: handle_unknown(); break;
}
switch
switch сравнивает целочисленное (или enum) выражение с набором констант. Для плотного набора значений компилятор может сгенерировать таблицу переходов — это быстрее цепочки if-else:
int day = 3;
switch (day) {
case 1: std::cout << "Пн"; break;
case 2: std::cout << "Вт"; break;
case 3: std::cout << "Ср"; break;
default: std::cout << "Неизвестный день"; break;
}
Fallthrough
Без break выполнение «проваливается» в следующий case. Это источник популярных багов, но иногда именно то, что нужно:
switch (val) {
case 1:
case 2:
std::cout << "1 или 2\n"; // оба case приходят сюда
break;
case 3:
std::cout << "3\n";
[[fallthrough]]; // явная пометка — намеренный провал (C++17)
case 4:
std::cout << "3 или 4\n";
break;
}
Атрибут [[fallthrough]] (C++17) сообщает компилятору и читателю, что провал намеренный, а не забытый break. Без него многие компиляторы с -Wall выдадут предупреждение.
Тернарный оператор
condition ? value_if_true : value_if_false — это выражение, а не оператор. Результат можно присвоить, передать в функцию или вернуть из неё:
int x = 10;
std::string sign = (x >= 0) ? "неотрицательное" : "отрицательное";
int abs_val = (x < 0) ? -x : x;
std::cout << (flag ? "да" : "нет");
Тернарный оператор уместен для лаконичного выбора значения. Избегайте вложенности — два уровня вложения уже делают код нечитаемым:
// Плохо: вложенный тернарник
std::string label = (x > 0) ? "pos" : (x < 0) ? "neg" : "zero";
// Хорошо: if-else явнее
std::string label;
if (x > 0) label = "pos";
else if (x < 0) label = "neg";
else label = "zero";
Short-circuit evaluation
Логические операторы && и || используют ленивое вычисление: правый операнд вычисляется только если это необходимо.
a && b— еслиaложно,bне вычисляетсяa || b— еслиaистинно,bне вычисляется
Это не просто оптимизация — это гарантия стандарта. Порядок вычисления строго слева направо.
Главное практическое применение — нулевая проверка перед разыменованием:
int* ptr = getPointer(); // может вернуть nullptr
// Безопасно: ptr != nullptr вычисляется первым
if (ptr != nullptr && *ptr > 0) {
use(*ptr);
}
// Если бы не short-circuit, *ptr разыменовал бы nullptr → UB
То же работает для || как «ранний выход»:
// find_user() не вызывается, если ptr == nullptr
if (ptr == nullptr || find_user(ptr->id).valid()) {
handle();
}
Short-circuit также защищает от дорогостоящих вычислений в условиях:
// scanDirectory() вызовется только если cache.has(key) вернул false
if (!cache.has(key) && scanDirectory(path).contains(key)) {
cache.insert(key);
}
Осторожно: & и | (побитовые операторы) не являются short-circuit — они вычисляют оба операнда всегда. Не путайте их с && и || в условиях.
Работа с указателями и nullptr
Всегда проверяйте указатель перед разыменованием. Распространённый паттерн — ранний выход:
void process(const std::string* name) {
if (name == nullptr) return;
std::cout << *name;
}
С C++11 используйте nullptr вместо NULL или 0 — это типобезопасно и явно:
int* p = nullptr; // хорошо
int* q = NULL; // устарело, макрос
int* r = 0; // работает, но неявно конвертирует int
Предпочтительнее всего — std::optional или умные указатели, которые убирают саму необходимость проверять «есть ли значение» вручную.
if constexpr (C++17)
Это ветвление на уровне компилятора. В отличие от обычного if, ветвь, условие которой ложно, не компилируется совсем — в ней может быть синтаксически корректный, но семантически невозможный для данного типа код.
Классический случай — шаблонная функция, которая ведёт себя по-разному в зависимости от типа:
template <typename T>
void print(const T& value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Целое: " << value << "\n";
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Вещественное: " << std::fixed << value << "\n";
} else {
std::cout << "Прочее: " << value << "\n"; // потребует operator<<
}
}
print(42); // → ветка is_integral
print(3.14); // → ветка is_floating_point
print("hello"); // → ветка else
Если бы это был обычный if, компилятор попытался бы скомпилировать все ветки для каждой инстанциации — и упал бы, когда value не поддерживает нужные операции.
if constexpr требует C++17. В C++23 появился if consteval — ветвление по признаку «мы внутри константного вычисления», но это уже узкоспециализированная возможность.
Предсказание ветвлений и атрибуты likely/unlikely
Современные процессоры предсказывают, какая ветвь if выполнится, и заранее загружают инструкции. Ошибка предсказания — пайплайн сбрасывается, и это стоит десятков тактов. Если одна ветвь явно редкая, можно подсказать компилятору:
int value = getValue();
if (value == 0) [[unlikely]] {
handle_error(); // случается редко
}
if (value > 0) [[likely]] {
process(value); // основной путь
}
Атрибуты [[likely]] и [[unlikely]] (C++20) влияют на компоновку кода — основной путь остаётся «в линии», редкий путь выносится. Не добавляйте их без профилировки: неверная подсказка хуже отсутствия подсказки.
std::visit как альтернатива if-else на типах
Длинные цепочки if (std::holds_alternative<T>(v)) — признак того, что лучше подойдёт std::visit с паттерн-матчингом на типах std::variant:
std::variant<int, double, std::string> v = 42;
std::visit([](auto&& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << val;
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << val;
} else {
std::cout << "string: " << val;
}
}, v);
std::visit гарантирует, что все альтернативы обработаны — если добавить новый тип в variant и не обновить лямбду, получите ошибку компиляции, а не молчаливый баг.
Частые ошибки
Присваивание вместо сравнения:
int x = 5;
if (x = 10) { ... } // всегда истинно — присваивает 10, а не сравнивает
if (x == 10) { ... } // правильно
Компилятор обычно предупреждает об этом. Некоторые пишут 10 == x («йода-условие»), чтобы сделать ошибку синтаксической. Лучше просто не игнорировать предупреждения.
Забытый break в switch:
switch (state) {
case A: doA(); // провал в B — баг
case B: doB(); break;
}
Обычный if вместо if constexpr в шаблоне:
template <typename T>
void bad(T val) {
if (std::is_integral_v<T>) {
val.to_string(); // ошибка компиляции для T=int, даже если ветка не выполняется
}
}
Обычный if не исключает ветку из компиляции — обе ветки должны быть валидны для любого T. if constexpr решает именно это.
Значение для собеседований
Условия — стандартный материал для проверки понимания языка на всех уровнях: новичков спрашивают про short-circuit, мидлов — про if constexpr в шаблонах, сеньоров — про предсказание ветвлений.
Что проверяют:
- Понимаете ли разницу между
&&и&(и что short-circuit — это гарантия стандарта) - Знаете ли, что
if constexpr— это компиляционное ветвление, а не рантаймовое - Можете ли объяснить fallthrough и
[[fallthrough]] - Понимаете ли
[[likely]]/[[unlikely]]и когда они имеют смысл - Умеете ли использовать
ifс инициализатором для ограничения области видимости переменной
Популярные направления вопросов:
- «Что такое short-circuit evaluation и для чего оно используется?» — ожидают нулевую защиту, не просто определение
- «Чем
if constexprотличается от обычногоifв шаблонах?» - «Что произойдёт, если забыть
breakв switch?» - «Как подсказать компилятору вероятность ветви?»
- «Когда в условии есть
if (auto it = m.find(k); it != m.end())— зачем это, и чем отличается от двух отдельных строк?»
Типичная ошибка: воспринимать if constexpr как «быстрый if» — ускорение времени выполнения. На самом деле это механизм выбора ветки при компиляции, чтобы отброшенная ветка не компилировалась вообще.