Introdução: quando C++ deixa de ser "apenas uma linguagem"
Você já escreveu dezenas de programas de console, aprendeu a usar std::vector e até entendeu templates. Mas C++ é uma linguagem que nunca perdoa uma abordagem superficial. O desenvolvedor iniciante passa para o próximo nível exatamente no momento em que encontra comportamento indefinido (UB) e começa a entender que o compilador não é um amigo gentil, mas um contratante rigoroso.
Neste artigo, analisaremos não erros banais como esquecer ; ou confundir = com ==. Falaremos sobre armadilhas que aguardam aqueles que já conhecem a sintaxe, mas ainda não dominam a filosofia da linguagem. Esses erros são a causa de 90% dos bugs difíceis de detectar em código industrial.
1. Ignorar a Rule of Five e a Rule of Zero
Problema: funções-membro especiais geradas implicitamente
Muitos desenvolvedores vindos de Java ou Python acreditam que o compilador "fará tudo bonito" com a cópia e movimentação de objetos. Isso é um equívoco perigoso.
Considere uma classe com recurso dinâmico:
class Buffer {public: Buffer(size_t size) : data(new char[size]), size(size) {} ~Buffer() { delete[] data; } // Tudo ok? Não! Não há construtor de cópia nem operador de atribuição!private: char* data; size_t size;};
void process() { Buffer a(100); Buffer b = a; // Cópia rasa! Dois ponteiros para a mesma memória.} // Liberação dupla — UB!A Rule of Five afirma: se você definir pelo menos um dos cinco métodos especiais (destrutor, construtor de cópia, operador de atribuição por cópia, construtor de movimento, operador de atribuição por movimento), você deve definir todos eles.
Solução: Use std::unique_ptr ou std::vector para gerenciar recursos. Esta é a abordagem Rule of Zero: se sua classe não gerencia recursos diretamente, o compilador gerará funções especiais corretas automaticamente.
class Buffer {public: Buffer(size_t size) : data(std::make_unique<char[]>(size)), size(size) {} // Rule of Zero — destrutor, cópia e movimento funcionam corretamente!private: std::unique_ptr<char[]> data; size_t size;};2. Uso incorreto da semântica de movimento
Problema: move não torna o objeto "vazio" automaticamente
Um dos erros mais traiçoeiros é acreditar que após std::move o objeto original se torna "seguro" para uso. Na verdade, std::move é apenas uma conversão para referência rvalue. O estado do objeto movido depende da implementação.
std::string a = "Hello, World!";std::string b = std::move(a);std::cout << a; // Tecnicamente — UB? Não, mas o conteúdo de a é indefinido!// Pode ser uma string vazia, ou pode ser "Hello, World!"O padrão garante apenas que o objeto movido está em um estado válido, mas indefinido. Você pode chamar a.clear() ou a = "new value", mas não deve confiar em seu conteúdo.
Dica: Após a movimentação, ou redefina o objeto para um estado conhecido, ou pare de usá-lo. Para tipos personalizados, sempre documente as pós-condições do construtor de movimento.
3. Vazamentos de memória através de ponteiros brutos e exceções
Problema: exceções "rompem" a cadeia de liberação de recursos
Mesmo desenvolvedores experientes às vezes escrevem código com gerenciamento manual de memória:
void process() { int* ptr1 = new int(42); int* ptr2 = new int(100); // Se uma exceção for lançada aqui — vazamento de ptr1! dangerous_function(); delete ptr2; delete ptr1;}Em C++ não há finally