O que é a Regra dos Três em C++? Analisando com exemplos
Se você já escreveu em C++ uma classe que gerencia memória dinâmica (por exemplo, contém new ou malloc), certamente já se deparou com a Regra dos Três (Rule of Three). Esta é uma das regras mais fundamentais da linguagem, que evita vazamentos de memória, deleções duplas e comportamento indefinido. Vamos analisá-la com exemplos reais para que você não apenas memorize a regra, mas entenda por que ela funciona.
Imagine: você escreveu uma classe que aloca memória no construtor. O compilador gentilmente gera para você o construtor de cópia e o operador de atribuição. Mas ele faz isso de forma rasa (shallow copy). E é aí que começam os problemas. A Regra dos Três diz: se sua classe define explicitamente um destrutor, construtor de cópia ou operador de atribuição, provavelmente você precisa definir todos os três. Por quê? Porque isso é um sinal de que sua classe gerencia algum recurso (memória, arquivo, conexão de rede) e a cópia deve ser profunda.
Problema: cópia rasa (shallow copy)
Vejamos um exemplo típico. A classe String, que armazena uma string C em memória dinâmica:
class String {private: char* data; size_t size;public: // Construtor String(const char* str) { size = strlen(str); data = new char[size + 1]; strcpy(data, str); }
// Destrutor ~String() { delete[] data; }
// Construtor de cópia — gerado pelo compilador (raso) // Operador de atribuição — gerado pelo compilador (raso)
void print() const { std::cout << data << std::endl; }};Tudo parece bem, até tentarmos copiar o objeto:
int main() { String s1("Hello"); String s2 = s1; // Chamada do construtor de cópia (gerado) s1.print(); s2.print(); // O programa falhará com erro de double free! return 0;}O que aconteceu? O compilador gerou um construtor de cópia que simplesmente copiou o ponteiro data. Agora s1.data e s2.data apontam para o mesmo bloco de memória. Quando os objetos saem de escopo, seus destrutores são chamados. O primeiro destrutor libera a memória, o segundo tenta liberá-la novamente — double free. O programa trava ou se comporta de forma imprevisível.
Solução: Regra dos Três em ação
Para corrigir a situação, é necessário implementar todas as três funções-membro especiais. Isso é a Regra dos Três. Vamos escrever a versão correta da classe:
class String {private: char* data; size_t size;public: // Construtor String(const char* str) { size = strlen(str); data = new char[size + 1]; strcpy(data, str); }
// Destrutor ~String() { delete[] data; }
// Construtor de cópia — cópia profunda String(const String& other) { size = other.size; data = new char[size + 1]; strcpy(data, other.data); }
// Operador de atribuição — cópia profunda String& operator=(const String& other) { if (this == &other) return *this; // proteção contra autoatribuição
delete[] data; // libera recursos antigos
size = other.size; data = new char[size + 1]; strcpy(data, other.data);
return *this; }
void print() const { std::cout << data << std::endl; }};Agora a cópia funciona corretamente: cada objeto possui seu próprio bloco de memória. A atribuição também está