Um Dono, Um Recurso — o std::unique_ptr

Um Dono, Um Recurso — o std::unique_ptr

Aqui o delete cru sai de cena. O unique_ptr possui um ponteiro do heap, garante a liberação automática e impede por construção que dois donos apontem para o mesmo recurso — o que faz a Regra do Zero virar o modo padrão de escrever. Posse dentro de classes, devolução de fábricas e os limites honestos.
Linguagem C++

11 min de leitura

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, incluindo make_unique, release, reset e o suporte a deletores personalizados.
  • Scott Meyers, Effective Modern C++ (2014), Item 18 ("Use std::unique_ptr for 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_unique and std::make_shared to direct use of new"): por que preferir as funções-fábrica ao new explícito.
  • Bjarne Stroustrup, A Tour of C++ (3ª ed.), seção sobre ponteiros inteligentes: a colocação do unique_ptr no centro da gestão de recursos moderna.
  • ISO C++ Core Guidelines, regras R.20 ("Use unique_ptr or shared_ptr to 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ávelauto 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.

Comentários

Mais em Linguagem C++

Buscas Instantâneas — map, set e suas Versões Hash
Buscas Instantâneas — map, set e suas Versões Hash

Nem todo problema é sobre sequências; muitos são sobre associações, e em C…

Escreva uma Vez, Use com Todo Tipo — Templates de Função
Escreva uma Vez, Use com Todo Tipo — Templates de Função

Escrever a mesma função para int, double e string é o tipo de repetição que o…

Extensão — Dados que Sobrevivem: Integrando SQLite com RAII
Extensão — Dados que Sobrevivem: Integrando SQLite com RAII

Envolver a API C do SQLite numa classe é o exercício de RAII mais próximo do…