DMarlon Studio

Lección 8 · 8 de 20

Copiar y mover · C++

Distinga entre copiar y mover en C++: la regla de los cinco, std::move y noexcept, con una imagen que gestiona su propia memoria.

C++ · Copiar y mover en DMarlon Studio: el código del ejemplo y su salida en la terminal

Image reserva sus píxeles con new int[size]() y los libera en el destructor. Una clase que gestiona memoria a mano necesita la regla de los cinco: si escribe el destructor, escriba también el constructor de copia, el de movimiento y las asignaciones de copia y de movimiento. Si no, la copia que genera el compilador copiaría solo el puntero, y las dos imágenes liberarían la misma memoria.

El constructor de copia, Image(const Image& o), hace una copia profunda: reserva memoria nueva y copia cada píxel. Es lo que ocurre en Image backup = photo;, y cuesta copiar dos millones de valores.

El constructor de movimiento, Image(Image&& o), recibe una referencia a rvalue (&&), un objeto que ya no se va a usar. En vez de copiar, se queda con su puntero y deja el original vacío pero válido (nullptr y tamaño 0). `std::move(photo)` no mueve nada por sí solo: solo indica que se puede robar de ahí, y después photo.size() vale 0. Se marca `noexcept` para que std::vector se atreva a mover en lugar de copiar cuando crece.

En album.push_back(Image(640 * 480)) la imagen es un temporal, así que se mueve sin pedirlo; con std::move(backup) se mueve también una variable con nombre. reserve(2) evita que el vector se amplíe a mitad y mueva elementos de más.

La asignación usa el modismo copiar e intercambiar: operator=(Image o) recibe el parámetro por valor (copiado o movido, según lo que se le pase) y lo intercambia con std::swap; el destructor de o libera los píxeles viejos. Una sola función cubre las dos asignaciones. En la práctica, si usa std::vector o punteros inteligentes en lugar de new, no tendrá que escribir ninguna de las cinco: es la regla del cero.

Código · main.cpp

#include <iostream>#include <utility>#include <vector>class Image {                 // owns a raw buffer, so it needs the rule of five    int size_;    int* pixels_;public:    explicit Image(int size) : size_(size), pixels_(new int[size]()) {}    ~Image() { delete[] pixels_; }                                          // 1 destructor    Image(const Image& o) : size_(o.size_), pixels_(new int[o.size_]) {     // 2 copy: deep        for (int i = 0; i < size_; ++i) pixels_[i] = o.pixels_[i];        std::cout << "copy " << size_ << " pixels\n";    }    Image(Image&& o) noexcept : size_(o.size_), pixels_(o.pixels_) {       // 3 move: steal        o.size_ = 0; o.pixels_ = nullptr; std::cout << "move, no copy\n";    }    Image& operator=(Image o) noexcept {      // 4 and 5: copy and move assignment        std::swap(size_, o.size_); std::swap(pixels_, o.pixels_); return *this;    }    int size() const { return size_; }};int main() {    Image photo(1920 * 1080);    Image backup = photo;                  // copy constructor    Image thumb = std::move(photo);        // move constructor: photo is left empty    std::cout << "photo " << photo.size() << ", backup " << backup.size() << ", thumb " << thumb.size() << "\n";    std::vector<Image> album; album.reserve(2);    album.push_back(Image(640 * 480));     // a temporary is moved, not copied    album.push_back(std::move(backup));    backup = thumb;                        // copy assignment: o is a copy of thumb    std::cout << "album " << album.size() << ", backup " << backup.size() << "\n";}

Salida · c++ -std=c++17 main.cpp -o main && ./main

copy 2073600 pixels
move, no copy
photo 0, backup 2073600, thumb 2073600
move, no copy
move, no copy
copy 2073600 pixels
album 2, backup 2073600

Para recordar

  • Copiar duplica los datos; mover traspasa los recursos y deja el original vacío.
  • T(const T&) copia y T(T&&) mueve.
  • std::move(x) convierte x en algo que se puede mover; no lo use después salvo para asignarle.
  • Marque el constructor de movimiento como noexcept.
  • Regla de los cinco si gestiona recursos a mano; regla del cero si no.

Ejercicio

Quite album.reserve(2); y vuelva a ejecutar el programa. Cuente los mensajes move de más que aparecen y explique de dónde salen.