← Назад к вопросам

Хорошо ли перегружать оператор ||

2.0 Middle🔥 71 комментариев
#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Хорошо ли перегружать оператор ||

Перегрузка оператора || в C++ — это очень плохая идея почти во всех случаях. Рассмотрим почему и когда (если вообще когда-то) её можно использовать.

Главная проблема: потеря short-circuit evaluation

Встроенный оператор || имеет гарантированное поведение:

// Стандартный || оператор
bool result = a || b;

// ГАРАНТИЯ: если a == true, то b НИКОГДА не вычисляется
// Это critical для производительности и корректности

При перегрузке теряется эта гарантия:

struct MyBool {
    bool value;
    
    MyBool operator||(const MyBool& other) const {
        std::cout << "Left operand evaluated\n";
        std::cout << "Right operand evaluated\n";  // ВСЕ операнды вычисляются!
        return MyBool{value || other.value};
    }
};

MyBool x{true};
MyBool y{false};

x || y;  // Выведет ОБА message, хотя y никогда не должен был вычисляться

Последствия:

// Проблема 1: Побочные эффекты
bool check_file_exists(const std::string& filename) {
    // Файл существует И открывается файл
    return std::ifstream(filename).is_open();
}

bool check_api_ok() {
    // Делаешь HTTP запрос
    return do_http_request();
}

// С перегруженным ||
if (MyBool{check_file_exists("config.txt")} || MyBool{check_api_ok()}) {
    // Если файл существует, API ВСЁ ЕЩЕ вызывается!
    // Это побочный эффект, который не ожидается
}
// Проблема 2: Производительность
bool expensive_check_1() {
    // Очень дорогая операция (1 сек)
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return false;
}

bool cheap_check_2() {
    // Быстрая операция (1ms)
    return true;
}

// С перегруженным ||
if (MyBool{expensive_check_1()} || MyBool{cheap_check_2()}) {
    // Потратил 1 сек, хотя cheap_check_2() уже true!
    // С нормальным ||: потратил бы 1 мс
}

Почему это техническая ошибка

C++ специально защищает встроенные операторы:

// НЕВОЗМОЖНО перегрузить эти операторы
// Потому что они ДОЛЖНЫ иметь short-circuit behavior:

bool operator||(...);   // ОШИБКА компилятора!
bool operator&&(...);   // ОШИБКА компилятора!

// Функция вызывается всегда с обоими операндами
// Это нарушает семантику языка

Почему это важно в стандарте C++:

Из C++ standard (ISO/IEC 14882):

5.15 Logical OR operator
The || operator groups left-to-right. It returns true if either 
of its operands is nonzero, and false otherwise. Unlike |, the || 
operator guarantees left-to-right evaluation and short-circuit evaluation.

Эта гарантия НАРУШАЕТСЯ при перегрузке оператора как функции.

Примеры неправильной перегрузки

Пример 1: Неправильный паттерн"Nullable/Optional"

template<typename T>
class Optional {
    T* value;
    
public:
    // ПЛОХО!
    Optional operator||(const Optional& other) const {
        return value != nullptr ? *this : other;  // Оба вычисляются!
    }
};

Optional<int> a(5);
Optional<int> b = get_expensive_optional();

Optional<int> result = a || b;  // b ВСЕГДА вычисляется!

Правильный способ: обычный if или функция

Optional<int> a(5);
Optional<int> b;

// Способ 1: обычный if (понятно что происходит)
Optional<int> result;
if (a.has_value()) {
    result = a;
} else {
    result = b;  // b не вычисляется если a.has_value()
}

// Способ 2: функция с понятным именем
result = a.or_else(b);  // Явно говорит что происходит

// Способ 3: обычный || для bool
if (a.has_value() || b.has_value()) {
    // ...
}

Пример 2: Кастомная логика валидации

struct Validator {
    bool is_valid;
    std::string error_message;
    
    // ОЧЕНЬ ПЛОХО!
    Validator operator||(const Validator& other) const {
        if (is_valid) return *this;
        return other;  // other ВСЕГДА вычисляется!
    }
};

// Использование
Validator check1;
Validator check2 = perform_expensive_validation();  // ВЫЧИСЛЯЕТСЯ всегда!

Validator result = check1 || check2;

Правильный способ:

struct Validator {
    bool is_valid;
    std::string error_message;
    
    // Функция с понятным именем
    Validator or_fallback(const std::function<Validator()>& fallback) const {
        if (is_valid) return *this;
        return fallback();  // fallback вычисляется ТОЛЬКО если нужен
    }
};

// Использование
Validator result = check1.or_fallback([] {
    return perform_expensive_validation();
});

Редкие исключения

Всё же есть casos где можно (но НЕ СЛЕДУЕТ) перегружать ||:

Case 1: Bitwise OR для целых чисел

Если ты работаешь с flags, ты можешь использовать | (bitwise), но НЕ ||:

enum class Permissions {
    READ = 1 << 0,
    WRITE = 1 << 1,
    EXECUTE = 1 << 2
};

// ПРАВИЛЬНО: используй | (bitwise OR), а не ||
Permissions flags = Permissions::READ | Permissions::WRITE;

// НЕПРАВИЛЬНО: не используй ||
Permissions flags = Permissions::READ || Permissions::WRITE;  // Ошибка!

Case 2: DSL (Domain Specific Language) с явной документацией

Если ты создаёшь DSL, где явно описано что || не имеет short-circuit:

// В Boost.Spirit (парсер) && и || имеют разную семантику
// Это ДОКУМЕНТИРОВАНО и разработчик ЗНАЕТ что происходит
auto rule = token("if") >> expr() >> token("then") >> statement() ||
            token("unless") >> expr() >> statement();

// Здесь || означает "альтернативный парсер", не logical OR
// ВАЖНО: это ЯВНО в документации

Правильные альтернативы

Что хочешьПравильный способПочему лучше
Logical ORa || b (встроенный)Short-circuit, быстро
Fallback valuea.or_else(b) функцияЯвно, контролируемо
Bitwise ORa | b (bitwise)Правильная семантика
Комбинирование условийif (cond1 || cond2)Понятно
Валидация с fallbackvalidate_1().or_fallback(validate_2)Явно, ленивая вычисление

Лучшие практики

1. Никогда не перегружай логические операторы

// НИКОГДА
bool operator||(const MyType& a, const MyType& b);
bool operator&&(const MyType& a, const MyType& b);

2. Используй понятные имена функций

// ХОРОШО
if (validator.is_valid() || fallback_validator.is_valid()) {
    // ...
}

// Или функция с явным именем
if (validator.passes() || fallback_validator.passes()) {
    // ...
}

3. Если нужен fallback с ленивым вычислением, используй функцию

// ХОРОШО
Optional<T> result = opt1.or_else([&] { return expensive_computation(); });

4. Документируй если ДЕЙСТВИТЕЛЬНО нужна перегрузка

/// WARNING: This operator does NOT have short-circuit evaluation!
/// Both operands will always be evaluated.
/// Use explicit if() statement if you want short-circuit behavior.
Validator operator||(const Validator& a, const Validator& b);

Что скажут на интервью

Вопрос: "Можешь перегрузить оператор || для своего класса?"

Хороший ответ:

"Нет, это плохая идея. Встроенный || имеет гарантированное 
short-circuit поведение — если левый операнд true, правый 
никогда не вычисляется. При перегрузке как обычной функции 
эта гарантия теряется, оба операнда ВСЕГДА вычисляются.

Это может привести к:
1. Побочным эффектам, которые не ожидаются
2. Огромным потерям производительности (дорогие вычисления)
3. Неправильному поведению программы

Вместо этого используй:
- Обычные функции с явными именами (or_else, or_default)
- Обычный if для условий
- Комбинируй встроенный || с методами класса

Итог

Не перегружай оператор ||. Никогда. В 99.9% случаев это ошибка.

Если думаешь что тебе нужно перегружать ||:

  1. Проверь есть ли функция с явным именем
  2. Используй обычный if
  3. Если нужна ленивая вычисление, передай lambda

Этот совет попадёт на code review и будет отклонен. Good code reviewers всегда ловят попытки перегружать логические операторы.