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
Técnica copy-and-swap
Con la implementación del contenedor Dynarray<> de que disponemos a partir del estudio realizado en posts anteriores, cabe preguntarse si el constructor copia y el operador de asignación copia generados automáticamente por el compilador son adecuados para nuestra plantilla. A poco que reflexionemos sobre esta cuestión concluiremos que ambas funciones especiales generadas por omisión son de todo punto inadmisibles en nuestro caso, al reducirse ambas a una mera copia byte a byte de la representación del objeto pasado como argumento (es decir, del puntero first y el entero count del objeto a copiar). Este comportamiento resultaría funcionalmente equivalente a:
template<typename T> inline // constructor copia
Dynarray<T>::Dynarray(Dynarray<T> const& d) noexcept
: first{d.first}, count{d.count} { }
template<typename T> inline // operador de asignación copia
auto Dynarray::operator=(Dynarray<T> const& d) noexcept -> Dynarray<T>&
{
first = d.first;
count = d.count;
return *this;
}
Así, al inicializarse la copia de un objeto, tanto el original como la copia harían referencia al mismo bloque de memoria en el free store. De ser destruido uno de estos dos objetos, por ejemplo, el restante quedaría invalidado, pues referenciaría un área de memoria previamente liberada por el destructor de la clase. Cualquier intento de acceder a los elementos de dicho array, o de destruir este segundo objeto, conllevaría comportamiento indefinido. El caso del operador de asignación es si cabe aún más grave que el del constructor copia, pues el array inicialmente referenciado por el objeto de destino no es siquiera destruido antes de producirse la asignación, produciéndose una fuga de memoria. Es, pues, responsabilidad del programador el definir las operaciones correctas. ¡Manos a la obra!
El constructor copia debe asignar un espacio en memoria suficiente para contener tantos elementos como haya en el array de origen. Se procederá entonces a realizar una copia de cada uno de dichos elementos en el nuevo array de destino. La implementación es, en este caso, trivial:
template<typename T> inline // constructor copia específico
Dynarray<T>::Dynarray(Dynarray<T> const& d)
: Base{d.count}
{
std::uninitialized_copy(d.first, d.first + d.count, first);
}
El operador de asignación copia producirá también un duplicado del array pasado como argumento, si bien debe destruir el array inicialmente referenciado por el objeto de destino. En una primera aproximación a este problema, escribiríamos:
template<typename T> inline // operador de asignación copia específico
auto Dynarray<T>::operator=(Dynarray<T> const& d) -> Dynarray<T>&
{
if (this != &d) { // protegemos contra auto-asignación
// destrucción de datos previos:
if (auto p = first) {
for (; p != (first + count); ++p)
p->~value_type();
this->dealloc();
first = nullptr;
count = 0;
}
// copia de la nueva representación:
if (auto const sz = d.count) {
first = this->alloc(sz); // (A)
count = sz;
try {
std::uninitialized_copy(d.first, d.first + sz, first); // (B)
}
catch (...) {
this->dealloc();
first = nullptr;
count = 0;
throw;
}
}
}
return *this;
}
Esta solución adolece de serias deficiencias, la más importante de las cuales consiste en que, de producirse el lanzamiento de una excepción durante la asignación del nuevo bloque de memoria en (A) o durante la construcción de uno de los elementos copiados en (B), el array original se perdería irreversiblemente, quedando vacío. Sería deseable proporcionar la garantía fuerte ante excepciones, de modo que, de producirse una excepción durante el proceso de copia, se mantenga la representación inicial del objeto. Este comportamiento puede lograrse mediante la denominada técnica copy-and-swap (copiar e intercambiar) [1]:
template<typename T> inline // operador de asignación copia específico
auto Dynarray::operator=(Dynarray<T> const& d) -> Dynarray<T>&
// garantía fuerte ante excepciones
{
// solución idiomática copy-and-swap:
Dynarray<value_type> tmp{d};
swap(tmp);
return *this;
} // destrucción automática de tmp en este punto
template<typename T> inline
void Dynarray<T>::swap(Dynarray<T>& d) noexcept // intercambio de representaciones
{
std::swap(first, d.first);
std::swap(count, d.count);
}
En primer lugar, construimos un nuevo objeto Dynarray<T> de nombre tmp en el cuerpo del operador de asignación copia, inicializado a partir del objeto d pasado como argumento. Para ello, invocamos al constructor copia definido anteriormente. A continuación, intercambiamos las representaciones del objeto de destino *this y el temporal tmp llamando a una función miembro pública swap() creada a tal efecto. Ésta simplemente intercambia tanto los punteros first como los enteros count de ambos objetos. Al abandonar el operador de asignación, el objeto tmp (y con él el array originalmente referenciado por el objeto de destino) es destruido automáticamente. La operación de intercambio swap() es noexcept. De emitirse una excepción durante la construcción del temporal tmp, ésta se propaga fuera del operador de asignación sin que se llegue a modificar la representación del objeto de destino.
Observemos que, en su versión final, hemos preferido no proteger el cuerpo del operador de asignación copia contra operaciones de auto-asignación dada su escasa probabilidad de ocurrencia y el coste de la secuencia de control if (this != &d) { /* ... */ }, innecesaria en todos los demás casos.
Referencias bibliográficas:
- Copy-and-swap idiom - https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
- Herb Sutter's GotW #59 - http://www.gotw.ca/gotw/059.htm
- Stroustrup, B, 'The C++ Programming Language'. Addison-Wesley, 4th Edition (2013).
No hay comentarios:
Publicar un comentario