La regla de 5: ¿usarlo o no?

20

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 Aestá vacío y no es realmente necesario. Entonces, ¿debería estar allí o debería eliminarse?

BЈовић
fuente
15
Las 2 citas hablan de cosas diferentes. O extraño totalmente tu punto.
Benjamin Bannier
1
@honk En el estándar de codificación de mi equipo, tenemos una regla para declarar siempre los 4 (constructor, destructor, constructores de copia). Me preguntaba si realmente tiene sentido hacerlo. ¿Realmente tengo que declarar siempre destructores, incluso si están vacíos?
Bћовић
En cuanto a los desctructores vacíos, piense en esto: codesynthesis.com/~boris/blog/2012/04/04/… . De lo contrario, la regla de 3 (5) tiene mucho sentido para mí, no tengo idea de por qué uno querría una regla de 4.
Benjamin Bannier
@honk Tenga cuidado con la información que encuentre en la red. No todo es verdad. Por ejemplo, virtual ~base () = default;no se compila (con una buena razón)
BЈовић
@VJovic, No, no tiene que declarar un destructor vacío, a menos que necesite hacerlo virtual. Y mientras estamos en el tema, tampoco deberías estar usando auto_ptr.
Dima

Respuestas:

44

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.

Thorsten Müller
fuente
Con el uso de punteros inteligentes, los destructores están vacíos en la mayoría de los casos (yo diría que> 99% de los destructores en mi base de código están vacíos, porque casi todas las clases usan el lenguaje pimpl).
Bћовић
Wow, eso es tanto proxeneta que lo llamaría maloliente. Con muchos compiladores, las espinillas serán más difíciles de optimizar (por ejemplo, más difíciles de alinear).
Benjamin Bannier
@honk ¿Qué quieres decir con "muchos compiladores espinillas"? :)
Publicado el
@VJovic: lo siento, error tipográfico: 'código con granos'
Benjamin Bannier
4

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.

Dima
fuente
2

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 =defaultpueden 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).

Un programador
fuente
El ejemplo demuestra mi punto. El destructor no es realmente necesario, pero la regla de 3 dice que debería estar allí.
Bћовић
1

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*ctiene un +que toma dos temporarios y genera un temporal: evitar clonar y eliminar puede ser útil)

Emilio Garavaglia
fuente
1

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:

  1. 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.

  2. 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.

Jules
fuente
-3

Hay otro punto que aún no se menciona en la discusión: un destructor siempre debe ser virtual.

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    virtual ~A(){}
    ...
}

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.

Lexi
fuente
1
Pero no quiero el constructor virtual. Si hago eso, entonces cada llamada a cualquier método usaría un despacho virtual. Por cierto, tenga en cuenta que no hay tal cosa como "constructor virtual" en c ++. Además, compilé el ejemplo como un nivel de advertencia muy alto.
Bћовић
IIRC, la regla que usa gcc para su advertencia, y la regla que generalmente sigo de todos modos, es que debería haber un destructor virtual si hay otros métodos virtuales en la clase.
Jules