Para ver o artigo anterior da série: Auto Pointer (auto_ptr)
Finalmente vamos ver um verdadeiro smart pointer
. O boost shared pointer é um smart pointer que gerencia a vida dos objetos utilizando contagem de referencias.
A contagem de referencias nada mais é do que um contador associado a cada objeto. Toda vez que um smart pointer passa a apontar para um objeto, ele incrementa esse contador, quando o smart pointer é destruído ou passa a apontar para outro objeto, o contador é decrementado. E por fim, o smart pointer toda vez que decrementar o contador deve verificar o seu valor, se chegou a zero, então ninguém mais referencia aquele objeto, logo ele pode ser destruído.
Vamos ao primeiro exemplo:
#include <boost/shared_ptr.hpp> int main(int argc, char **argv) { using namespace boost; shared_ptr<int> p(new int(5)); //contador de referencias agora é 1 *p = 3; //alterando o valor do inteiro shared_ptr<int> p2 = p; //contador de referencias é 2, p e p2 apontam para o mesmo objeto return 0; //p2 e p sao destruidos, contador de referencias chega a zero e int tambem é destruido. }
Simples não? Agora vamos criar um pequeno objeto e usa-lo com o shared pointer:
#include <boost/shared_ptr.hpp> #include <string> class Person { public: inline void SetName(const std::string &name) { m_Name = name; } private: std::string m_Name; }; int main(int argc, char **argv) { using namespace boost; shared_ptr<Person> p(new Person()); p->SetName("Bruno"); return 0; }
Note que não existem diferença nenhuma de uso em relação a um ponteiro normal (assim como os outros smart pointer que vimos anteriormente). Agora vamos fazer uma pequena mudança na classe pessoa:
#include <boost/shared_ptr.hpp> #include <string> class Person; class Person { public: typedef boost::shared_ptr<Person> PersonPtr_t; inline void SetName(const std::string &name) { m_Name = name; } inline void SetFilho(PersonPtr_t ptr) { m_Filho = ptr; } inline void SetPai(PersonPtr_t ptr) { m_Pai = ptr; } private: std::string m_Name; PersonPtr_t m_Filho; PersonPtr_t m_Pai; }; int main(int argc, char **argv) { Person::PersonPtr_t pai(new Person()); pai->SetName("Pai"); Person::PersonPtr_t filho(new Person()); filho->SetName("Filho"); pai->SetFilho(filho); return 0; }
Agora criamos dois objetos dinamicamente e ainda associamos um com o outro (pai passou a possuir uma referencia para Filho). No final da execução da função main, o primeiro a ser destruído é o ponteiro filho (lembre-se, em C++ a destruição de variáveis locais é sempre feita na ordem inversa de criação). O ponteiro filho vai decrementar o contador de referencias que vai chegar a um, pois o objeto “Pai” possui também um ponteiro para Filho. Em seguida, o ponteiro pai vai ser destruído, como seu contador de referencias chega a zero, ele destrói o objeto “Pai”, em consequência o destrutor de “Pai” é executado, forçando a destruição das suas variáveis membros, o que significa que o membro “m_Filho” é destruído, nesse momento o contador de referencias do filho finalmente chega a zero e ele é então destruído!.
A sequência de destruição fica parecida com:
filho.~shared_ptr() DecrementaContador(); pai.~shared_ptr() DecrementaContador(); Destroy() Person::~Person() m_Filho::~shared_ptr() DecrementaContador() Destroy() Person::~Person() //Destrutor do filho é executado
Note que ainda não usei o método SetPai, que vai ser usado no próximo exemplo.
Referências Circulares
No exemplo anterior não utilizei o método SetPai porque ele causa um problema fatal (que vai nos gerar um memory leak):
int main(int argc, char **argv) { Person::PersonPtr_t pai(new Person()); pai->SetName("Pai"); Person::PersonPtr_t filho(new Person()); filho->SetName("Filho"); pai->SetFilho(filho); //contador de filho chega a 2 filho->SetPai(pai); //contador de pai chega a 2 return 0; }
No final do bloco, quando o ponteiro filho é destruído, ele decrementa o contador de referências do filho, que chega a um, nesse caso, o objeto filho não é destruído (lembre-se, o objeto pai também possui uma referência). Em seguida, o ponteiro pai é destruído, ele vai então decrementar o contador de referencia do pai, que também chega a um, logo ele também não é destruído!!!
Resultado: nosso programa não possuí mais nenhuma referência para os objetos, mas como um referência o outro, a contagem de referencia nunca chega a zero, logo eles nunca são destruídos!! É como aquele casal de namorados chatos no telefone:
Ela: Desliga amor
Ele: Não desliga você mor!
Ela: Só desligo depois de você amor!
Ele: Eu também só vou desligar depois de você mor!
…
Overhead
O shared pointer possui um pequeno overhead toda vez que ele é copiado (pois tem que ficar administrando o contador de referencias). Mas o maior overhead, que pode pesar para algumas aplicações (como jogos em consoles) é a própria criação do contador de referencias.
Toda vez que criamos um novo objeto e associamos ele a um shared_ptr, um objeto para controle de referencias é criado (que é bem pequeno, deve ser do tamanho de um ou dois ponteiros). Mas temos o custo da alocação de memória extra e da possível fragmentação (não sei se a boost internamente usa algum pool para minimizar isso). Mas para a maioria das aplicações, isso é irrelevante.
Na pagina da boost existe uma parte da documentação que mostra o overhead: Smart Pointer Timings.
! Conclusões
Os smart_pointers da boost são excelentes e resolvem muitos dos problemas que enfrentamos no dia dia, eu recomendo muito o seu uso, é muito melhor do que fazer o seu próprio, pois o shared_ptr resolve vários problemas que não tratamos aqui:
- Exception safe
- Multithread: é possível que varias threads compartilhem o mesmo objeto, não o mesmo shared_ptr. E isso não torna o seu objeto thread safe, esse problema é do programador!
- Casts (que vamos ver no futuro)
- Já foi testado, testado, usado, consertado, …, etc
No próximo post vamos explorar o problema da referencia circular e conhecer mais um smart pointer da boost.
Próximo artigo da série: Boost Weak Pointer
agosto 18, 2008 às 18:40 |
O shared_ptr é muito útil porque permite ao programador C++ alguns dos benefícios que se tem quando se programa em Java, como o fato de não ter de se preocupar explicitamente com a destruição de objetos alocados. Muito bom o seu post.
agosto 19, 2008 às 9:01 |
Obrigado Edson! Outra grande vantagem dessas técnicas de smart ptr é que elas não ficam apenas restritas ao gerenciamento de memória, podemos utiliza-las para gerenciar qualquer recurso de um programa (conexões, handlers, locks, etc).
maio 8, 2009 às 7:51 |
[...] Shared Pointer, Weak Pointer e Scoped [...]