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 |
|---|---|---|---|---|
|
|
Não | Não muda | Ainda o unique_ptr |
|
nada | Sim (o proprietário muda/descarta o recurso) | Normalmente (ou um novo recurso) |
unique_ptr deleta o recurso antigo |
|
|
Sim (a propriedade sai para fora) | Torna‑se |
Aquele que recebeu o (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.
GO TO FULL VERSION