Artículos de la serie:
- Introducción
- Una plantilla base para el contenedor Dynarray<>
- Primeros constructores para la plantilla Dynarray<>
- Técnica copy-and-swap
- Semántica de movimiento
- Ú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 (float, double 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:
- Trivial copy constructor - https://en.cppreference.com/w/cpp/language/copy_constructor#Trivial_copy_constructor
- std::memmove() - https://en.cppreference.com/w/cpp/string/byte/memmove
No hay comentarios:
Publicar un comentario