C++ Smart Pointers

Smart Pointers

Smart Pointers são os recursos que mais me agradam no C++. Com C, cabe ao programador manter os detalhes de gestão do ponteiro no contexto e, quando isso não acontece, os erros pipocam por toda parte. São incontáveis as vezes que me deparei com bugs, exceptions, etc devido a um ponteiro solto. E os problemas mais prováveis são:

  • Leaks de memória
  • Liberação da memória oque não deveria ser liberado
  • Liberação da memória de forma inapropriada (free() contra align_free(), delete contra delete[])
  • Uso de memória ainda não foi alocada
  • A “memória de pensamento” (thinking memory) alocada depois de ser liberada porque o ponteiro em si não foi atualizado para NULL

O C++ fornece três tipos de smart pointers que cuidam desses problemas de forma automática:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

Principais benefícios dos Smart Pointers

A maioria dos problemas de ponteiros surge porque os programadores devem manter o tracking do ponteiro o tempo todo e de forma completa – esquecer que uma parte, uma alocação, ou realocação ou ainda a liberação fatalmente resultará em erro.

Com os smart pointers C++, os detalhes são gerenciados para você por trás das cenas:

  • Se um smart pointer ficar fora do escopo, o deletador apropriado será chamado automaticamente. A memória não é deixada pendurada
  • Os smart pointers serão automaticamente definidos como nullptr quando não inicializados ou quando a memória for liberada.
  • As contagens de referência são tratadas automaticamente para std::shared_ptr
  • Se uma função especial delete ou free() precisar ser chamada, ela será especificada no tipo e declaração do ponteiro, e será automaticamente chamada em delete.

std::unique_ptr

std::unique_ptr é uma construção que deve ser usada quando uma parte da memória pode ter apenas um proprietário no momento. É um container leve, que não requer uso de memória para usá-lo:
sizeof(std::unique_ptr) == sizeof(int *)

Ao inicializar std::unique_ptr, use memória que ainda não esteja alocada. Usar memória alocada anteriormente pode resultar em comportamento inesperado – a memória pode ser liberada inesperadamente.

std::unique_ptr<uint32_t> x(new uint32_t(0xffffffff));
uint32_t t = 0x00100000;
x = &t; // resulta em erro de compilacao

O objeto std::unique_ptr não pode ser copiado – ele só tem movimento semântico.

std::unique_ptr<uint32_t> x, y(new uint32_t(0xdedededed));
x.reset(new uint32_t(0xf00));
x = y; // resulta em erro de compilacao

Pode-se excluir o ponteiro como normalmente se faria. Também pode-se obter acesso ao ponteiro bruto gerenciado por std::unique_ptr.

if(x) {
    //x.get() o comportamento eh indefenido se x == nullptr
    uint32_t z = *(x.get());
    // abaixo é equivalente
    z = *x;
}

std::unique_ptr chama delete por padrão, mas pode-se especificar um delete personalizado. O tipo do delete que se deseja usar faz parte do tipo de ponteiro, portanto, não se pode misturar e combinar ponteiros de diferentes esquemas de alocação.

// funcao lambda de delacao
auto uint32_deleterrrrr = [](uint32_t * x) {
    printf("Aplicando delete em: %p\n", x);
    delete x; 
};

//o deletor faz parte do tipo de ptr
std::unique_ptr< uint32_t, decltype(uint32_deleterrrrr)> 
    x(new uint32_t(0xabcdefed), uint32_deleterrrrr);

STD::UNIQUE_PTR<T[]>

std::unique_ptr também tem um tipo de matriz (std::unique_ptr <T[]>). Dependendo do seu uso, também deve-se considerar std::vector e std::array para gerenciar conjuntos de dados indexáveis.

STD::MAKE_UNIQUE

std::make_unique fornece uma maneira de construir e retornar um objeto std::unique_ptr.

Class myObject {...};
auto v = std::make_unique<myObject>();

std::make_unique é um recurso novo do C++ 14, ele não tem suporte no C++ 11.

Observe que std::make_unique não permite o uso de um deletor personalizado.

std::shared_ptr

std::shared_ptr é o tipo de ponteiro a ser usado para memória que pode pertencer a vários recursos ao mesmo tempo. std::shared_ptr mantém uma contagem de referência de objetos de ponteiro. Dados gerenciados por std::shared_ptr só são liberados quando não há objetos restantes apontando para os dados.

std::shared_ptr é mais custoso que std::unique_ptr. std::shared_ptr é 2x o lado de um ponteiro descoberto, uma vez que armazena um ponteiro para o bloco de controle e um ponteiro para o objeto. Todos os objetos std::shared_ptr e std::weak_ptr que apontam para a mesma memória compartilharão um único bloco de controle.

Além da penalidade de tamanho, há também uma penalidade de acesso ao usar std::shared_ptr o overhead do gerenciamento do bloco de controle e da utilização de operações atômicas ao acessar o std::shared_ptr.

std::shared_ptr é inicializado da mesma maneira que std::unique_ptr

std::shared_ptr<uint32_t> x(new uint32_t(0xabcdefed));

Ao contrário de std::unique_ptr, as operações de cópia são permitidas com std::shared_ptr. Para fazer um segundo ponteiro compartilhado para os mesmos dados, passa-se o objeto std::shared_ptr para o construtor:

std::shared_ptr<uint32_t> y(x); // copia de x
x = y; // copia de y

Ao inicializar um std::shared_ptr, a mesma regra se aplica: “usar a memória que ainda não está alocada. Usar memória alocada anteriormente pode resultar em comportamento inesperado – a memória pode ser liberada inesperadamente”.

uint32_t * t = new uint32_t(0x1234abcd);
std::shared_ptr<uint32_t> a(t); // correto 
std::shared_ptr<uint32_t> b(t); // casca de banana

Como std::unique_ptr, std::shared_ptr chama delete por padrão – mas pode-se especificar um deletador customizado. O tipo do deletador que deseja-se usar não é parte do tipo de ponteiro para std::shared_ptr.

// funcao lambda para deletador
auto uint32_deleterrr = [](uint32_t * x) {
   printf("delatando: %p\n");
    delete x; };
std::shared_ptr<uint32_t>
   x(new uint32_t(0xf00ba7), uint32_deleterrr);

Ao contrário de std::unique_ptr com seu tipo de matriz, std::shared_ptr é usado apenas para representar objetos únicos.

STD::ALLOCATE_SHARED

std::allocate_shared é usado quando um alocador customizado é necessário para um std::shared_ptr. std::allocate_shared usa o Allocator especificado e passa os argumentos restantes para o construtor do objeto.

std::shared_ptr ptr = std::allocate_shared(align_new, arg1, arg2);

STD::MAKE_SHARED

Ao contrário de std::make_unique, que só está disponível em C++ 14, o std::make_shared está disponível em C++ 11.

class Thingy;
std::shared_ptr<Thingy> pThingy(new Thingy());
auto pThingy(std::make_shared<Thingy>()); 

Observe que o std::make_shared não permite o uso de um deletador customizado.

SHARED_FROM_THIS

Muitas vezes é desejável fazer um std::shared_ptr para um objeto usando o ponteiro this. Como mencionado em um exemplo de código acima, o perigo que vem do uso de ponteiros brutos para inicializar o std::shared_ptr se aplica a isso também.

C++ 11 trabalha em torno deste problema utilizando shared_from_this. Ao criar uma classe, derivar de std::enable_shared_from_this. Isso habilitará uma função de membro chamada shared_from_this() que pode ser usada para fornecer acesso seguro ao ponteiro this, impedindo blocos de controle duplicados para o mesmo objeto.

class Thingy : public std::enable_shared_from_this<Thingy>

Thingy::shared_from_this();
listOfThings.emplace_back(shared_from_this());

A única ressalva ao usar shared_from_this é que ele requer que um std::shared_ptr seja criado uma vez antes de ser chamado. Recomenda-se fazer este std::shared_ptr no construtor.

std::weak_ptr

Um std::weak_ptr é simplesmente um std::shared_ptr que é permitido balançar. std::weak_ptr é o mesmo tamanho que std::shared_ptr.

Um std::weak_ptr é criado usando um std::shared_ptr:

std::shared_ptr<uint32_t> x(new uint32_t(0xabcdefde));
std::weak_ptr<uint32_t> y(x); // onde y é um ptr fraco para x

std::weak_ptr não afeta a contagem de referência primária. Os dados serão liberados quando a contagem de referência std::shared_ptr atingir 0:

x = nullptr; // referente cnt = 0, delete object, y agora solto

Você pode verificar se os dados apontados pelo std::weak_ptr são válidos:

if(y.expired()) {...} // ainda um ptr valido? (aqui nao)

std::weak_ptr não tem suporte para operações de “desreferenciação”. Para acessar os dados, você deve primeiro converter o std::weak_ptr em um objeto std::shared_ptr:

// para uso real do ptr, deve converter para shared_ptr
std::shared_ptr<uint32_t> z = y.lock(); // null se expirado
auto t(y); // similar, mas dispara uma excecao se expirada

Conversão entre ponteiros

// prototype
std::unique_ptr<uint32_t> generate_uint(void) { ... } 

// unique_ptr
std::unique_ptr<uint32_t> x(new uint32_t(0xabcdefed));
// converte unique_ptr para shared_ptr.
std::shared_ptr<uint32_t> y = std::move(x);
y = generate_uint();
// converte weak_ptr para shared_ptr
std::shared_ptr<uint32_t> z = y.lock();
auto t(y); // similar, mas dispara uma excecao se estiver expirada

Note que não há opções para converter longe de um std::shared_ptr. Você está preso com isso para sempre!

*** A OctalMind é uma empresa especializada no desenvolvimento de sistemas de alta tecnologia.