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 A
está vazio e não é realmente necessário. Então, deveria estar lá ou deveria ser removido?
fonte
virtual ~base () = default;
não compila (com uma boa razão)auto_ptr
.Respostas:
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.
fonte
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.
fonte
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
=default
podem 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).
fonte
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*c
tem um+
que leva dois temporários e gera um temporário: evitar clone e exclusão pode ser útil)fonte
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:
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.
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.
fonte
Há outro ponto ainda não mencionado na discussão: Um destruidor deve sempre ser virtual.
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.
fonte