Referencias: Lvalue, Rvalue y Trampas
Explora las referencias lvalue y rvalue, comprende las categorias de valor de C++, aprende las reglas de colapso de referencias y las referencias de reenvio, y evita los errores de referencias colgantes.
Referencias Lvalue
Una referencia lvalue (T&) es un alias — otro nombre para un objeto existente. A diferencia de los punteros, las referencias deben ser inicializadas, no pueden ser nulas, y no pueden ser reasignadas para referirse a un objeto diferente.
Las referencias lvalue se enlazan a lvalues — expresiones que tienen identidad (puedes tomar su direccion). Variables, elementos de array y punteros desreferenciados son todos lvalues. Una referencia lvalue no-const (int&) no puede enlazarse a un temporal (rvalue) porque modificar un temporal es casi siempre un error.
Fundamentos de Referencias Lvalue
Las referencias lvalue son el caballo de batalla de C++ — se usan para paso por referencia, sobrecarga de operadores y evitar copias.
#include <iostream>
#include <string>
void append_greeting(std::string& s) {
s += ", welcome!"; // modifies the caller's string
}
int main() {
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20;
std::cout << x << '\n'; // 20 — x was modified through ref
// const reference can bind to a temporary (rvalue)
const std::string& greeting = std::string("Hello");
// The temporary's lifetime is extended to match greeting's scope
std::cout << greeting << '\n'; // "Hello"
// Non-const lvalue reference CANNOT bind to rvalue:
// std::string& bad = std::string("oops"); // COMPILER ERROR
std::string name = "Alice";
append_greeting(name);
std::cout << name << '\n'; // "Alice, welcome!"
}Categorias de Valor en C++
C++ tiene una taxonomia precisa de categorias de valor de expresiones. Comprenderlas es esencial para la semantica de movimiento y el reenvio perfecto (perfect forwarding):
- lvalue — tiene identidad, no puede ser movido implicitamente. Ejemplos: variables, *ptr, arr[i], literales de cadena.
- prvalue (rvalue puro) — no tiene identidad, puede ser movido. Ejemplos: 42, x + y, std::string("temp"), funcion que retorna por valor.
- xvalue (valor expirante) — tiene identidad Y puede ser movido. Ejemplos: std::move(x), static_cast, miembro de un rvalue.
- glvalue (lvalue generalizado) — lvalue o xvalue (tiene identidad).
- rvalue — prvalue o xvalue (puede ser movido).
La idea clave: una variable de referencia rvalue es en si misma un lvalue — tiene un nombre y una direccion. Esta es la fuente de mucha confusion y es precisamente por lo que existe std::forward.
Referencias Rvalue (T&&)
Las referencias rvalue se enlazan a rvalues (temporales y objetos pasados por std::move). Son la base de la semantica de movimiento — te permiten detectar cuando un objeto esta a punto de ser destruido y robar sus recursos.
#include <iostream>
#include <string>
#include <utility> // std::move
void process(const std::string& s) {
std::cout << "lvalue: " << s << '\n';
}
void process(std::string&& s) {
std::cout << "rvalue: " << s << '\n';
// s is an lvalue inside this function (it has a name!)
// To move from s, you'd need std::move(s)
}
int main() {
std::string a = "hello";
process(a); // calls lvalue overload
process(std::string("world")); // calls rvalue overload
process(std::move(a)); // calls rvalue overload — a is now moved-from
// IMPORTANT: a is in a valid but unspecified state
// Only safe operations: assign to it, destroy it
std::cout << "a after move: '" << a << "'\n"; // likely empty
}Colapso de Referencias y Referencias de Reenvio
Cuando las referencias se combinan en plantillas, se aplican las reglas de colapso de referencias (reference collapsing):
- T& & colapsa a T&
- T& && colapsa a T&
- T&& & colapsa a T&
- T&& && colapsa a T&&
La regla es simple: si cualquiera de las referencias es una referencia lvalue, el resultado es una referencia lvalue. Solo && && da &&.
Una referencia de reenvio (forwarding reference) (tambien llamada referencia universal) es T&& donde T es un parametro de plantilla deducido. Puede enlazarse tanto a lvalues como a rvalues. auto&& tambien es una referencia de reenvio. Pero std::vector NO lo es — T ya es conocido. const T&& tampoco es una referencia de reenvio.
Una referencia colgante (dangling reference) se refiere a un objeto destruido. A diferencia de los punteros colgantes, no hay verificacion de nullptr — el codigo usara silenciosamente datos basura o fallara.
Causas comunes:
- Retornar una referencia a una variable local — la variable se destruye cuando la funcion retorna.
- Capturar por referencia en una lambda que sobrevive a la variable referenciada.
- const auto& a un temporal dentro de una expresion — const auto& val = get_map()["key"] es colgante si get_map() retorna por valor, porque el mapa temporal se destruye en el punto y coma.
- Bucle range-based for sobre un temporal — for (auto& x : get_vector()) esta bien (el temporal vive durante el bucle), pero for (auto& x : get_wrapper().get_vector()) puede colgar si el wrapper se destruye.
Extension de Tiempo de Vida con const T&
Una referencia lvalue const puede extender el tiempo de vida de un temporal, pero solo cuando se enlaza directamente. Esto NO funciona a traves de llamadas a funciones o cadenas de acceso a miembros.
#include <iostream>
#include <string>
std::string make_greeting() { return "Hello, World!"; }
const std::string& identity(const std::string& s) { return s; }
int main() {
// SAFE: const ref directly bound to temporary
const std::string& good = make_greeting();
std::cout << good << '\n'; // OK — lifetime extended
// DANGEROUS: lifetime extension does NOT propagate through functions
const std::string& bad = identity(make_greeting());
// bad is DANGLING — the temporary was destroyed after identity() returned
// std::cout << bad << '\n'; // UNDEFINED BEHAVIOR
// SAFE: just use a value
std::string safe = make_greeting(); // move or copy-elision
std::cout << safe << '\n'; // always safe
}- Las referencias lvalue (
T&) son alias — deben ser inicializadas, no pueden ser nulas ni reasignadas const T&puede enlazarse a temporales y extiende su tiempo de vida (pero solo cuando se enlaza directamente)- Las referencias rvalue (
T&&) se enlazan a temporales y habilitan la semantica de movimiento - Una referencia rvalue con nombre es en si misma un lvalue — esto es critico de entender
- Colapso de referencias: cualquier
&en la cadena hace que el resultado sea& - Las referencias de reenvio (
T&&conTdeducido) se enlazan tanto a lvalues como a rvalues
Quiz — Pon a prueba tus conocimientos
(15 XP)1. Dado `void f(std::string&& s)`, cual es la categoria de valor de `s` dentro de `f`?
2. Cual regla de colapso de referencias es correcta?
3. Cuando funciona la extension de tiempo de vida con `const T&` para temporales?