Операторы, выражения и преобразования
Преимущество мастер-оператора, арифметические преобразования, опасности подписанного / неподписанного смешивания и четыре литья C++.
Выражения: строительные блоки вычислений
выражение в C++ представляет собой последовательность операторов и операндов, вычисляющих значение. Даже простое утверждение, такое как x = a + b;, содержит несколько подвыражений: a, b, a + b и x = a + b. (само назначение является выражением, возвращающим присвоенное значение).
Каждое выражение имеет два свойства: a type и a value category (lvalue, rvalue и т.д.). Понимание этих свойств важно, потому что C++ применяет неявные преобразования между типами, и правила для этих преобразований могут дать удивительные результаты, если вы не будете осторожны.
Оператор precedence определяет, как операторы сгруппированы (что связывает более плотно), в то время как associativity определяет направление группировки для операторов на том же уровне приоритета. Получение этих ошибок приводит к тонким ошибкам, которые компилируются без предупреждения.
Случайность оператора удивила
Преимущество оператора является одним из наиболее распространенных источников ошибок. Если вы сомневаетесь, используйте скобки, чтобы выразить свое намерение:
#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. Каждый из них имеет определенную цель и различные уровни безопасности:
#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++ следует использовать для безопасного отбрасывания в полиморфной иерархии классов?