Melhores Práticas com Smart Pointers em C++ Moderno

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_ptr ou shared_ptr para expressar propriedade de recursos
  • Use unique_ptr como padrão; reserve shared_ptr para quando houver propriedade compartilhada real
  • Construa shared_ptr através de make_shared()
  • Construa unique_ptr através de make_unique()
  • Empregue std::weak_ptr para resolver referências circulares entre shared_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_unique foi introduzido no C++14, enquanto make_shared já 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, sugira T* ou T&.
  • Se uma função recebe const unique_ptr<T>&, sugira const T* ou const 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, sugira T* ou T&.
  • Se uma função recebe shared_ptr<T> por valor ou const& sem copiá-lo/movê-lo, sugira T* ou T&.
  • 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.

Tags: cpp smart-pointers unique-ptr shared-ptr weak-ptr

Publicado em 7-17 16:51