A regra de 5 - para usá-lo ou não?

20

A regra 3 ( a regra 5 no novo padrão c ++) declara:

Se você precisar declarar explicitamente o destruidor, o construtor de cópias ou o operador de atribuição de cópias, provavelmente precisará declarar explicitamente todos os três.

Mas, por outro lado, o " Código Limpo " de Martin recomenda remover todos os construtores e destruidores vazios (página 293, G12: Desordem ):

De que uso é um construtor padrão sem implementação? Tudo o que serve é desordenar o código com artefatos sem sentido.

Então, como lidar com essas duas opiniões opostas? Os construtores / destruidores vazios devem realmente ser implementados?


O próximo exemplo demonstra exatamente o que quero dizer:

#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 bem usando g ++ 4.6.1 com:

g++ -std=c++0x -Wall -Wextra -pedantic example.cpp

O destruidor de struct Aestá vazio e não é realmente necessário. Então, deveria estar lá ou deveria ser removido?

BЈовић
fonte
15
As 2 citações falam sobre coisas diferentes. Ou sinto totalmente sua falta.
Benjamin Bannier
1
@honk No padrão de codificação da minha equipe, temos uma regra para sempre declarar todos os quatro (construtor, destruidor, copiador de construtores). Fiquei me perguntando se realmente faz sentido fazer. Eu realmente tenho que sempre declarar destruidores, mesmo que estejam vazios?
BЈовић
Quanto aos desctrutores vazios, pense sobre isso: codesynthesis.com/~boris/blog/2012/04/04/… . Caso contrário, a regra de 3 (5) faz todo o sentido para mim, nenhuma idéia porque alguém iria querer uma regra de 4.
Benjamin Bannier
@honk Cuidado com as informações que você encontra na rede. Nem tudo é verdade. Por exemplo, virtual ~base () = default;não compila (com uma boa razão)
BЈовић
@VJovic, Não, você não precisa declarar um destruidor vazio, a menos que precise torná-lo virtual. E enquanto estamos no assunto, você também não deveria estar usando auto_ptr.
Dima

Respostas:

44

Para começar, a regra diz "provavelmente", portanto nem sempre se aplica.

O segundo ponto que vejo aqui é que, se você precisar declarar um dos três, é porque está fazendo algo especial como alocar memória. Nesse caso, os outros não estariam vazios, pois precisariam lidar com a mesma tarefa (como copiar o conteúdo da memória alocada dinamicamente no construtor de cópias ou liberar essa memória).

Portanto, como conclusão, você não deve declarar construtores ou destruidores vazios, mas é muito provável que, se um for necessário, os outros também sejam necessários.

Como no seu exemplo: Nesse caso, você pode deixar o destruidor de fora. Obviamente, não faz nada. O uso de ponteiros inteligentes é um exemplo perfeito de onde e por que a regra 3 não se aplica.

É apenas um guia sobre onde você pode examinar seu código novamente, caso você tenha esquecido de implementar uma funcionalidade importante que, de outra forma, poderia ter perdido.

thorsten müller
fonte
Com o uso de ponteiros inteligentes, os destruidores estão vazios na maioria dos casos (eu diria que> 99% dos destruidores na minha base de código estão vazios, porque quase todas as classes usam o idioma pimpl).
BЈовић
Uau, isso é tanta coisa que eu chamaria de mau cheiro. Com muitos compiladores, o pimpled será mais difícil de otimizar (por exemplo, mais difícil de incorporar).
22712 Benjamin Bannier
@honk O que você quer dizer com "muitos compiladores espinhados"? :)
BЈовић
@VJovic: desculpe, erro de digitação: 'pimpled code'
Benjamin Bannier
4

Realmente não há contradição aqui. A regra de 3 fala sobre o destruidor, o construtor de cópias e o operador de atribuição de cópias. Tio Bob fala sobre construtores padrão vazios.

Se você precisar de um destruidor, sua classe provavelmente conterá ponteiros para a memória alocada dinamicamente, e você provavelmente desejará ter um copiador de cópias e um operator=()que faça uma cópia profunda. Isso é completamente ortogonal para a necessidade ou não de um construtor padrão.

Observe também que em C ++ há situações em que você precisa de um construtor padrão, mesmo que esteja vazio. Digamos que sua classe tenha um construtor não padrão. Nesse caso, o compilador não irá gerar um construtor padrão para você. Isso significa que os objetos desta classe não podem ser armazenados em contêineres STL, porque esperam que os objetos sejam construtíveis por padrão.

Por outro lado, se você não planeja colocar os objetos de sua classe em contêineres STL, um construtor padrão vazio certamente é uma confusão inútil.

Dima
fonte
2

Aqui, o seu potencial (*) equivalente ao construtor / designação / destruidor padrão tem um propósito: documentar o fato de que você tem alguma dúvida sobre o problema e determinar que o comportamento padrão estava correto. Aliás, no C ++ 11, as coisas não se estabilizaram o suficiente para saber se elas =defaultpodem servir a esse propósito.

(Há outro propósito em potencial: fornecer uma definição fora da linha em vez da definição embutida padrão, melhor documentar explicitamente se você tiver algum motivo para fazê-lo).

(*) Potencial, porque não me lembro de um caso da vida real em que a regra dos três não se aplicava; se eu precisava fazer algo em um, precisava fazer algo nos outros.


Edite após adicionar um exemplo. seu exemplo usando auto_ptr é interessante. Você está usando um ponteiro inteligente, mas não o que está pronto para o trabalho. Prefiro escrever um que seja - especialmente se a situação ocorrer com frequência - do que fazer o que você fez. (Se não me engano, nem o padrão nem o impulso fornecem um).

AProgrammer
fonte
O exemplo demonstra meu ponto de vista. O destruidor não é realmente necessário, mas a regra 3 diz que ele deveria estar lá.
BЈовић
1

A regra 5 é uma extensão cautalativa da regra 3, que é um comportamento cauteloso contra possível uso indevido de objetos.

Se você precisar de um destruidor, significa que você fez algum "gerenciamento de recursos" que não seja o padrão (apenas construa e destrua valores ).

Como copiar, atribuir, mover e transferir por padrão os valores de cópia , se você não estiver mantendo apenas valores , precisará definir o que fazer.

Dito isto, o C ++ exclui a cópia se você definir a movimentação e exclui a movimentação se você definir a cópia. Na maioria dos casos, você precisa definir se deseja emular um valor (portanto, copiar mut clona o recurso e mover não faz sentido) ou um gerenciador de recursos (e, portanto, move o recurso, onde a cópia não faz sentido: a regra de 3 se torna a regra dos outros 3 )

Os casos em que é necessário definir copiar e mover (regra 5) são bastante raros: normalmente você tem "grande valor" que deve ser copiado se for fornecido a objetos distintos, mas pode ser movido se retirado de um objeto temporário (evitando um clone então destrua ). Esse é o caso de contêineres STL ou aritméticos.

Um caso pode ser matricial: eles precisam dar suporte à cópia porque são valores ( a=b; c=b; a*=2; b*=3;não devem influenciar um ao outro), mas podem ser otimizados dando suporte também à movimentação ( a = 3*b+4*ctem um +que leva dois temporários e gera um temporário: evitar clone e exclusão pode ser útil)

Emilio Garavaglia
fonte
1

Prefiro uma redação diferente da regra das três, que parece mais razoável, que é "se sua classe precisa de um destruidor (que não seja um destruidor virtual vazio), provavelmente também precisa de um construtor de cópias e operador de atribuição".

Especificá-lo como um relacionamento unidirecional do destruidor torna algumas coisas mais claras:

  1. Não se aplica nos casos em que você fornece um construtor de cópias ou operador de atribuição não padrão como apenas uma otimização.

  2. O motivo da regra é que o construtor de cópias padrão ou o operador de atribuição pode estragar o gerenciamento manual de recursos. Se você estiver gerenciando recursos manualmente, é provável que tenha percebido que precisará de um destruidor para liberá-los.

Jules
fonte
-3

Há outro ponto ainda não mencionado na discussão: Um destruidor deve sempre ser virtual.

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

O construtor precisa ser declarado como virtual na classe base para torná-lo virtual em todas as classes derivadas também. Portanto, mesmo que sua classe base não precise de um destruidor, você acaba declarando e implementando um destruidor vazio.

Se você colocar todos os avisos em (-Wall -Wextra -Weffc ++), o g ++ o alertará sobre isso. Considero uma boa prática sempre declarar um destruidor virtual em qualquer classe, porque você nunca sabe se sua classe acabará se tornando uma classe base. Se o destruidor virtual não for necessário, não fará mal. Se for, você economiza tempo para encontrar o erro.

Lexi
fonte
1
Mas eu não quero o construtor virtual. Se eu fizer isso, todas as chamadas para qualquer método usariam despacho virtual. Entre, observe que não existe "construtor virtual" em c ++. Além disso, compilei o exemplo como um nível de aviso muito alto.
BЈовић
O IIRC, a regra que o gcc usa para seu aviso, e a regra que eu sigo de qualquer maneira, é que deve haver um destruidor virtual se houver outros métodos virtuais na classe.
Jules