Menu
Coddy logo textTech

Semántica de movimiento y Rvalues

Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy — lección 83 de 104.

En C++, cada expresión es un lvalue (tiene una identidad persistente, se puede direccionar) o un rvalue (es temporal, está a punto de ser destruido). Comprender esta distinción desbloquea la semántica de movimiento: una potente optimización que evita copias innecesarias.

Una referencia rvalue, declarada con &&, se vincula específicamente a objetos temporales. Esto te permite "robar" recursos de objetos que están a punto de desaparecer de todos modos:

#include <iostream>
#include <utility>

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : data(new int[s]), size(s) {
        std::cout << "Constructed\n";
    }
    
    // Constructor de movimiento - roba recursos
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;  // Deja el origen en un estado válido
        other.size = 0;
        std::cout << "Moved\n";
    }
    
    ~Buffer() { delete[] data; }
};

int main() {
    Buffer b1(1000);
    Buffer b2(std::move(b1));  // Activa el constructor de movimiento
}

La función std::move en realidad no mueve nada; simplemente convierte un lvalue en una referencia rvalue, indicando que estás dispuesto a ceder los recursos del objeto. El movimiento real ocurre en el constructor de movimiento o en el operador de asignación por movimiento.

La semántica de movimiento mejora drásticamente el rendimiento cuando se trabaja con objetos pesados en recursos, como contenedores o cadenas. En lugar de realizar copias profundas de megabytes de datos, simplemente se transfiere la propiedad del puntero, una operación de tiempo constante independientemente del tamaño.

challenge icon

Desafío

Fácil

Vamos a construir una clase DataBuffer que gestione recursos y demuestre la semántica de movimiento en acción. Verás cómo mover recursos en lugar de copiarlos puede mejorar drásticamente la eficiencia al transferir la propiedad de la memoria asignada dinámicamente.

Organizarás tu código en tres archivos:

  • DataBuffer.h: Define tu clase DataBuffer que gestiona un arreglo de enteros asignado dinámicamente.

    Tu clase debe tener miembros privados para el puntero de datos (int*), el tamaño (size_t) y un nombre (std::string) para ayudar a rastrear qué búfer es cuál durante las operaciones.

    Declara lo siguiente:

    • Un constructor que reciba un nombre std::string y un tamaño size_t, asigne el arreglo e imprima: [name] constructed with size [size]
    • Un constructor de movimiento que reciba una referencia rvalue, robe los recursos e imprima: [name] moved from [source_name] (donde el búfer de destino toma el nombre del origen)
    • Un destructor que imprima [name] destroyed (o empty destroyed si el búfer fue el origen de un movimiento)
    • Un método getSize() que devuelva el tamaño actual
    • Un método getName() que devuelva el nombre del búfer

    Recuerda marcar tu constructor de movimiento como noexcept y dejar el objeto de origen en un estado vacío válido (nullptr, tamaño 0, nombre "empty").

  • DataBuffer.cpp: Implementa todos los métodos declarados en tu cabecera. Cuando se ejecute el destructor, solo elimina los datos si el puntero no es nulo. Incluye <iostream> para la salida.

  • main.cpp: Lee dos entradas:
    1. Un nombre para tu búfer (string)
    2. Un tamaño para tu búfer (entero)

    Crea un DataBuffer con el nombre y tamaño dados. Luego crea un segundo búfer moviendo el primero usando std::move(). Después del movimiento, imprime el estado de ambos búferes:

    • Original: [name] size=[size]
    • New: [name] size=[size]

    Incluye <utility> para std::move.

Por ejemplo, con las entradas Alpha y 100:

Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyed

Con las entradas Buffer y 50:

Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyed

Observa cómo el constructor de movimiento transfiere la propiedad de la memoria asignada sin copiar ningún dato. El búfer original queda en un estado vacío pero válido, y cuando ambos búferes se destruyen al final del programa, solo el que aún posee la memoria la elimina realmente.

Pruébalo tú mismo

#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"

int main() {
    std::string name;
    int size;
    
    std::cin >> name;
    std::cin >> size;
    
    // TODO: Crear un DataBuffer con el nombre y tamaño dados
    
    // TODO: Crear un segundo búfer moviendo el contenido del primero usando std::move()
    
    // TODO: Imprimir el estado de ambos búferes:
    // Original: [name] size=[size]
    // New: [name] size=[size]
    
    return 0;
}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación Orientada a Objetos