&&, || и ! объединяют условия, причём && и || вычисляют правый
операнд только если он нужен для результата. Этот пропуск — гарантия языка,
а не оптимизация компилятора.
Результат сравнения — обычное значение
Сравнение не обязано сразу попадать в if. Это выражение типа bool:
bool less = 5 < 6;
std::cout << std::boolalpha << less << '\n'; // truestd::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. - Ставить защиту справа от
&&, а опасную операцию слева. - Заменять
&&на&«чтобы было короче». - Путать
=и==в условии. - Сравнивать знаковое с беззнаковым и удивляться результату — см. Арифметические преобразования типов.