O que é a regra de 3 em C++ e por que ela importa na prática
A regra de 3 é um princípio de design de classes em C++ que diz o seguinte: se uma classe precisa de um destrutor definido pelo usuário, um construtor de cópia ou um operador de atribuição por cópia, ela provavelmente precisa de todos os três. Não é uma regra do compilador. O código vai compilar sem ela. É puramente sobre não fazer algo que vai estourar em tempo de execução.
Como aplicar a regra de 3 em atividades de estudo
Ao trabalhar com regra de 3 atividades, o mais comum é receber um exercício onde uma classe gerencia memória alocada dinamicamente e você precisa implementar os três membros. Aqui está o passo a passo que eu sigo quando resolvo esses exercícios, e que também funciona em cenários reais de manutenção de código legado. Primeiro, identifique o recurso. Se a classe usa new ou new[] em algum lugar, você lida com um recurso gerenciado manualmente. Alocar memória é o exemplo clássico, mas pode ser um arquivo aberto, um mutex, um descritor de rede ou qualquer coisa que precise de limpeza explícita.
Segundo, escreva o construtor de cópia. Ele recebe uma referência const para o mesmo tipo e faz uma cópia profunda do recurso. Terceiro, escreva o operador de atribuição por cópia. Ele precisa fazer o mesmo que o construtor de cópia, mas com proteção extra contra autoatribuição. Quarto, escreva o destrutor. Ele libera o recurso. No meu primeiro semestre de faculdade, eu fiz um exercício onde a classe Texto guardava um char* e eu só implementei o destrutor. O código funcionava em 90% dos casos. Na hora da apresentação, passei um objeto como argumento por valor para uma função. O programa travou num segmentation fault porque o construtor de cópia padrão fazia uma cópia superficial dos ponteiros, e dois objetos passaram a apontar para o mesmo bloco de memória. O destrutor foi chamado duas vezes no mesmo endereço. Isso é exatamente o tipo de problema que a regra de 3 existe para evitar.
Implementação prática passo a passo
Vamos construir uma classe simples que gerencia um array de inteiros. Sem a regra de 3, isso é perigoso.
O erro comum: classe incompleta
Considere esta classe: class Vetor { int* dados; int tamanho; public: Vetor(int n) : tamanho(n) { dados = new int[tamanho]; } ~Vetor() { delete[] dados; } };
O construtor aloca memória. O destrutor libera. Parece completo. Mas o compilador gera automaticamente um construtor de cópia e um operador de atribuição que copiam os ponteiros, não os dados. Duas instâncias passam a compartilhar o mesmo int*. Quando a primeira é destruída, a segunda herda um ponteiro invalidado. Isso se chama dangling pointer e é um dos erros mais difíceis de debugar em C++.
A correção com regra de 3
Adicionando os três membros faltantes: class Vetor { int* dados; int tamanho; public: Vetor(int n) : tamanho(n) { dados = new int[tamanho]; } Vetor(const Vetor& outro) : tamanho(outro.tamanho) { dados = new int[tamanho]; for(int i=0; i
👉 Clique no botão abaixo para saber mais sobre o assunto!
Observe alguns detalhes que começam a parecer óbvios só depois que você vê o código rodando quebrado várias vezes. O operador de atribuição verifica autoatribuição com this != &outro. Sem essa verificação, você deletaria o próprio objeto enquanto tentava copiá-lo, e o comportamento seria indefinido. O construtor de cópia e o operador fazem cópia profunda, linha por linha, não apenas uma cópia de ponteiro.
Lições que não estão nos livros
Uma coisa que poucos mencionam em materiais introdutórios sobre regra de 3 atividades é que existem classes onde você precisa dos três membros mas a implementação é trivial, e outras onde a implementação correta exige cuidado com exceções. Quando o new no construtor de cópia falha e lança uma exceção, o objeto original nunca foi modificado, o que é bom. Mas no operador de atribuição, se a alocação falhar após você já ter deletado os dados antigos, seu objeto fica no estado mais ruim possível: nem com os dados velhos, nem com os novos. Minha solução prática para isso é usar o padrão copy-and-swap quando possível. Em vez de deletar e realocar no operador de atribuição, eu crio uma cópia local e depois swap. Se a cópia falhar, o original permanece intacto. Funciona assim:
Vetor& operator=(Vetor outro) { swap(outro); return *this; } void swap(Vetor& outro) { std::swap(dados, outro.dados); std::swap(tamanho, outro.tamanho); } Esse operador recebe o argumento por valor, o que significa que o construtor de cópia já foi chamado antes de entrar no corpo. A única coisa que sobra é o swap, que nunca falha. É mais limpo, mais seguro contra exceções e, curiosamente, mais curto. A desvantagem é que esse padrão funciona bem quando a classe tem membros que suportam swap eficiente. Nem todos os recursos se comportam assim.
Quando a regra de 3 não se aplica
Tem casos onde implementar os três membros manualmente é contraproducente. Se a classe não gerencia nenhum recurso, a regra de 3 não se aplica. O compilador gera versões corretas de graça. Tentar implementá-los manualmente nessas situações só adiciona_complexidade_ desnecessária e potencial para bugs. Também não faz sentido seguir a regra de 3 para classes que querem comportamento de único proprietário. Nesse caso, você não quer cópia. Você delete o construtor de cópia e o operador de atribuição. Isso transforma a classe em move-only, que é um conceito mais moderno e aparece na regra de 5 do C++11. Forçar a regra de 3 aqui criaria cópias superficiais acidentais em vez de impedir o problema.
Testando se sua implementação está correta
Depois de implementar a regra de 3 em alguma atividade prática, teste com este cenário mínimo. Crie um objeto, copie-o, atribua-o a si mesmo, destrua ambos e verifique se não há acesso a memória liberada. Use Valgrind no Linux ou o Memory Debugger do Visual Studio. Se houver memory leak ou double-free, o Valgrind aponta a linha exata. Em ambientes Windows sem Valgrind, o Address Sanitizer é alternativa válida. Compile com -fsanitize=address e rode o teste. Ele detecta uso de memória após liberação,-buffer overflows e outros problemas relacionados a regras de três mal implementadas.
Erros frequentes em atividades acadêmicas
No dia a dia corrigindo exercícios de alunos, vejo padrões repetitivos. O primeiro erro é esquecer o parâmetro const no construtor de cópia. O segundo é usar memcpy para copiar structs com ponteiros, achando que é mais rápido. O terceiro, e mais grave, é implementar apenas dois dos três membros, na esperança de que o compilador se vire. O compilador não se vira. Ele faz exatamente o que você disse, nada mais. Outro erro comum em regra de 3 atividades é copiar o tamanho mas não o conteúdo. O objeto copia fica com o tamanho certo mas lixo na memória apontada. Também vejo gente colocar lógica de limpeza no construtor de cópia, o que não faz sentido, ou esquecer de retornar *this no operador de atribuição, quebrando encadeamento como a = b = c.
Resumo objetivo
A regra de 3 existe porque C++ gera membros padrão que são insatisfatórios quando há recursos gerenciados manualmente. A solução é escrever explicitamente o destrutor, o construtor de cópia e o operador de atribuição por cópia. Teste com sanitizadores. Evite implementar quando não há recurso gerenciado. Para cenários modernos, considere a regra de 5 ou a regra de 0, que substituem a abordagem manual por move semantics e smart pointers respectivamente. Se estiver procurando material prático para treinar, busque por regra de 3 atividades com foco em classes que usam vetores dinâmicos, strings C-style e gerenciamento de arquivos. São os três cenários mais recorrentes em provas e em código legado que você vai encontrar na prática profissional.