&&, || и ! объединяют условия, причём && и || вычисляют правый операнд только если он нужен для результата. Этот пропуск — гарантия языка, а не оптимизация компилятора.

Результат сравнения — обычное значение

Сравнение не обязано сразу попадать в if. Это выражение типа bool:

bool less = 5 < 6;
std::cout << std::boolalpha << less << '\n';   // true

std::boolalpha меняет только формат вывода: вместо 1 и 0 печатаются слова. У bool два значения, но объект такого типа не занимает один бит — чаще всего sizeof(bool) равен единице, а точный размер определяет реализация.

Между bool и числами есть преобразования в обе стороны. Ноль даёт false, любое ненулевое — true; обратно true продвигается в 1, false в 0:

true + 5;      // int: 6
10L * false;   // long: 0

== и = похожи только внешне

int x = 5;
bool first = (x == 0);    // false; x по-прежнему 5
bool second = (x = 0);    // false; x теперь 0

Вторая строка допустима и компилируется: сначала присваивание, затем присвоенное значение приводится к bool. Именно поэтому опечатка в условии if (x = 0) молча проходит компиляцию.

Объединение условий

Математическая запись двойного неравенства напрямую не работает:

int x = 42;
std::cout << (0 < x < 10) << '\n';        // true (!)
std::cout << (0 < x && x < 10) << '\n';   // false

Первое выражение разбирается как (0 < x) < 10. Результат первого сравнения — true, после продвижения это единица, а единица меньше десяти. Проверка получилась совсем не та, причём без единого предупреждения.

&& требует истинности обоих условий, || — хотя бы одного, ! переворачивает:

x < 0 || x > 10;   // вне отрезка [0, 10]
!(x == 0);         // то же, что x != 0

Короткое замыкание

Левый операнд вычисляется всегда, правый — только при необходимости. Если слева от && уже false, всё выражение ложно. Если слева от || уже true, всё выражение истинно. В обоих случаях правая часть вообще не вычисляется.

int x = 12;
int y = 0;
 
bool large = y != 0 && x / y > 2;   // false, деления не произошло

Проверка y != 0 защищает деление. Поменять части местами нельзя: в x / y > 2 && y != 0 опасная операция случится до проверки, и получится Неопределённое поведение.

Это не «компилятор может пропустить, если захочет». Порядок вычисления и пропуск правого операнда закреплены семантикой встроенных логических операций.

Тонкости

  • Гарантия относится к встроенным && и ||. Перегруженные версии этих операций (встретятся позже, в своих классах) короткого замыкания не дают.
  • Побитовые & и | вычисляют оба операнда всегда. Замена && на & в защитном условии превращает рабочий код в UB — см. Побитовые операции.
  • Результат && и || имеет тип bool, а не тип операндов.
  • Внутри условия можно объявить переменную — см. Условный оператор if.

Частые ошибки

  • Писать 0 < x < 10 вместо 0 < x && x < 10.
  • Ставить защиту справа от &&, а опасную операцию слева.
  • Заменять && на & «чтобы было короче».
  • Путать = и == в условии.
  • Сравнивать знаковое с беззнаковым и удивляться результату — см. Арифметические преобразования типов.