Ir al contenido

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.

lvalue_refs.cpp
#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(x), 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.

rvalue_refs.cpp
#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.

Error común

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.

lifetime_extension.cpp
#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
}
Puntos clave
  • 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&& con T deducido) 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?