La regla de 3 ( la regla de 5 en el nuevo estándar de c ++) establece:
Si necesita declarar explícitamente el destructor, el constructor de copia o el operador de asignación de copia usted mismo, probablemente deba declarar explícitamente los tres.
Pero, por otro lado, el " Código Limpio " de Martin aconseja eliminar todos los constructores y destructores vacíos (página 293, G12: Desorden ):
¿De qué sirve un constructor predeterminado sin implementación? Todo lo que sirve para hacer es desordenar el código con artefactos sin sentido.
Entonces, ¿cómo manejar estas dos opiniones opuestas? ¿Deberían implementarse realmente constructores / destructores vacíos?
El siguiente ejemplo demuestra exactamente lo que quiero decir:
#include <iostream>
#include <memory>
struct A
{
A( const int value ) : v( new int( value ) ) {}
~A(){}
A( const A & other ) : v( new int( *other.v ) ) {}
A& operator=( const A & other )
{
v.reset( new int( *other.v ) );
return *this;
}
std::auto_ptr< int > v;
};
int main()
{
const A a( 55 );
std::cout<< "a value = " << *a.v << std::endl;
A b(a);
std::cout<< "b value = " << *b.v << std::endl;
const A c(11);
std::cout<< "c value = " << *c.v << std::endl;
b = c;
std::cout<< "b new value = " << *b.v << std::endl;
}
Compila bien usando g ++ 4.6.1 con:
g++ -std=c++0x -Wall -Wextra -pedantic example.cpp
El destructor para struct A
está vacío y no es realmente necesario. Entonces, ¿debería estar allí o debería eliminarse?
fuente
virtual ~base () = default;
no se compila (con una buena razón)auto_ptr
.Respuestas:
Para empezar, la regla dice "probablemente", por lo que no siempre se aplica.
El segundo punto que veo aquí es que si tiene que declarar uno de los tres, es porque está haciendo algo especial como asignar memoria. En este caso, los otros no estarían vacíos ya que tendrían que manejar la misma tarea (como copiar el contenido de la memoria asignada dinámicamente en el constructor de copias o liberar dicha memoria).
Como conclusión, no debe declarar constructores o destructores vacíos, pero es muy probable que si se necesita uno, también se necesiten los otros.
En cuanto a su ejemplo: en tal caso, puede dejar el destructor fuera. No hace nada, obviamente. El uso de punteros inteligentes es un ejemplo perfecto de dónde y por qué no se cumple la regla de 3.
Es solo una guía sobre dónde echar un segundo vistazo a su código en caso de que haya olvidado implementar una funcionalidad importante que de otro modo podría haberse perdido.
fuente
Realmente no hay contradicción aquí. La regla de 3 habla sobre el destructor, el constructor de copia y el operador de asignación de copia. El tío Bob habla sobre constructores predeterminados vacíos.
Si necesita un destructor, entonces su clase probablemente contenga punteros a la memoria asignada dinámicamente, y probablemente desee tener una copiadora y una
operator=()
copia profunda. Esto es completamente ortogonal a si necesita o no un constructor predeterminado.Tenga en cuenta también que en C ++ hay situaciones en las que necesita un constructor predeterminado, incluso si está vacío. Digamos que su clase tiene un constructor no predeterminado. En ese caso, el compilador no generará un constructor predeterminado para usted. Eso significa que los objetos de esta clase no pueden almacenarse en contenedores STL, porque esos contenedores esperan que los objetos sean construibles por defecto.
Por otro lado, si no está planeando colocar los objetos de su clase en contenedores STL, un constructor predeterminado vacío ciertamente es un desorden inútil.
fuente
Aquí, su potencial (*) equivalente al constructor / asignación / destructor predeterminado tiene un propósito: documentar el hecho que ha tenido sobre el problema y determinó que el comportamiento predeterminado era correcto. Por cierto, en C ++ 11, las cosas no se han estabilizado lo suficiente como para saber si
=default
pueden servir para ese propósito.(Hay otro propósito potencial: proporcionar una definición fuera de línea en lugar de la definición en línea predeterminada, mejor documentar explícitamente si tiene alguna razón para hacerlo).
(*) Potencial porque no recuerdo un caso de la vida real en el que la regla de los tres no se aplicara, si tenía que hacer algo en uno, tenía que hacer algo en los demás.
Edite después de agregar un ejemplo. su ejemplo usando auto_ptr es interesante. Está utilizando un puntero inteligente, pero no uno que esté a la altura del trabajo. Prefiero escribir uno que sea, especialmente si la situación ocurre a menudo, que hacer lo que hiciste. (Si no me equivoco, ni el estándar ni el refuerzo proporcionan uno).
fuente
La regla de 5 es una extensión cautalativa de la regla de 3 que es un comportamiento cautelativo contra un posible mal uso del objeto.
Si necesita tener un destructor, significa que realizó una "gestión de recursos" distinta de la predeterminada (solo construir y destruir valores ).
Dado que copiar, asignar, mover y transferir por defecto valores de copia , si no tiene solo valores , debe definir qué hacer.
Dicho esto, C ++ elimina la copia si define el movimiento y elimina el movimiento si define la copia. En la mayoría de los casos, debe definir si desea emular un valor (por lo tanto, copiar mut clonar el recurso y moverlo no tiene sentido) o un administrador de recursos (y, por lo tanto, mover el recurso, donde copiar no tiene sentido: la regla de 3 se convierte en la regla de los otros 3 )
Los casos en los que tiene que definir tanto copiar como mover (regla de 5) son bastante raros: generalmente tiene un "gran valor" que debe copiarse si se le da a objetos distintos, pero puede moverse si se toma de un objeto temporal (evitando un clon luego destruye ). Ese es el caso de los contenedores STL o contenedores aritméticos.
Un caso puede ser matrices: tienen que admitir copia porque son valores (
a=b; c=b; a*=2; b*=3;
no deben influirse entre sí) pero pueden optimizarse admitiendo también movimiento (a = 3*b+4*c
tiene un+
que toma dos temporarios y genera un temporal: evitar clonar y eliminar puede ser útil)fuente
Prefiero una formulación diferente de la regla de tres, que parece más razonable, que es "si su clase necesita un destructor (que no sea un destructor virtual vacío), probablemente también necesite un constructor de copia y un operador de asignación".
Al especificarlo como una relación unidireccional desde el destructor, se aclaran algunas cosas:
No se aplica en los casos en que proporcione un constructor de copia no predeterminado u operador de asignación solo como una optimización.
La razón de la regla es que el constructor de copia predeterminado o el operador de asignación pueden arruinar la gestión manual de recursos. Si está administrando recursos manualmente, es probable que se haya dado cuenta de que necesitará un destructor para liberarlos.
fuente
Hay otro punto que aún no se menciona en la discusión: un destructor siempre debe ser virtual.
El constructor debe declararse como virtual en la clase base para hacerlo virtual en todas las clases derivadas también. Por lo tanto, incluso si su clase base no necesita un destructor, terminará declarando e implementando un destructor vacío.
Si pone todas las advertencias en (-Wall -Wextra -Weffc ++) g ++ le advertirá sobre esto. Considero una buena práctica declarar siempre un destructor virtual en cualquier clase, porque nunca se sabe si su clase eventualmente se convertirá en una clase base. Si no se necesita el destructor virtual, no hace daño. Si es así, ahorra tiempo para encontrar el error.
fuente