Перейти к содержимому

Операторы, выражения и преобразования

Преимущество мастер-оператора, арифметические преобразования, опасности подписанного / неподписанного смешивания и четыре литья C++.

Выражения: строительные блоки вычислений

выражение в C++ представляет собой последовательность операторов и операндов, вычисляющих значение. Даже простое утверждение, такое как x = a + b;, содержит несколько подвыражений: a, b, a + b и x = a + b. (само назначение является выражением, возвращающим присвоенное значение).

Каждое выражение имеет два свойства: a type и a value category (lvalue, rvalue и т.д.). Понимание этих свойств важно, потому что C++ применяет неявные преобразования между типами, и правила для этих преобразований могут дать удивительные результаты, если вы не будете осторожны.

Оператор precedence определяет, как операторы сгруппированы (что связывает более плотно), в то время как associativity определяет направление группировки для операторов на том же уровне приоритета. Получение этих ошибок приводит к тонким ошибкам, которые компилируются без предупреждения.

Случайность оператора удивила

Преимущество оператора является одним из наиболее распространенных источников ошибок. Если вы сомневаетесь, используйте скобки, чтобы выразить свое намерение:

precedence.cppc++23
#include <iostream>

int main() {
    // Precedence trap #1: bitwise vs comparison
    int x = 5;
    // This does NOT check if bit 1 is set:
    if (x & 1 == 0) {              // Parsed as: x & (1 == 0) → x & 0 → 0
        std::cout << "Never reached!\n";
    }
    // Correct:
    if ((x & 1) == 0) {            // Parentheses fix the grouping
        std::cout << "Even\n";
    }

    // Precedence trap #2: ternary vs assignment
    int a = 1, b = 2;
    int result = a > b ? a : b = 10;  // Parsed as: (a > b ? a : b) = 10 — ERROR!
    // Correct:
    int correct = (a > b) ? a : b;    // Parenthesize for clarity

    // Precedence trap #3: pointer dereference vs member access
    // (*ptr).member  vs  ptr->member  — both are equivalent
    // *ptr.member    is parsed as *(ptr.member) — usually a compile error

    // Short-circuit evaluation
    int* ptr = nullptr;
    // Safe: the second operand is NOT evaluated if the first is false
    if (ptr != nullptr && *ptr > 0) {
        std::cout << "Value: " << *ptr << '\n';
    }
    // The && and || operators guarantee left-to-right evaluation and short-circuit
    // Overloaded && and || do NOT short-circuit — avoid overloading them
}
Вывод

Нажмите «Запустить», чтобы скомпилировать и выполнить этот пример.

Арифметические преобразования и продвижение целых чисел

Когда вы смешиваете типы в выражении, C++ применяет неявные преобразования в соответствии с набором правил, называемых обычными арифметическими преобразованиями:

1. Целое продвижение : Типы меньше, чем int (char, short, bool) сначала продвигаются к int (или unsigned int, если значение не может соответствовать int). Это означает, что char + char производит int, а не char.

2. Балансирование: Если после продвижения операнды по-прежнему различаются по типу, «меньший» тип преобразуется в «большой» после иерархии: int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double.

3.Подписанное/неподписанное взаимодействие: Когда подписанный тип соответствует неподписанному типу того же ранга, подписанное значение преобразуется в неподписанное. Именно здесь происходят наиболее опасные неявные преобразования.

Подводный камень

Смешивание подписанных и неподписанных целых чисел является одной из наиболее распространенных и опасных ловушек в C++. Когда отрицательное подписанное значение неявно преобразуется в неподписанное, оно оборачивается вокруг очень большого положительного числа (так как неподписанные типы используют модульную арифметику).

Это часто происходит при сравнении значений int с size_t (который является unsigned) — например, при итерации по std::vector с петлевым счетчиком int и сравнении его с vector.size().

Компиляция с -Wsign-compare (входит в -Wall) для устранения этих проблем.

  • Отрицательный инт, преобразованный в неподписанный, становится очень большим числом (например, -1 становится 4294967295 для 32-битных).
  • Сравнение подписанных и неподписанных значений преобразует подписанное значение в неподписанное первое.
  • vector.size() возвращает size t (неподписанный) — сравнение с отрицательным int дает неправильные результаты
  • Использование -Wsign-compare для улавливания подписанных/неподписанных вопросов сравнения во время компиляции

C++ Оригинальное название: Casts vs C-Style Casts

C++ предоставляет четыре названных оператора литья, которые заменяют опасные литья в стиле C (Type)expr. Каждый из них имеет определенную цель и различные уровни безопасности:

casts.cppc++23
#include <iostream>
#include <cstdint>

class Base {
public:
    virtual ~Base() = default;
    virtual void speak() { std::cout << "Base\n"; }
};

class Derived : public Base {
public:
    void speak() override { std::cout << "Derived\n"; }
    void only_in_derived() { std::cout << "Special!\n"; }
};

int main() {
    // static_cast — compile-time checked, for "natural" conversions
    double pi = 3.14159;
    int truncated = static_cast<int>(pi);    // Explicit: you acknowledge the truncation
    std::cout << truncated << '\n';          // 3

    // dynamic_cast — runtime-checked, for safe downcasting in class hierarchies
    Base* base_ptr = new Derived();
    Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
    if (derived_ptr) {                       // Check for success!
        derived_ptr->only_in_derived();       // Safe
    }

    // const_cast — adds or removes const (use sparingly!)
    const int ci = 42;
    // int& mutable_ref = const_cast<int&>(ci);  // Legal but modifying ci is UB!
    // Only valid use: calling a non-const API with a const object you know won't be modified

    // reinterpret_cast — reinterprets the bit pattern (dangerous!)
    std::int64_t address = reinterpret_cast<std::int64_t>(base_ptr);
    std::cout << "Address: " << address << '\n';

    // C-style cast: (int)pi — NEVER use this in C++
    // It tries static_cast, then const_cast, then reinterpret_cast — 
    // you don't know which one was applied!

    delete base_ptr;
}
Вывод

Нажмите «Запустить», чтобы скомпилировать и выполнить этот пример.

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

Используйте названные слепки, чтобы сообщить намерение и получить соответствующую проверку компилятора:

- static_cast Для четко определенных конверсий: конверсии числового типа, апкасты в иерархиях классов и void* для типизированного указателя. Это то, что вы будете использовать чаще всего.
- dynamic_cast Для безопасного понижения в полиморфных иерархиях. Требуется хотя бы одна виртуальная функция в базовом классе. Возвращает nullptr (для указателей) или бросает std::bad_cast (для ссылок) на отказ. Имеет временную стоимость.
- const_cast Только для взаимодействия с устаревшими API, которые не являются правильными. Никогда не используйте его для изменения объекта.
reinterpret_cast — для низкоуровневой переинтерпретации битов. Практически всегда указывается платформенный код. Редко требуется в коде приложения.

Никогда не используйте литья в стиле C (Type)expr в C++ — они скрывают, какая конверсия выполняется и обходят проверки безопасности.

  • Используйте static cast для числовых конверсий и безопасных апкастов — это проверяется во время компиляции
  • Используйте dynamic cast для безопасного нисходящего потока в полиморфных иерархиях — он проверяется во время выполнения
  • Используйте const cast только для взаимодействия с некорректными устаревшими API
  • Никогда не используйте слепки в стиле C в C++ — они скрывают, какая конверсия применяется
Ключевые выводы
  • Преимущество оператора определяет группировку — когда есть сомнения, используйте скобки, чтобы сделать намерение явным.
  • Краткосрочная оценка в & & и | | гарантирует правосторонний порядок и пропускает ненужную оценку
  • Целое продвижение преобразует небольшие типы (char, short, bool) в int перед арифметикой.
  • Смешивание подписанных и неподписанных типов преобразует подписанное значение в неподписанное — отрицательное значение становится огромным положительным числом.
  • Используйте четыре названных каста (static cast, dynamic cast, const cast, reinterpret cast) вместо кастов в стиле C
  • Литья в стиле C опасны, потому что они молча пробуют несколько типов литья, не сообщая вам, какой из них был применен.

Тест — проверьте свои знания

(15 XP)

1. Что такое выражение `x & 1 == 0`?

2. Что происходит, когда вы сравниваете `-1 < 0u` (где `0u` не подписан)?

3. Какой листок C++ следует использовать для безопасного отбрасывания в полиморфной иерархии классов?