CodeGym /Cursos /C++ SELF /std::unique_ptr — get() / reset() / release()

std::unique_ptr — get() / reset() / release()

C++ SELF
Nível 43, Lição 1
Disponível

1. Fronteira da propriedade

Antes de apertar os botões get(), reset() e release(), vale parar por meio segundo e fazer a si mesmo uma pergunta adulta: “quem será o proprietário do recurso após esta operação?”.

Em C++ erros de propriedade raramente aparecem como “uma bela mensagem de erro” — normalmente são vazamentos, quedas repentinas ou “parece funcionar, mas às vezes não” (o que é especialmente perigoso).

Imagine que o recurso é a única chave do apartamento. unique_ptr é a pessoa que guarda essa chave e se compromete a trancar o apartamento quando sair. Então get() é “dar a chave para olhar, mas não entregar”, reset() é “jogar a chave fora e trancar o apartamento (ou trocar de apartamento)”, e release() é “entregar a chave para outra pessoa para sempre e não responder mais pelo apartamento”. E é exatamente nesse último ponto que as pessoas normalmente cometem erros dramáticos.

Vamos desenhar um fluxograma simples de escolha de operação:

flowchart TD
    A["Quero fazer algo com unique_ptr"] --> B{"Precisa trocar de dono?"}
    B -->|Não, só acesso| C["get() (observar)"]
    B -->|Sim| D{"Quem será o dono após a operação?"}
    D -->|Ninguém: recurso deve desaparecer ou ser substituído| E["reset() (destruir/substituir)"]
    D -->|Outra pessoa, mas não um unique_ptr| F["release() (entregar para fora) + plano imediato de posse"]

A ideia principal: get() não altera o proprietário, reset() destrói (ou substitui) o recurso no proprietário atual, release() faz o proprietário ser… alguém fora, e isso é perigoso.

2. get(): “dar para ver”

Quando você vê pela primeira vez p.get(), a mão pode querer tratá‑lo como “ah, é só um ponteiro cru, então posso fazer qualquer coisa com ele”. E é aí que o C++ sorri maliciosamente.

get() de fato retorna T*, mas semanticamente é um observador temporário: o acesso ao objeto é dado, e a responsabilidade pela liberação permanece com o unique_ptr.

Um cenário típico de uso de get() — você tem uma função que historicamente aceita um ponteiro cru (ou é mais conveniente expressar “nullable” assim), mas não deve deletar o objeto. Então você passa p.get(), e tudo bem — até o momento em que você tenta transformar esse ponteiro em um segundo proprietário ou guardá‑lo “para depois”.

Mini‑exemplo: temos uma “sessão” da aplicação que imprimimos. A impressão não deve possuir a sessão — ela apenas lê os dados.

#include <iostream>
#include <memory>
#include <string>

struct Session {
    std::string user;
};

void print_session(const Session* s) {
    if (!s) { std::cout << "no session\n"; return; } // no session
    std::cout << "user = " << s->user << '\n';       // user = Alice
}

E agora o uso de get():

#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    print_session(sess.get());
}

Note: print_session aceita const Session*, ou seja, “posso receber nullptr”. Esse é um bom contrato para observação. E sess permanece o proprietário.

Agora um anti‑exemplo importante, que compila, parece inocente, mas é logicamente criminoso:

#include <memory>

int main() {
    auto a = std::make_unique<int>(5);
    std::unique_ptr<int> b(a.get()); // PERIGOSO: segundo dono!
}

Por que isso é ruim: tanto a quanto b pensam que são donos do mesmo int. Quando ambos forem destruídos — ocorrerá dupla liberação. Ou seja, você destruiu com as próprias mãos o sentido do unique_ptr.

Outra armadilha “silenciosa” de get() — guardar o ponteiro por mais tempo do que a vida do proprietário garante. Se você pegou T* raw = p.get(), e em seguida em algum lugar acima no código fez p.reset(), então raw agora aponta para memória já liberada. O ponteiro cru não se tornará automaticamente nullptr, porque ele não é “esperto” (é só um endereço).

3. reset(): destruir ou substituir o recurso

reset() é uma operação que geralmente parece “colocar ordem”. Ela é necessária quando o proprietário deve ou soltar o recurso (ficar vazio), ou substituir o recurso por outro.

E isso soa simples — mas é útil lembrar: reset() não apenas muda o endereço dentro do unique_ptr, ele também executa a destruição do objeto antigo (se havia).

Em termos humanos reset() responde à pergunta: “eu, como proprietário, não quero mais possuir este objeto”. E então o objeto é destruído imediatamente, não “algum dia depois”. Por isso reset() é uma boa ferramenta quando você quer liberar o recurso antecipadamente (por exemplo, para liberar memória antes do fim da função).

Mini‑exemplo: temos uma sessão opcional. Ao executar “logout” queremos remover a sessão.

#include <iostream>
#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess.reset();                       // sessão destruída, sess vazio
    std::cout << std::boolalpha << !sess << '\n'; // true
}

Agora a variante “substituir o recurso”. Imagine que o “login” recria a sessão.

#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess = std::make_unique<Session>(Session{"Bob"}); // Session antigo destruído
}

Tecnicamente aqui não usamos explicitamente reset(), mas semanticamente acontece o mesmo: o recurso antigo é liberado, o novo é adotado. Também é possível escrever assim:

#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess.reset(new Session{"Bob"}); // funciona, mas make_unique costuma ser melhor
}

Por que std::make_unique é melhor — já discutimos antes: deixa a intenção mais clara e reduz código manual de gerenciamento.

Importante entender o efeito: após reset() ou substituição, todos os “observadores” (ponteiros crus T*, obtidos via get() anteriormente) tornam‑se potencialmente perigosos. Não porque reset() seja ruim, mas porque você trocou/destruiu o objeto, e os observadores não sabem disso.

4. release(): entregar a propriedade para fora

release() é a mais traiçoeira das três operações. Parece “quase get()”, mas faz o oposto em termos de semântica.

Se get() dá um ponteiro cru sem transferir a propriedade, então release() entrega um ponteiro cru com transferência de propriedade: o unique_ptr deixa de ser dono e fica vazio (nullptr).

Ou seja, após release() a responsabilidade por delete volta a ser… sua. Sim, para aquele “você” que começou a usar unique_ptr justamente para não pensar em delete.

Portanto a regra é simples: release() só deve ser usada quando você tem certeza de quem será o próximo proprietário, e normalmente — imediatamente.

Mini‑exemplo para “mostrar a mecânica”:

#include <iostream>
#include <memory>

int main() {
    auto p = std::make_unique<int>(10);

    int* raw = p.release();                 // p vazio
    std::cout << (p == nullptr) << '\n';    // 1

    delete raw; // agora somos responsáveis por deletar (senão vazamento)
}

No código “moderno” real não é desejável fazer isso. Um cenário mais seguro — chamar release() apenas para em seguida passar a propriedade a outro dono (por exemplo, se você tem uma API que aceita T* e promete “agora eu sou dono”).

Por exemplo, podemos criar uma função “adotar e embrulhar de volta em unique_ptr”, para que na fronteira entre “mundo antigo/novo” não fiquemos com ponteiros crus por muito tempo:

#include <memory>

std::unique_ptr<int> adopt(int* p) {
    return std::unique_ptr<int>(p);
}

int main() {
    auto a = std::make_unique<int>(7);
    auto b = adopt(a.release()); // propriedade mudou, a ficou vazia
}

A ideia correta: o ponteiro cru existe por um período muito curto e imediatamente volta a ficar sob um dono RAII.

Repetindo: release() não é “apenas obter o ponteiro”. É “entregar a propriedade”.

Resumo rápido: get() vs reset() vs release()

Quando a cabeça cansa, uma tabela ajuda. O importante aqui não é “como parece”, mas “o que acontece com o proprietário” e “quem é responsável pela deleção”.

Operação O que retorna Muda o proprietário? Estado do unique_ptr depois Quem deleta o objeto
get()
T*
Não Não muda Ainda o unique_ptr
reset()
nada Sim (o proprietário muda/descarta o recurso) Normalmente
nullptr
(ou um novo recurso)
unique_ptr deleta o recurso antigo
release()
T*
Sim (a propriedade sai para fora) Torna‑se
nullptr
Aquele que recebeu o
T*
(manualmente ou um novo dono)

Essa lógica se encaixa bem na “regra de um só dono”: get() não adiciona um dono, reset() não adiciona dono (o dono é o mesmo, apenas o recurso muda), enquanto release() literalmente diz: “agora não há dono dentro do unique_ptr”.

5. Exemplo prático: sessão ToDo e fronteiras de propriedade

Para que o tema não fique puramente teórico, vamos continuar construindo nossa pequena aplicação de console (um ToDo/nota simples). Não vamos entrar em OOP pesado, então faremos struct e funções simples.

Começamos pelos dados: tarefas e sessão.

#include <string>
#include <vector>

struct Task {
    std::string text;
    bool done{};
};

struct Session {
    std::string user;
    std::vector<Task> tasks;
};

Agora faremos duas funções “observadoras”: uma imprime a sessão, outra imprime a contagem de tarefas. Elas aceitam const Session*, porque pode não haver sessão (por exemplo, usuário não logado).

#include <iostream>

void print_user(const Session* s) {
    if (!s) { std::cout << "guest\n"; return; }     // guest
    std::cout << s->user << '\n';                   // Alice
}

void print_task_count(const Session* s) {
    if (!s) { std::cout << 0 << '\n'; return; }     // 0
    std::cout << s->tasks.size() << '\n';           // 2
}

E agora em main criaremos um unique_ptr<Session> e daremos acesso seguro de “olhar” via get():

#include <memory>

int main() {
    std::unique_ptr<Session> sess; // vazio: não logado

    print_user(sess.get());

    sess = std::make_unique<Session>(Session{"Alice", {{"Buy milk", false}}});
    print_user(sess.get());
}

Aqui get() é a ferramenta ideal: não transferimos propriedade, apenas concedemos acesso de leitura.

Agora adicionemos “logout”, onde faz sentido usar reset():

#include <iostream>
#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice", {}});
    sess.reset();                                 // removemos a sessão
    std::cout << std::boolalpha << !sess << '\n'; // true
}

O sentido é simples: o proprietário é o mesmo (sess), mas ele ficou vazio, e o objeto foi destruído.

E finalmente — a situação em que alguém “de fora” quer tomar a propriedade. No design moderno seria melhor aceitar unique_ptr diretamente, mas suponha que temos uma API “legacy”:

#include <iostream>

void legacy_take_session(Session* s) {
    // Suponha que o sistema legacy promete: "eu vou deletar s"
    std::cout << "legacy got session\n"; // legacy got session
    delete s;
}

Então a fronteira fica assim:

#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice", {}});
    legacy_take_session(sess.release()); // entregamos a propriedade para fora
    // sess agora está vazio, e isso é normal
}

Aqui release() é justificado: você realmente entrega a propriedade para fora, e o próximo dono está claramente definido (a função legacy). Mas se a função legacy não deletar o objeto — haverá vazamento. Se ela deletar, e você depois tentar deletar de novo — haverá double‑free. Portanto release() exige disciplina e um contrato claro.

6. Erros típicos

Erro nº1: criar um segundo proprietário a partir de get().
Esse é um dos erros lógicos mais desagradáveis, porque parece “natural”: você obteve T* e o embrulhou em um novo unique_ptr. O problema é que o primeiro unique_ptr continua existindo e acha que é dono. Como resultado, dois donos tentarão liberar o mesmo recurso. O raciocínio correto: get() é apenas para olhar, não criamos um proprietário.

Erro nº2: usar um ponteiro cru após reset() ou substituição do recurso.
Isso acontece frequentemente por acidente: você salvou Task* t = ...get(), depois em algum ponto mais alto do código fez reset() (ou simplesmente atribuiu um novo unique_ptr), e o ponteiro virou pendente. O unique_ptr não é culpado: ele honestamente destruiu o objeto. A culpa é nossa, por esquecer que o observador não estende a vida do proprietário.

Erro nº3: tratar release() como “apenas obter T*”.
release() é “eu não sou mais dono, levem e cuidem”. Se após release() você não passar o ponteiro para um novo proprietário nem deletá‑lo manualmente, o vazamento é quase garantido. Por isso release() é usado raramente e geralmente “na fronteira” com APIs antigas, onde a propriedade é representada por ponteiros crus.

Erro nº4: pensar que após release() o unique_ptr ainda pode ser desreferenciado.
Após release() o unique_ptr fica vazio (nullptr). Ele é válido como objeto (pode ser reatribuído), mas desreferenciar *p ou usar p->field já não é permitido sem checagem. É a mesma disciplina de pós‑std::move: a fonte normalmente fica vazia e deve ser tratada como tal até prova em contrário.

Erro nº5: misturar “quem possui” e “quem usa” em uma função sem um contrato.
Por exemplo, uma função aceita Session* e às vezes faz delete internamente e às vezes não. De fora não é possível entender quem é responsável pelo recurso. Como resultado, uma chamada causa vazamento, outra causa double‑free. Mesmo quando você é forçado a trabalhar com ponteiros crus, mantenha uma regra clara: ou a função sempre possui e sempre deleta, ou nunca deleta (e isso deve ficar claro pelo design/nome/documentação). unique_ptr ajuda justamente a tornar essas fronteiras óbvias.

1
Tarefa
C++ SELF, nível 43, lição 1
Bloqueado
Observador do sensor
Observador do sensor
1
Tarefa
C++ SELF, nível 43, lição 1
Bloqueado
Entrada e saída
Entrada e saída
1
Tarefa
C++ SELF, nível 43, lição 1
Bloqueado
Tokens vivos
Tokens vivos
1
Tarefa
C++ SELF, nível 43, lição 1
Bloqueado
Módulo legado
Módulo legado
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION