En general, cualquier clase que administre un recurso debe ser no copiable o tener semántica de copia especializada. Lo contrario también es cierto: cualquier clase que no sea copiable o necesite una semántica de copia especializada está administrando un recurso. "Gestionar un recurso" en la lengua de C++ en la práctica significa responsable de algo de espacio en la memoria, o de una conexión a una red o una base de datos, o un identificador a un archivo, o una transacción de deshacer, y así sucesivamente.
La gestión de recursos captura bastantes ejemplos. Estas son responsabilidades que toman una operación de prefijo, una operación de sufijo y posiblemente alguna acción intermedia. La gestión de la memoria, por ejemplo, implica adquirir un identificador de una dirección de memoria que administraremos, quizás perder el tiempo con esa memoria, y finalmente liberar el controlador (porque si amas algo, que sea gratis).
template<typename T>
struct memory {
memory(T const& val = T()) : p(new T(val)) { }
~memory() { delete p }
T& operator*() const { return *p; }
private:
T* p;
};
// ...
{
memory<int> m0;
*m0 = 3;
std::cout << *m0 << '\n';
}
Esta clase memory
es casi correcta: se adquiere automáticamente el espacio de memoria subyacente y libera de forma automática, incluso si una excepción se propaga algún tiempo después de que adquirió su recurso. Pero tenga en cuenta este escenario:
{
memory<double> m1(3.14);
memory<double> m2(m1); // m2.p == m1.p (do you hear the bomb ticking?)
}
Debido a que no proporcionamos la semántica de copia especializadas para memory
, el compilador proporciona su propio constructor de copia y copiar asignación. Estos hacen la cosa errónea: m2 = m1
significa m2.p = m1.p
, de modo que los dos punteros apuntan a la misma dirección.Está mal porque cuando m2
sale del alcance libera su recurso como un buen objeto responsable, y cuando m1
sale del alcance también libera su recurso, ese mismo recurso m2
ya ha liberado, completando una eliminación doble - un notorio escenario de comportamiento indefinido. Además, en C++ es extremadamente fácil hacer copias de un objeto sin siquiera darse cuenta: una función toma su parámetro por valor, devuelve su parámetro por valor o toma su parámetro por referencia pero luego llama a otra función que toma (o devuelve) su parámetro por valor ... Es más fácil simplemente asumir que las cosas serán intentar copiarlas.
Todo esto para decir que cuando la razón de ser de una clase es la administración de un recurso, inmediatamente debe saber que debe manejar la copia. Debe decidir
- que admite la copia, mientras que a decidir qué copiar significa: el intercambio seguro de los recursos, la realización de una copia profunda de los recursos que subyacen lo que no hay intercambio en absoluto, o la combinación de los dos enfoques como en copy-on-write o copia perezosa Cualquiera que sea la ruta que elija, deberá proporcionar un constructor de copia especializada y un operador de asignación de copias.
- o no admite ningún tipo de copia del recurso, en cuyo caso desactiva el constructor de copia y el operador de asignación de copia.
Iría tan lejos y diré que la gestión de recursos es el único caso en el que se deshabilita la copia o se proporciona una semántica de copia especializada. Esta es solo otra perspectiva en The Rule of Three.
A singleton sería un ejemplo. –