Semánticas de copia y movimiento (I)

Última actualización de la serie: 11 de octubre de 2020

Artículos de la serie:
  1. Introducción
  2. Una plantilla base para el contenedor Dynarray<>
  3. Primeros constructores para la plantilla Dynarray<>
  4. Técnica copy-and-swap
  5. Semántica de movimiento
  6. Últimos pasos. Sentencias de control for basadas en rango
 

Introducción


En nuestra primera serie de posts, destinada a la implementación de una clase genérica Complex<> capaz de representar números complejos, se omitieron deliberadamente los detalles relacionados con la inicialización de objetos a partir de otros ya existentes. A modo de ejemplo, en el código siguiente se crea el número complejo y como copia exacta de x:

   auto x = Complex<double>{1.0, 2.0},  // x = (1,2)         y = x;  // y = (1,2); equivalente a: auto y = Complex<double>{x};

Tampoco se hizo mención a la asignación de objetos. Consideremos el código siguiente, en el que x es ahora un objeto ya existente cuyo valor original es eliminado para almacenar una copia exacta de y:

   auto x = Complex<double>{};          // x = (0,0)    auto y = Complex<double>{1.0, 2.0};  // y = (1,2)    x = y; // x es ahora una copia independiente de y, x = (1,2)

Aun cuando no se hizo mención explícita de tales operaciones, éstas son ciertamente posibles con cualquier instancia de la plantilla Complex<> diseñada en posts anteriores. Ello se debe a que el compilador define implícitamente dos funciones miembro inline public especiales para la clase: 

(a) Un constructor copia, que permite inicializar un objeto copiando la representación de otro dado (véase el primer bloque de código de ejemplo de este artículo):

   template<typename T> inline constexpr    Complex<T>::Complex(Complex<T> const&) noexcept;

(b) Un operador de asignación copia, que permite copiar la representación de un objeto dado en otro ya existente (véase el segundo bloque de ejemplo):

   template<typename T> inline constexpr    auto Complex<T>::operator=(Complex<T> const&) noexcept -> Complex<T>&;

Observemos, en este último caso, cómo el valor retornado es una referencia al propio objeto con el fin de permitir la concatenación de asignaciones del tipo x = y = z (equivalente a x.operator=(y.operator=(z))).

Conviene remarcar en este punto que, si bien la sintaxis puede confundir al programador principiante en C++, auto y = x implica una inicialización, invocándose al constructor copia y no al operador de asignación. En ese sentido, la sentencia es equivalente a auto y = Complex<double>{x}.

Si bien resulta innecesario en el caso de la plantilla Complex<>, podríamos haber indicado explícitamente que las funciones miembro anteriores corresponden a aquéllas generadas por defecto por el compilador:

   template<std::floating_point T>    class Complex {       T re_,         im_;    public:       // ...       Complex(Complex<T> const& c) = default;  // constructor copia       auto& operator=(Complex<T> const& c) noexcept = default; // operador de asignación copia    };

Independientemente del parámetro T utilizado (floatdouble o long double), la clase Complex<T> es lo suficientemente sencilla (en particular, carece tanto de funciones miembro virtuales como de clases base y sus datos miembro privados son de tipo primitivo) como para que tanto el constructor copia como el operador de asignación copia por defecto sean triviales [1], es decir, realicen la copia de un objeto byte a byte, de manera análoga a si se utilizase la función de bajo nivel std::memmove [2].

En esta nueva serie de posts aprenderemos cuándo y cómo definir constructores copia y operadores de asignación copia para nuestras clases. Como ejercicio básico que nos permita analizar al detalle las peculiaridades de la semántica de copia en C++, implementaremos una plantilla de clase Dynarray<> que encapsule arrays de tamaño conocido en tiempo de ejecución. Por supuesto, prestaremos también especial atención a las garantías ofrecidas por sus funciones miembro ante la emisión de excepciones, en conexión con artículos anteriores. Ello dará pie a discutir uno de los problemas más insidiosos del lenguaje antes del lanzamiento del estándar C++11 (felizmente solucionado a través de la inclusión de la denominada semántica de movimiento), a saber, la incapacidad de las funciones de retornar objetos de gran tamaño en memoria sin afectar al rendimiento y la seguridad del código. Como veremos, la inclusión de constructores de movimiento y operadores de asignación de movimiento proporciona un mecanismo natural y eficiente de transferencia de recursos.


Referencias bibliográficas:
  1. Trivial copy constructor - https://en.cppreference.com/w/cpp/language/copy_constructor#Trivial_copy_constructor
  2. std::memmove() - https://en.cppreference.com/w/cpp/string/byte/memmove

No hay comentarios:

Publicar un comentario