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.

sábado, 13 de dezembro de 2008

Google Mock

A Google liberou seu C++ Mock Framework sob a nova licensa BSD, complementando seu Google C++ Testing Framework (ou somente Google Test). Ao que me parece, eles fizeram um trabalho bem completo, deixando o framework flexível e fácil de usar.

O Google Mock é baseado no EasyMock, no jMock e no Hamcrest, sendo construído para C++ sob o Google Test. Para utilizá-lo você precisará, além do Google Test que já vem incluso, da std::tr1::tuple, encontrada em algumas implementações de novos compiladores e na boost. Por enquanto ele está acoplado ao Google Test, mas há planos de retirar esta dependência.

Já vinha utilizando o MockPP e vendo o Google Mock tenho a impressão de seu uso ser um pouco mais simples. Assim que tiver um tempo (coisa rara), vou analisá-lo melhor e fazer uma breve comparação, talvez postando-a aqui no blog.

Mais informações sobre o Google Mock aqui.

sábado, 6 de dezembro de 2008

Funcionamento dos frameworks xUnit - Parte 1/2

Durante a série de artigos sobre TDD, tenho citado o uso de frameworks de testes unitários baseados na JUnit. Os exemplos que serão exibidos durante a série partem do princípio que o leitor conhece o funcionamento de um framework xUnit.
Sendo assim, coloco aqui uma explicação breve sobre a estrutura da maioria destes frameworks, de forma a facilitar o entendimento dos próximos artigos.


Os chamados frameworks xUnit surgiram do framework SUnit, construído para a linguagem Smalltalk, por Kent Beck e Erich Gamma. A partir daí, o framework foi portado para diversas linguagens (é praticamente certa a existência de um framework para a sua linguagem favorita) e sua adoção cresceu exponencialmente, sobretudo por projetos que adotaram TDD (como a maioria dos projetos XP).

Idéia

A idéia do framework é facilitar a criação, agrupamento e execução de testes unitários, permitindo sua automatização e a aplicação de seus conceitos. Como seu uso, podemos executar, de uma só vez, os seguintes testes:
  • Teste unitário: verifica se cada pequena parte do programa funciona corretamente.
  • Teste de integração: verifica se as partes do programa funcionam corretamente quando utilizadas conjuntamente.
  • Teste de regressão: verifica se uma alteração numa parte do programa afeta o funcionamento de outras partes, inclusive as não relacionadas diretamente.
Ao permitir executar estes testes de forma simples e rápida, sua adoção como parte do processo de construção do programa se torna indolor. Por exemplo, a execução dos testes pode ser feita após cada linkagem do programa, de forma automática, bastando configurar a ferramenta que gerencia o processo de compilação e linkagem para executar os testes ao final.

Com isso refatoração de partes do código se torna muito mais segura e quaisquer falhas no código são detectadas imediatamente, dando um feedback rápido para o desenvolvedor.

Funcionamento Básico

Um projeto de testes baseado na xUnit, poderá exibir sua saída em modo gráfico ou em modo texto, dependo de sua implementação padrão. A maioria gera saída em modo texto, mas em quase todos existem implementações de saídas em modo gráfico que podem ser adquiridas gratuitamente pela Internet.

O framework funciona a partir da criação de um grupo de testes e da execução dos testes nele contidos. Dentro deste grupo de testes, é permitido adicionar, além dos testes, outros grupos. Ao disparar o método que executa os testes contidos em um grupo, ele executará seus testes e solicitará aos seus subgrupos que também executem seus próprios testes. Assim, todos os testes da hierarquia serão executados. No ponto de entrada do programa, como uma função main, geralmente haverá um grupo de testes principal que iniciará todo o processo.

Como funciona um teste

Cada teste criado deve fazer uma verificação sobre o estado ou comportamento de um determinado código. Nas xUnits, essa verificação é feita utilizando métodos ou macros do tipo assert, que, em sua maioria, recebem uma variável booleana como parâmetro e lançam uma exceção caso o valor da variável seja falso. A exceção dá informações adicionais, como o número e o conteúdo da linha de código onde estava sendo chamado o assert. Por exemplo:

assert( 1 > 2 );

Como o resultado da condição é false, uma exceção é lançada informando algo como "Exceção lançada na linha 1. Condição não cumprida: 1 > 2".

Cada método deverá verificar uma e só uma funcionalidade, que poderá compreender em um ou mais asserts. Por exemplo:

void CalculaMaiorDeDoisCorretamente()
{
assert( 2 == MaiorDeDois( 1, 2 ) );
assert( 0 == MaiorDeDois( 0, 0 ) );
assert( -1 == MaiorDeDois( -1, -2 ) );
}
O teste verifica se a função MaiorDeDois funciona com esperado. Caso algum assert falhe, é exibida a linha de código com a expressão que falhou, como descrito no exemplo anterior.

Originalmente, em praticamente todas as linguagens, um assert interrompe a execução do programa gerando um erro, que decreve a condição não cumprida. Para que o assert não funcione desta maneira, os frameworks geralmente criam um novo assert, como sendo um método da classe de teste a qual herdamos. Esta lança uma exceção ao invés de interromper a execução do programa.

Onde são colocados os testes

Cada teste deve ser colocado em um grupo de testes, que é chamado formalmente de Suíte de Testes (Test Suit). Geralmente nos frameworks xUnit, existe uma classe chamada TestSuit que será como uma lista de (ponteiros para) métodos de teste que você escreverá.
Você deverá adicionar (o endereço de) cada método de teste que você escrever à um objeto de TestSuit. Posteriormente, ela poderá executar todos os métodos que você adicionou - um após o outro. Havendo algum assert dentro deles que não teve sua condição satisfeita, o programa termina com uma exceção (conforme descrito acima).

Na verdade, os métodos que você criou não poderão ser adicionados diretamente no TestSuite. Ao invés disso, você deve criar um objeto de uma classe chamada TestCaller (a "chamadora do teste") que terá o nome do teste e um ponteiro para (o endereço d)ele. Algo como:

TestCaller< MinhaClasseTeste > *chamadaTeste =
new TestCaller< MinhaClasseTeste >(
"NomeDoSeuMetodoDeTeste",
&NomeDoSeuMetodoDeTeste
);

Para simplificar este código, é preferível declarar um tipo que mapeie o TestCaller ao tipo de nossa classe:

typedef TestCaller< MinhaClasseTeste > Chamada;

Agora, substituindo no código anterior, fica:

Chamada *chamadaTeste = new Chamada(
"NomeDoSeuMetodoDeTeste", &NomeDoSeuMetodoDeTeste
);

Agora, para criarmos nossa Suíte de Testes, faremos:

Test* MinhaClasseTeste::suite()
{
Test *suite = new TestSuite( "MinhaClasseTeste" );

Chamada *chamadaTeste = new Chamada(
"NomeDoSeuMetodoDeTeste", &NomeDoSeuMetodoDeTeste
);
suite->addTest( chamadaTeste );

// Ou adiciona diretamente, como neste outro metodo
suite->addTest( new Chamada(
"MeuOutroMetodoDeTeste", &MeuOutroMetodoDeTeste
) );

return ( suite );
}

Existe uma classe chamada TestCase, que agrupa logicamente testes que verificam o comportamento de uma determinada funcionalidade. Ela possui um construtor que recebe uma string que representa o nome do caso de teste, para facilitar sua identificação, os métodos setUp e tearDown para implementação de fixtures, além de um método estático (para quem não conhece métodos estáticos, pense somente como se fosse um método que você pode acessar sem precisar instanciar um objeto da classe) chamado suite, que retorna um (objeto de) TestSuite - o objeto que conterá os nossos testes.
Quando precisarmos criar nossos testes, podemos criar uma classe filha de TestCase. Ela conterá todo o comportamento necessário para a execução de testes e nossos métodos de teste, com seus asserts.

Exemplo prático

Assim, para criar um novo caso de teste chamado TesteContaBancaria, faremos:
  1. A declaração da classe como filha de TestCase;
  2. A declaração nossos testes, preferencialmente na parte privada da classe, pois os mesmos não serão acessados por outras classes;
  3. A implementação dos testes;
  4. A implementação de um método estático suite que criará um TestSuite e adicionará à ele (o endereço de) todos os nossos testes;
Os métodos setUp e tearDown não precisam ser implementados, a menos que você precise fazer um fixture (que veremos mais tarde).

class TesteContaBancaria : public TestCase
{
public:
TesteContaBancaria(std::string nome);

// Suite de testes
static Test* suite();

private:
// Metodo de teste
void ContaTransfereCorretamente();
};


TesteContaBancaria::TesteContaBancaria(
std::string nome
) : TestCase( nome ) // Chama o construtor de TestCase
{
}


Test* TesteContaBancaria::suite()
{
// Cria a suite de testes
Test *suite = new TestSuite( "TesteContaBancaria" );

typedef TestCaller< TesteContaBancaria > Chamada;

// Adiciona o metodo ContaTransfereCorretamente para
// ser chamado pela suite
suite->Add( new Chamada(
&ContaTransfereCorretamente,
"ContaTransfereCorretamente"
));

return ( suite );
}


void TesteContaBancaria::ContaTransfereCorretamente()
{
ContaBancaria minhaConta, suaConta;

minhaConta.DefinirSaldo( 1000.00 );
suaConta.DefinirSaldo( 0.00 );
minhaConta.Transferir( suaConta, 400.00 );

CHECK_EQUALS( 400.00, suaConta.Saldo() );
CHECK_EQUALS( 600.00, minhaConta.Saldo() );
}


Obs: A macro CHECK_EQUALS aqui foi criada sobre assert, e recebe o valor esperado e o valor atual. Se os valores diferirem, uma exceção será lançada. Existem diversas outras macros disponíveis para auxiliar na verificação de estados, dependendo da linguagem utilizada.

Repare que o método de teste ContaTransfereCorretamente é privado. Isto se deve ao fato que ele nunca será acessado diretamente fora da classe e também não se espera que a classe de teste tenha filhas, que irão chamar o método diretamente. Assim, costuma ser um padrão de TDD colocar os métodos de teste como sendo privados.

Este é o funcionamento básico de um framework xUnit. Na parte 2 irei expor o uso de fixtures e mostrarei outros métodos/macros úteis para as verificações de estado. Até lá.