quinta-feira, 25 de dezembro de 2008

C++: Quando usar cada Smart Pointer

auto_ptr, shared_ptr, scoped_ptr, … Qual utilizar ? E, principalmente, por quê ?

Primeiramente, vamos recordar o que é um Smart Pointer e quais são os recursos que geralmente utilizamos dele. Após isto, ficará mais fácil identificar qual deles usar.

Um Smart Pointer (SP) é uma classe template que guarda um ponteiro para um objeto que é dinamicamente alocado ou, sendo mais explícito, alocado usando new. Quando a instância da classe template é destruída, ela destrói o objeto para o qual aponta. Assim, o objeto apontado é destruído sem que o programador tenha que fazê-lo explicitamente, chamando delete.

Seu uso, logicamente, não fica restrito à esta função. Um Smart Pointer permite se passar pelo ponteiro para o objeto original, através da declaração dos operadores -> e *.

Veja um exemplo:

template< class T >
class SmartPointerSimples
{
public:
SmartPointerSimples(T *ptr = NULL) : _ptr( ptr ) {};
~SmartPointerSimples() { delete _ptr; };
T* operator ->() const { return _ptr; };
T& operator *() const throw( exception ) {
if ( _ptr != NULL ) return *_ptr;
else throw exception( "Ei, o ponteiro é nulo!" );
};
private:
SmartPointerSimples();
SmartPointerSimples(const SmartPointerSimples &);
SmartPointerSimples& operator =(
const SmartPointerSimples &
);

T *_ptr;
};



Suponha que haja uma classe Autor e que seja necessário criar um ponteiro para a mesma:



class Autor
{
public:
Autor() : _nome( "" ) {};
const string& Nome() const { return _nome; };
void DefinirNome(const string &nome)
{ _nome = nome; };
private:
// ...(outros construtores e operadores)
string _nome;
};



Assim, faríamos:



    SmartPointerSimples< Autor > autor( new Autor() );
autor->DefinirNome( "Thiago" );
cout << "Autor: " << autor->Nome() << endl;



Repare que o objeto autor é utilizado da mesma forma como se ele fosse um ponteiro comum. Como visto, isto se deve ao fato de implementarmos o operador –> (veja declaração acima).


Quando o SP autor é destruído, ele destrói a instância de Autor para o qual ele aponta (veja o destrutor de SmartPointerSimples).


Este é o comportamento mais básico de um SP. Ele funciona como um ponteiro, mas libera o programador da preocupação de gerenciar memória, fazendo uma espécie de “coleta automática de lixo”.


Transferência de Posse


E se quisermos passar o objeto apontado pelo Smart Pointer para outro Smart Pointer ou para um ponteiro ?


Para implementarmos a transferência de posse na classe SmartPointerSimples, poderíamos passar o construtor de cópia e o operador de atribuição para a parte pública da classe, implementá-los, acrescentar um método reset para redefinir o valor do ponteiro e mais um método release, para que a classe também pudesse se desfazer do objeto apontado, passando seu controle para outro objeto.


Veja alguns exemplos de como seria sua utilização:


// Transferência através do operador de atribuição
SmartPointerSimples< Autor > autorA( new Autor() );
SmartPointerSimples< Autor > autorB;
autorB = autorA;

// Transferência através de construtor de cópia
SmartPointerSimples< Autor > autorA( new Autor() );
SmartPointerSimples< Autor > autorB( autorA );

// Transferência através do método release...
SmartPointerSimples< Autor > autorA( new Autor() );
SmartPointerSimples< Autor > autorB;
// ...de smart pointer para smart pointer
autorB.reset( autorA.release() );
// ...ou para um ponteiro simples
Autor *pAutor = autorB.release();

A transferência de posse é útil em muitos casos, como quando desejamos passar a responsabilidade de destruição de um objeto para outro, explicitamente. Nestes casos, é altamente recomendado o uso de um SP justamente para indicar que a responsabilidade sobre o objeto foi passada adiante.


Posse Compartilhada


Quando o objeto apontado pelo SP é referenciado por diversos outros objetos em memória, o SP não deve destruí-lo. Isto porque estes outros objetos poderão tentar acessar este objeto posteriormente, o que provavelmente acarretará num erro.


Para impedir a destruição do objeto enquanto outros estiverem fazendo referência ao mesmo, o SP pode controlar seu acesso fazendo uma contagem de uso. Um contador é incrementado cada vez que o objeto é atribuído ou copiado através de seu construtor de cópia e decrementado quando se tenta destruí-lo ou se atribui outro objeto. Esta contagem de referências garante que o delete será chamado apenas quando o contador estiver em zero, indicando que não há mais nenhum outro objeto fazendo referência ao mesmo. Este é o lado mais “smart” de um Smart Pointer.


Transferência de Posse Vs. Posse Compartilhada


Smart Pointers que possuem Transferência de Posse não têm Posse Compartilhada (não têm contagem de referências). Ter Transferência de Posse implica em não poderem ser utilizados em containers padrão: vector, list, deque, etc. Isto porque os containers usam o construtor de cópia ou o operador de atribuição quando você armazena ou acessa algum elemento, respectivamente. Lembrando que, ao usá-los, você transfere a posse.


Desta forma, só use em containers os Smart Pointers que não tiverem Transferência de Posse. A menos que você saiba muito bem o que está fazendo…


Arrays


O uso de arrays é um caso à parte.  A destruição de um array implica em uma chamada a delete diferenciada (com [])


Autor autores[] = new Autor[ 10 ];
...
delete [] autores;

que garante a chamada do destrutor em cada objeto no array.


Assim, Smart Pointers não especializados não executam esse tipo de chamada e por isso não são capazes gerenciar bem arrays.


Analisando alguns Smart Pointers


scoped_ptr

Muitas vezes quando será desejado usar um SP não será preciso nem a transferência de posse nem a contagem de referências. Para estes usos mais simples, quando só queremos nos livrar da dor de cabeça de ter ponteiros não desalocados, podemos utilizar scoped_ptr.


O scoped_ptr é parte do TR1, fazendo-o ficar sob o namespace std::tr1. Após sair o novo padrão do C++ (09) voltará ao namespace std. Atualmente, existe uma implementação na biblioteca boost. Veja seu código aqui.


O scoped_ptr:



  • não é copiável

  • não possui transferência de posse

  • não possui posse compartilhada (contagem de referências) 

  • não gerencia bem arrays. Use scoped_array (abaixo) para isto.


scoped_array

É a versão de scoped_ptr especializada para gerenciamento de arrays. Veja seu código aqui.


auto_ptr

É o tipo de SP mais usado, haja vista ser parte do padrão vigente (C++98), estando disponível em qualquer compilador C++. Fica sob o namespace std. Veja sua documentação aqui.



  • possui todas as funcionalidades do scoped_ptr

  • possui transferência de posse (logo, não tem posse compartilhada)

  • não possui suporte a múltiplas threads

  • não gerencia bem arrays


shared_ptr

É parte da TR1 (std::tr1) e da boost. Veja seu código aqui.



  • possui todas as funcionalidades de auto_ptr

  • possui posse compartilhada (contagem de referências)

  • suporta múltiplas threads

  • não gerencia bem arrays. Use shared_array (abaixo) para isto.


shared_array

É a versão de shared_ptr especializada para o gerenciamento de arrays. Veja seu código aqui.


Outros


Separei o weak_ptr e o intrusive_ptr dos demais pois o uso deles ficará restrito à alguns casos. Vejamos:


weak_ptr

Em casos onde há referência cíclica entre ponteiros, eles não são desalocados automaticamente, pois essa relação de dependência é difícil de detectar.


Vamos supor a seguinte situação:


class Pai;
class Filho;

class Pai
{
public:
shared_ptr< Filho > filho;
};

class Filho
{
public:
shared_ptr< Pai > pai;
};

void FazAlgo()
{
shared_ptr< Pai > oPai( new Pai );
shared_ptr< Filho > oFilho( new Filho );

// Esta' feita a referencia circular:
oPai->filho = oFilho;
oFilho->pai = oPai;

...

// Ops! Chegando ao fim do bloco nenhum
// deles sera' desalocado!
}

O código acima irá causar problemas porque a contagem de referências de ambos continuará em 1. Os objetos não irão detectar a referência cruzada, logo, não irão decrementar o contador para que chegue a zero e o objeto apontado possa ser deletado. Este é um caso onde se deve usar weak_ptr.


O weak_ptr pode trabalhar em conjunto com shared_ptr para resolver problemas relacionados à posse e referências circulares. Diferentemente dos SP tradicionais, ele não destrói o objeto para o qual aponta quando seu destrutor é executado. E, principalmente, ele não é levado em consideração pelos shared_ptrs quanto à contagem de referências, ou seja, o shared_ptr irá deletar o objeto para o qual aponta mesmo que hajam weak_ptrs apontando para o mesmo.


Assim, para resolver o problema anterior, por exemplo, poderíamos substituir um dos shared_ptr (o da classe Filho por exemplo) por um weak_ptr. Como ele não seria levado em consideração na contagem de referências pelo shared_ptr, o contador iria chegar a zero e o objeto apontado seria destruído.


O weak_ptr:



  • não destrói o objeto para o qual aponta

  • não influencia na contagem de referências de um shared_ptr

  • não possui transferência de posse

  • possui posse compartilhada (contagem de referências)

  • trabalha junto à shared_ptr para resolver problemas de referência cíclica

  • não possui suporte à múltiplas threads


Veja seu código aqui.


intrusive_ptr

Quando objetos já possuem sua própria contagem de referências, é recomendado o uso de intrusive_ptr. O “intrusive” dele se refere a ele esperar que o objeto para o qual aponta implemente determinados métodos (ou funções amigas) para a contagem de referências:


// Incrementa a contagem
void intrusive_ptr_add_ref(T *p);
// Decrementa a contagem
void intrusive_ptr_release(T *p);

Estes métodos podem ser implementados na classe fazendo uma chamada para seus métodos reais de contagem de referências. Por exemplo, imagine que a classe Autor vista anteriormente possua contagem de referências e os métodos privados IncRef() e DecRef() que respectivamente incrementam e decrementam seu contador.


A implementação dos métodos para o intrusive_ptr poderiam ser simplesmente a chamada destes métodos:


inline void intrusive_ptr_add_ref(Autor *a)
{
a->IncRef();
}

inline void intrusive_ptr_release(Autor *a)
{
a->DecRef();
}

O intrusive_ptr:



  • funciona como um shared_ptr

  • usa a contagem de referências do objeto ao qual aponta ao invés de ter sua própria


Veja seu código aqui. Um uso comum de intrusive_ptr é em objetos COM. Neste modelo, os objetos possuem sua própria contagem de referências, que devem implementar ao herdar da interface IUnknown, através dos métodos AddRef() e Release().


Resumo


Vimos que cada Smart Pointer tem uma finalidade e que seu uso vai depender da necessidade. Para ajudar na decisão de qual utilizar, segue abaixo um pequeno diagrama:


SmartPointerAssinado


Considerações


Espero ter auxiliado a desmistificar um pouco o uso dos vários SP disponíveis. A introdução dos mesmos na C++0x irá abrir um novo leque de opções que auxiliará na resolução dos diversos problemas relacionados ao gerenciamento de memória. É certo que estas classes possam deixar algumas lacunas, precisando da intervenção do programador para a resolução de determinados problemas, mas para a maioria dos casos, as implementações propostas atendem plenamente.

Um comentário:

Rodrigo 'Skhaz' Delduca disse...

Otimo artigo, só faltou dizer que o "scoped_ptr" não possui 'deleter'
Abraços