Operadores, Expresiones y Conversiones
Domina la precedencia de operadores, las conversiones aritmeticas, los peligros de mezclar con signo/sin signo y los cuatro casts con nombre de C++.
Expresiones: Los Bloques de Construccion del Computo
Una expresion en C++ es una secuencia de operadores y operandos que computa un valor. Incluso una sentencia simple como x = a + b; contiene multiples sub-expresiones: a, b, a + b, y x = a + b (la asignacion en si misma es una expresion que retorna el valor asignado).
Cada expresion tiene dos propiedades: un tipo y una categoria de valor (lvalue, rvalue, etc.). Entender estas propiedades es esencial porque C++ aplica conversiones implicitas entre tipos, y las reglas para estas conversiones pueden producir resultados sorprendentes si no tienes cuidado.
La precedencia de operadores determina como se agrupan los operadores (cual se enlaza mas fuerte), mientras que la asociatividad determina la direccion de agrupamiento para operadores del mismo nivel de precedencia. Equivocarse en estos produce errores sutiles que compilan sin advertencia.
Sorpresas de Precedencia de Operadores
La precedencia de operadores es una de las fuentes mas comunes de errores. Cuando tengas dudas, usa parentesis para hacer tu intencion explicita:
#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
}Pulsa Ejecutar para compilar y ejecutar este ejemplo.
Conversiones Aritmeticas y Promocion de Enteros
Cuando mezclas tipos en una expresion, C++ aplica conversiones implicitas siguiendo un conjunto de reglas llamado las conversiones aritmeticas habituales:
1. Promocion de enteros: Los tipos mas pequenos que int (char, short, bool) primero se promueven a int (o unsigned int si el valor no cabe en int). Esto significa que char + char produce un int, no un char.
2. Equilibrado: Si los operandos aun difieren en tipo despues de la promocion, el tipo "menor" se convierte al "mayor" siguiendo una jerarquia: int -> unsigned int -> long -> unsigned long -> long long -> unsigned long long -> float -> double -> long double.
3. Interaccion signed/unsigned: Cuando un tipo con signo se encuentra con un tipo sin signo del mismo rango, el valor con signo se convierte a sin signo. Aqui es donde ocurren las conversiones implicitas mas peligrosas.
Mezclar enteros con signo y sin signo es una de las trampas mas comunes y peligrosas en C++. Cuando un valor con signo negativo se convierte implicitamente a sin signo, se envuelve a un numero positivo muy grande (ya que los tipos sin signo usan aritmetica modular).
Esto ocurre frecuentemente al comparar valores int con size_t (que es unsigned) — por ejemplo, al iterar sobre un std::vector con un contador de bucle int y compararlo con vector.size().
Compila con -Wsign-compare (incluido en -Wall) para detectar estos problemas.
- Un int negativo convertido a unsigned se convierte en un numero muy grande (por ejemplo, -1 se convierte en 4294967295 para 32 bits)
- Comparar valores con signo y sin signo convierte el valor con signo a sin signo primero
- vector.size() retorna size_t (unsigned) — comparar con un int negativo da resultados incorrectos
- Usa -Wsign-compare para detectar problemas de comparacion signed/unsigned en tiempo de compilacion
Casts con Nombre de C++ vs Casts Estilo C
C++ proporciona cuatro operadores de cast con nombre que reemplazan el peligroso cast estilo C (Type)expr. Cada uno tiene un proposito especifico y diferentes niveles de seguridad:
#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;
}Pulsa Ejecutar para compilar y ejecutar este ejemplo.
Usa los casts con nombre para comunicar la intencion y obtener la verificacion apropiada del compilador:
- static_cast — Para conversiones bien definidas: conversiones de tipo numerico, upcasts en jerarquias de clases, y de void* a puntero tipado. Este es el cast que usaras con mas frecuencia.
- dynamic_cast — Para downcasting seguro en jerarquias polimorficas. Requiere al menos una funcion virtual en la clase base. Retorna nullptr (para punteros) o lanza std::bad_cast (para referencias) en caso de fallo. Tiene un costo en tiempo de ejecucion.
- const_cast — Solo para interactuar con APIs heredadas que no son const-correctas. Nunca lo uses para modificar un objeto verdaderamente const.
- reinterpret_cast — Para reinterpretacion de bits a bajo nivel. Casi siempre indica codigo especifico de plataforma. Raramente necesario en codigo de aplicacion.
Nunca uses casts estilo C (Type)expr en C++ — ocultan que conversion se esta realizando y evitan las verificaciones de seguridad.
- Usa static_cast para conversiones numericas y upcasts seguros — se verifica en tiempo de compilacion
- Usa dynamic_cast para downcasts seguros en jerarquias polimorficas — se verifica en tiempo de ejecucion
- Usa const_cast solo para interactuar con APIs heredadas que no son const-correctas
- Nunca uses casts estilo C en C++ — ocultan que conversion se aplica
- La precedencia de operadores determina el agrupamiento — cuando tengas dudas, usa parentesis para hacer la intencion explicita
- La evaluacion en cortocircuito en && y || garantiza orden de izquierda a derecha y omite la evaluacion innecesaria
- La promocion de enteros convierte tipos pequenos (char, short, bool) a int antes de la aritmetica
- Mezclar tipos con signo y sin signo convierte el valor con signo a sin signo — un int negativo se convierte en un numero positivo enorme
- Usa los cuatro casts con nombre (static_cast, dynamic_cast, const_cast, reinterpret_cast) en lugar de casts estilo C
- Los casts estilo C son peligrosos porque intentan silenciosamente multiples tipos de cast sin decirte cual se aplico
Quiz — Pon a prueba tus conocimientos
(15 XP)1. Como se evalua realmente la expresion `x & 1 == 0`?
2. Que sucede cuando comparas `-1 < 0u` (donde `0u` es unsigned)?
3. Que cast de C++ deberias usar para downcasting seguro en una jerarquia de clases polimorfica?