As diretrizes fundamentais de C++ estabelecidas por especialistas como Bjarne Stroustrup e Herb Sutter definem padrões para uso correto e eficiente de "C++ moderno", com foco em segurança de tipos, gerenciamento de recursos e prevenção de vazamentos.
Gerenciamento de Recuross com Smart Pointers
- Prefira
unique_ptroushared_ptrpara expressar propriedade de recursos - Use
unique_ptrcomo padrão; reserveshared_ptrpara quando houver propriedade compartilhada real - Construa
shared_ptratravés demake_shared() - Construa
unique_ptratravés demake_unique() - Empregue
std::weak_ptrpara resolver referências circulares entreshared_ptr - Passe smart pointers como parâmetros apenas quando a semântica de tempo de vida precisar ser explícita
- Siga as convenções da biblioteca padrão ao criar smart pointers customizados
- Use
unique_ptr<T>como parâmetro para transferir propriedade - Use
unique_ptr<T>&como parâmetro para indicar reatribuição do ponteiro - Use
shared_ptr<T>como parâmetro para compartilhar propriedade explicitamente - Use
shared_ptr<T>&quando houver possibilidade de reatribuir o ponteiro compartilhado - Use
const shared_ptr<T>&quando o objeto pode ter seu contador de referência incrementado - Evite repassar ponteiros ou referências obtidos de "aliases" de smart pointers
Representando Propriedade com Smart Pointers
O uso de smart pionters elimina vazamentos de memória decorrentes de gerenciamento manual.
void processar()
{
// incorreto: risco de vazamento se uma exceção ocorrer
Objeto* bruto { new Objeto };
// correto: propriedade exclusiva
auto exclusivo = std::make_unique<Objeto>();
// correto: propriedade compartilhada
auto compartilhado = std::make_shared<Objeto>();
}
Verificação estática: alerte quando o resultado de new for atribuído a um ponteiro bruto ou quando um ponteiro de propriedade retornado por função for armazenado em ponteiro bruto.
Preferindo unique_ptr sobre shared_ptr
unique_ptr possui semântica mais simples e previsível: o momento de destruição é determinístico e não há overhead de contador de referência.
Exemplo inadequado — overhead desnecessário:
void executar()
{
std::shared_ptr<Base> instancia = std::make_shared<Derivado>();
// uso local apenas; contador nunca passa de 1
} // destruição de instancia
Exemplo adequado:
void executar()
{
std::unique_ptr<Base> instancia = std::make_unique<Derivado>();
// uso local
} // destruição de instancia
Verificação estática: se uma função aloca um shared_ptr localmente sem repassá-lo, sugira unique_ptr.
Criando shared_ptr com make_shared()
make_shared produz código mais conciso e pode alocar o bloco de controle adjacente ao objeto, reduzindo overhead.
std::shared_ptr<Recurso> a { new Recurso{42} }; // inadequado
auto b = std::make_shared<Recurso>(42); // adequado
A versão com make_shared menciona Recurso apenas uma vez, sendo tipicamente mais curta e eficiente.
Verificação estática: alerte quando shared_ptr for construído diretamente de new.
Criando unique_ptr com make_unique()
make_unique oferece construção mais limpa e garante segurança excepcional em expressões compostas.
Nota:
make_uniquefoi introduzido no C++14, enquantomake_sharedjá existia no C++11.
// funcional, mas repetitivo
std::unique_ptr<Componente> c {new Componente{9}};
// preferível: sem repetição
auto d = std::make_unique<Componente>(9);
Verificação estática: alerte quando unique_ptr for construído diretamente de new.
Quebrando Referências Circulares com weak_ptr
Como shared_ptr baseia-se em contagem de referências, ciclos impedem que o contador chegue a zero. O weak_ptr resolve esse problema.
#include <memory>
class Noh;
class Cabeca {
public:
explicit Cabeca(const std::shared_ptr<Noh>& proximo)
: proximo_(proximo) {}
private:
std::shared_ptr<Noh> proximo_;
};
class Noh {
public:
explicit Noh(const std::weak_ptr<Cabeca>& antecessor)
: antecessor_(antecessor) {}
void executar() {
if (auto ref = antecessor_.lock()) {
// utilizar *ref
}
}
private:
std::weak_ptr<Cabeca> antecessor_;
};
Herb Sutter observa que "compartilhamento temporário de propriedade" descreve mais precisamente o mecanismo do que "quebrar ciclos".
Bjarne Stroustrup complementa: quebrar ciclos é o objetivo; o compartilhamento temporário é o meio de alcançá-lo.
Verificação estática: se um ciclo for detectado estaticamente, weak_ptr torna-se desnecessário.
Smart Pointers como Parâmetros
Passe smart pointers como parâmetros apenas quando precisar expressar semântica de tempo de vida explicitamente. Para uso geral, prefira T* ou T&.
Smart Pointers Não-Padrão
Qualquer tipo que sobrecarrega os operadores unários * e -> é considerado um smart pointer:
- Se for copiável, comportar-se como
shared_ptr - Se não for copiável, comportar-se como
unique_ptr
Exemplo inadequado:
#include <boost/intrusive_ptr.hpp>
void acionar(boost::intrusive_ptr<Controle> p) {
p->ativar();
}
p é um smart pointer compartilhado, mas a cópia por valor gera overhead desnecessário quando a função não participa do gerenciamento de tempo de vida. Nesse caso, use Controle& ou Controle*.
Transferindo Propriedade com unique_ptr<T>
Declarar um parâmetro como unique_ptr<T> documenta e impõe a transferência de propriedade para a função.
Reatribuição com unique_ptr<T>&
Declarar como unique_ptr<T>& indica que a função pode fazer o ponteiro apontar para outro objeto (reatribuição).
// assume a propriedade do recurso
void consumir(std::unique_ptr<Dado>);
// apenas utiliza o recurso
void apenas_usar(Dado*);
// pode reatribuir o ponteiro
void realocar(std::unique_ptr<Dado>&);
Exemplo inadequado:
// geralmente não é o pretendido
void confuso(const std::unique_ptr<Dado>&);
Verificação estática:
- Se uma função recebe
unique_ptr<T>&sem reatribuir em nenhum caminho, sugiraT*ouT&. - Se uma função recebe
const unique_ptr<T>&, sugiraconst T*ouconst T&.
Compartilhando Propriedade com shared_ptr<T>
Usar shared_ptr<T> como parâmetro indica explicitamente que a função compartilha a propriedade do objeto.
Reatribuindo com shared_ptr<T>&
class ConsumidorDeDado {
public:
explicit ConsumidorDeDado(std::shared_ptr<Dado> d) noexcept
: dado_{std::move(d)} {}
private:
std::shared_ptr<Dado> dado_;
};
void substituirDado(std::shared_ptr<Dado>& d) {
d = std::make_shared<Dado>(Dado{});
}
// compartilha propriedade, incrementa contador
void compartilhar(std::shared_ptr<Dado>);
// pode reatribuir
void realocar(std::shared_ptr<Dado>&);
// pode incrementar contador
void possivel_compartilhar(const std::shared_ptr<Dado>&);
Verificação estática:
- Se uma função recebe
shared_ptr<T>&sem reatribuir, sugiraT*ouT&. - Se uma função recebe
shared_ptr<T>por valor ouconst&sem copiá-lo/movê-lo, sugiraT*ouT&. - Se uma função recebe
shared_ptr<T>&&, sugira passar por valor.
Evitando Ponteiros de Aliases de Smart Pointers
Violar esta regra é a principle causa de perda de contagem de referência e ponteiros pendentes. Ao obter um ponteiro ou referência de um smart pointer, garanta que o smart pointer não seja resetado durante a árvore de chamadas.
std::shared_ptr<Dado> global = /* ... */;
void auxiliar() {
// se for o último shared_ptr, Dado é destruído
global = /* ... */;
}
void trabalhar(Dado& ref) {
auxiliar();
utilizar(ref); // comportamento indefinido
}
O código a seguir não passaria em revisão:
void meu_codigo() {
// INCORRETO: ref pode ficar pendente após auxiliar()
trabalhar(*global);
// INCORRETO: mesmo problema
global->processar();
}
Solução: criar uma cópia local para manter o contador acima de zero durante toda a execução:
void meu_codigo() {
// contador incrementado, válido durante toda a árvore de chamadas
auto ancora = global;
// CORRETO: ponteiro derivado de smart pointer local
trabalhar(*ancora);
// CORRETO: idem
ancora->processar();
}
Verificação estática:
- Alerte se um ponteiro ou referência usado em chamadas for derivado de um smart pointer não-local ou potencialmente alias.
- Para
shared_ptr, sugira criar uma cópia local antes de extrair o ponteiro ou referência.