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.
Desafío
FácilVamos 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 claseDataBufferque 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::stringy un tamañosize_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(oempty destroyedsi 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
noexcepty dejar el objeto de origen en un estado vacío válido (nullptr, tamaño 0, nombre "empty").- Un constructor que reciba un nombre
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:- Un nombre para tu búfer (string)
- Un tamaño para tu búfer (entero)
Crea un
DataBuffercon el nombre y tamaño dados. Luego crea un segundo búfer moviendo el primero usandostd::move(). Después del movimiento, imprime el estado de ambos búferes:Original: [name] size=[size]New: [name] size=[size]
Incluye
<utility>parastd::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 destroyedCon 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 destroyedObserva 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;
}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
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico12Características de C++ moderno
Semántica de movimiento y RvaluesReenvío perfectoExpresiones Lambda en profundidadstd::function y std::bindconstexpr y constevalBindings estructuradosoptional, variant, any