No artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco chegamos à Regra do Zero: componha suas classes com tipos que já se gerenciam e você nunca escreve as cinco funções especiais. Prometi apresentar os membros mais importantes desse clube — os ponteiros inteligentes —, e começamos pelo mais fundamental e mais usado deles, o std::unique_ptr. Ele é a resposta direta do C++ moderno ao par new/delete (e, por tabela, ao malloc/free): um objeto que possui um ponteiro para o heap, garante sua liberação automática, e proíbe, por construção, que dois donos apontem para o mesmo recurso. Ao fim desta aula, o delete cru terá praticamente desaparecido do seu vocabulário — e isso é uma ótima notícia.
O problema que ele resolve
Em C++ pré-moderno (e conceitualmente em C), alocar no heap significava parear cada aquisição com uma liberação manual:
// Estilo antigo, PROBLEMÁTICO — não faça isso hoje.
struct Conexao { /* ... */ };
void trabalha() {
Conexao* c = new Conexao(); // aloca no heap (como malloc, mas chama construtor)
// ... usa c ...
// Se um 'return' antecipado ou uma exceção ocorrer aqui, o delete abaixo
// nunca roda e a Conexao vaza.
delete c; // preciso lembrar SEMPRE, em TODOS os caminhos
}
É o mesmo drama do fclose do artigo Nascimento e Morte de um Objeto — Construtores, Destrutores e o RAII na Prática, agora com memória. O std::unique_ptr embrulha esse ponteiro num objeto RAII: o recurso é liberado automaticamente quando o unique_ptr sai de escopo, em qualquer caminho de saída.
Criando e usando: std::make_unique
A forma idiomática de criar um unique_ptr é std::make_unique (C++14), que aloca e devolve o ponteiro inteligente já configurado:
#include <memory> // unique_ptr, make_unique vivem aqui
#include <iostream>
struct Conexao {
Conexao() { std::cout << "conexão aberta\n"; }
~Conexao() { std::cout << "conexão fechada\n"; }
void enviar(const std::string& msg) { std::cout << "enviando: " << msg << '\n'; }
};
void trabalha() {
// Cria uma Conexao no heap, possuída por um unique_ptr.
auto c = std::make_unique<Conexao>(); // 'c' é std::unique_ptr<Conexao>
c->enviar("olá"); // use -> como um ponteiro comum
(*c).enviar("mundo"); // ou * para desreferenciar
// Nenhum delete escrito! Ao sair de escopo, o unique_ptr destrói a Conexao.
} // ← "conexão fechada" impressa aqui, automaticamente
int main() {
trabalha();
std::cout << "fim\n";
return 0;
}
Saída:
conexão aberta
enviando: olá
enviando: mundo
conexão fechada
fim
Você usa c-> e *c exatamente como um ponteiro cru, mas não há delete. E — ponto crucial — se uma exceção fosse lançada entre a criação e o fim da função, a Conexao ainda seria destruída, porque a limpeza está amarrada ao objeto unique_ptr, não a uma linha de código. É RAII aplicado à memória do heap.
O "unique" é levado a sério: posse exclusiva
O nome não mente: um unique_ptr é o dono único do recurso. Isso é imposto pelo compilador — um unique_ptr não pode ser copiado. Faz sentido: se pudesse, dois donos apontariam para o mesmo objeto e ambos tentariam deletá-lo (o double free do artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco). O compilador simplesmente proíbe:
auto a = std::make_unique<int>(42);
// auto b = a; // ERRO de compilação: unique_ptr não é copiável
O que você pode fazer é transferir a posse com std::move — entregando o recurso de um dono a outro, deixando o primeiro vazio:
#include <memory>
#include <iostream>
int main() {
auto a = std::make_unique<int>(42);
std::cout << "a aponta para " << *a << '\n';
auto b = std::move(a); // TRANSFERE a posse de a para b
// Agora b é o dono; a ficou vazio (nulo).
if (!a) std::cout << "a agora está vazio\n";
std::cout << "b aponta para " << *b << '\n';
return 0; // b destrói o int; a não faz nada (já é nulo)
}
Isso é exatamente o construtor/atribuição de movimento do artigo anterior, agora como sua ferramenta diária: o unique_ptr implementa a Regra dos Cinco internamente — cópia proibida, movimento permitido —, e você colhe o benefício sem escrever nada. Repare no elo com o Quando Copiar Dá Errado: a intuição de "mover é transferir posse" agora tem um rosto concreto.
Onde ele brilha: posse dentro de classes e devolução de fábricas
Dois usos são onipresentes. O primeiro: um unique_ptr como membro de classe faz a Regra do Zero funcionar mesmo quando a classe possui um objeto do heap — a classe não precisa de destrutor, pois o unique_ptr membro se encarrega. O segundo: funções-fábrica que criam um objeto e devolvem sua posse ao chamador, com clareza sobre quem manda no tempo de vida:
#include <memory>
#include <iostream>
#include <string>
struct Widget {
std::string nome;
Widget(std::string n) : nome(std::move(n)) {}
};
// Fábrica: cria um Widget e ENTREGA sua posse a quem chamou.
std::unique_ptr<Widget> cria_widget(std::string nome) {
return std::make_unique<Widget>(std::move(nome));
// O retorno é movido para o chamador — barato e claro.
}
int main() {
auto w = cria_widget("botão"); // 'w' passa a ser o dono
std::cout << "criado: " << w->nome << '\n';
return 0; // w destrói o Widget automaticamente
}
A assinatura std::unique_ptr<Widget> como retorno comunica algo que um Widget* cru jamais comunicaria com clareza: "eu criei isto e estou te passando a posse; agora a responsabilidade de destruí-lo é sua — e o unique_ptr cuidará disso por você". Em C, devolver um ponteiro cru sempre deixava no ar a dúvida "quem libera isto?". Aqui a resposta está no tipo.
A honestidade sobre limites
unique_ptr é o ponteiro inteligente que você deve alcançar por padrão — leve (não custa mais que um ponteiro cru), sem sobrecarga de tempo de execução, e cobrindo a esmagadora maioria dos casos de posse no heap. Mas ele impõe um dono só. E quando o modelo do problema exige que várias partes compartilhem a posse de um mesmo recurso, sem que nenhuma seja "a dona"? Para isso o unique_ptr não serve — e forçá-lo seria errado. Existe outro ponteiro inteligente para esse cenário, com um custo que precisa ser compreendido antes de adotado. Ele é o assunto do próximo artigo.
O unique_ptr não é um ponteiro com enfeites: é a posse transformada em tipo. Ele não pode ser copiado, apenas movido — e essa proibição, incômoda no começo, é justamente o que torna impossível haver dois donos do mesmo recurso. Daí a preferência por make_unique em vez do new solto, e o std::move explícito quando a posse muda de mãos: o código passa a declarar quem é o dono em cada momento, informação que em C ficava por conta do comentário — e do comentário desatualizado.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/memory/unique_ptr: a referência completa de
std::unique_ptr, incluindomake_unique,release,resete o suporte a deletores personalizados. - Scott Meyers, Effective Modern C++ (2014), Item 18 ("Use
std::unique_ptrfor exclusive-ownership resource management"): o argumento definitivo a favor do uso padrão que adotamos. - Scott Meyers, Effective Modern C++, Item 21 ("Prefer
std::make_uniqueandstd::make_sharedto direct use ofnew"): por que preferir as funções-fábrica aonewexplícito. - Bjarne Stroustrup, A Tour of C++ (3ª ed.), seção sobre ponteiros inteligentes: a colocação do
unique_ptrno centro da gestão de recursos moderna. - ISO C++ Core Guidelines, regras R.20 ("Use
unique_ptrorshared_ptrto represent ownership") e R.22/R.23: as diretrizes oficiais sobre posse e ponteiros inteligentes.
Exercícios
Exercício 1
Reescreva o trecho de estilo antigo abaixo usando std::make_unique, eliminando o delete manual. Explique por que a nova versão não vaza mesmo se uma exceção ocorrer no meio.
struct Recurso { Recurso(){} ~Recurso(){} void usa(){} };
void f() {
Recurso* r = new Recurso();
r->usa();
delete r;
}
Ver resposta
✓ Resposta: A versão moderna:
#include <memory>
struct Recurso { Recurso(){} ~Recurso(){} void usa(){} };
void f() {
auto r = std::make_unique<Recurso>();
r->usa();
// sem delete: o unique_ptr destrói o Recurso ao sair de escopo
}
Ela não vaza mesmo com exceção porque a destruição do Recurso está amarrada ao fim de vida do objeto r. Se uma exceção for lançada em r->usa(), a pilha é desenrolada, r sai de escopo, e seu destrutor (que deleta o Recurso) roda durante esse desenrolar. No código antigo, uma exceção antes do delete r; pularia essa linha e vazaria.
Exercício 2
O código a seguir não compila. Diga por quê e mostre como transferir a posse corretamente.
#include <memory>
auto a = std::make_unique<int>(1);
auto b = a; // erro?
Ver resposta
✓ Resposta: Não compila porque std::unique_ptr não é copiável — auto b = a; tentaria copiar, o que criaria dois donos do mesmo int. A intenção legítima é transferir a posse com std::move:
#include <memory>
auto a = std::make_unique<int>(1);
auto b = std::move(a); // a posse passa de a para b; a fica nulo
Exercício 3
Escreva uma função-fábrica std::unique_ptr<std::string> cria_saudacao(const std::string& nome) que devolve um std::string no heap com o texto "Olá, <nome>". Demonstre o uso e comente o que a assinatura comunica sobre posse.
Ver resposta
✓ Resposta: A fábrica:
#include <memory>
#include <string>
#include <iostream>
std::unique_ptr<std::string> cria_saudacao(const std::string& nome) {
return std::make_unique<std::string>("Olá, " + nome);
}
int main() {
auto s = cria_saudacao("Marcelo");
std::cout << *s << '\n'; // Olá, Marcelo
return 0;
}
A assinatura, ao devolver std::unique_ptr<std::string>, comunica que a função cria o objeto e entrega sua posse ao chamador — que passa a ser o único responsável por seu tempo de vida, com destruição automática garantida. Um std::string* cru deixaria "quem deleta?" no ar.
Exercício 4
Após auto p = std::make_unique<int>(7); auto q = std::move(p);, qual é o estado de p? Escreva um if que comprove esse estado e explique por que desreferenciar p agora seria um erro.
Ver resposta
✓ Resposta: Após o movimento, p fica vazio (nulo), pois a posse foi transferida para q:
#include <memory>
#include <iostream>
int main() {
auto p = std::make_unique<int>(7);
auto q = std::move(p);
if (!p) std::cout << "p está vazio\n"; // esta linha executa
std::cout << *q << '\n'; // 7
return 0;
}
Desreferenciar p agora (*p) seria erro porque p não aponta para nada — é o equivalente a desreferenciar nullptr, comportamento indefinido. Depois de mover, o objeto de origem só pode ser destruído ou reatribuído, não usado como se ainda possuísse o recurso.
Exercício 5
Uma classe Janela possui um Botao alocado no heap. Escreva-a de duas formas — uma com Botao* cru (e todo o cuidado da Regra dos Cinco) e outra com std::unique_ptr<Botao> — e argumente por que a segunda é preferível, ligando à Regra do Zero do artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco.
Ver resposta
✓ Resposta: As duas formas:
// Forma A: Botao* cru — exige toda a disciplina da Regra dos Cinco.
struct Botao {};
class JanelaA {
public:
JanelaA() : b_(new Botao()) {}
~JanelaA() { delete b_; }
JanelaA(const JanelaA&) = delete; // teria de definir cópia...
JanelaA& operator=(const JanelaA&) = delete; // ...movimento, etc.
private:
Botao* b_;
};
// Forma B: unique_ptr — Regra do Zero, nada a escrever.
#include <memory>
class JanelaB {
public:
JanelaB() : b_(std::make_unique<Botao>()) {}
// sem destrutor, sem cópia/movimento manuais: o unique_ptr cuida de tudo
private:
std::unique_ptr<Botao> b_;
};
A forma B é preferível porque o std::unique_ptr membro implementa corretamente destruição e movimento por dentro, então JanelaB não precisa de nenhuma das cinco funções especiais — é a Regra do Zero do artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco concretizada. A forma A obriga você a escrever (e manter corretas) a liberação e as regras de cópia/movimento à mão, cada uma uma oportunidade de bug. Menos código, mais segurança.