domingo, 6 de novembro de 2011

Ordem de inicialização dos atributos de um objeto

Diz a lenda que o seguinte código dá um erro em tempo de execução:

#include <cstring>

class Foo
{
public:
  Foo ( int size ) :
    mSize(size),
    mValues( new char[mSize] )
    {
      memset( mValues, 0, mSize );
    }
  ~Foo ()
  {
    delete[] mValues;
  }

private:
  char* mValues;
  const int mSize;
};

int main()
{
  Foo deu(10);
  return 0;
}


Você sabe dizer qual é?

O problema aqui está na alocação de memória para mValues. Observe o construtor:

Foo ( int size ) :
  mSize(size),
  mValues( new char[mSize] )
  {
    memset( mValues, 0, mSize );
  }

Aparentemente está ok, não é? No exemplo acima, o objeto é criado passando 10 como parâmetro. Sendo assim, o valor 10 é atribuído para mSize, e depois mValues é alocado com o tamanho de mSize, e então é feito um memset para zerar todos os valores de mValues.
Muito bonito, mas o problema está na ordem de inicialização das variáveis. Quem define esta ordem é a ordem em que as variáveis foram declaradas dentro da classe.

Note:

private:
  char* mValues;
  const int mSize;


Não importa se você vai chamar

Foo ( int size ) :
  mSize(size),
  mValues( new char[mSize] )
{
  ...
}

ou

Foo ( int size ) :
  mValues( new char[mSize] ),
  mSize(size)
{
  ...
}
que mValues sempre vai ser inicializado antes de mSize.

Para resolver o problema?
Inverta a ordem de declaração das variáveis.

O seguinte código:

#include <cstring>

class Foo
{
public:
  Foo ( int size ) :
    mSize(size),
    mValues( new char[mSize] )
  {
    memset( mValues, 0, mSize );
  }
  ~Foo ()
  {
    delete[] mValues;
  }

private:
  const int mSize;
  char* mValues;
};

int main()
{
  Foo deu(10);
  return 0;
}

Não apresenta problema algum.

Dizem que é uma boa prática inicializar as variáveis no construtor na mesma ordem que elas foram declaradas na classe para evitar este tipo de dúvida. Se você tiver paciência para fazê-lo, mande bala! :)

sábado, 22 de outubro de 2011

Hello World alternativo

#include <cstdio>

class HelloWorld
{
public:
    HelloWorld()
    {
        printf("Hello World!");
    }
    ~HelloWorld(){}
};

HelloWorld obj;

int main()
{
    return 0;
}

Signed/Unsigned Mismatch

Dando manutenção em códigos antigos ao longo da vida, percebi que os warnings do tipo “signed/unsigned mismatch” são extremamente comuns. Acredito que isso se deva ao fato de que situações de comparação entre variáveis signed e unsigned acontecem com frequência, e como estes warnings parecem inofensivos, ninguém lhes dá atenção.

Mas não é bem assim. Todo warning deve ser levado a sério.

Observe os seguintes exemplos:

Ex. 1:


int x = -1;
unsigned int y = 10U;

if ( x > y )
{
   printf("X eh maior");
}
else
{
   printf("Y eh maior");
}

Ex. 2:

signed char x = -1;
unsigned char y = 10U;

if ( x > y )
{
   printf("X eh maior");
}
else
{
   printf("Y eh maior");
}


Os dois exemplos são praticamente iguais. A única diferença é que, no primeiro, foram utilizadas variáveis do tipo int. No segundo, foram utilizadas variáveis do tipo char.

Rodando os exemplos em um PC, obtemos os seguintes resultados:

Ex. 1:
X eh maior

Ex. 2:
Y eh maior

Mas como???

Bom, vamos lá.
Quando você faz uma operação qualquer (soma, subtração, comparação...), a regra diz que a operação será feita em uma base com pelo menos o tamanho do maior dos operandos. Em uma mesma base, o tipo unsigned é considerado maior que o tipo signed.
Observe a tabela abaixo:

Operação
Base em que o cálculo será realizado
int32 + int16
int32 ou maior.
int8 + uint8
uint8 ou maior.
(int16)int8 == uint8
int16 ou maior.
int16 - int16
int16 ou maior.

Agora voltemos aos exemplos acima.
No segundo caso, foi feita uma comparação entre um signed char e um unsigned char. Estes tipos tem 8 bits em um PC normal (pode variar em alguns raros sistemas embarcados), portanto o compilador é livre para resolver a operação na base 8 bits ou em uma maior.

Por questões de desempenho, o compilador optou por realizar esta comparação na base int. Isto porque um processador de 32 bits realiza uma operação na base 32 mais rápido do que na base 8 (acredito que um processador 64 bits teria realizado a operação na base 64, mas não cheguei a testar isso).

Como (int)-1 é menor que (int)10, o resultado da operação foi que Y é maior.

Agora, no primeiro exemplo, foi feita a comparação entre um int e um unsigned int. Como o unsigned tem preferência sobre o tipo signed, o compilador precisava realizar a operação em uma base de tamanho maior ou igual a unsigned int. Como não haveria nenhum ganho de desempenho se a operação fosse realizada em uma base maior que esta, a operação foi realizada em unsigned int.

Assim, o valor (int)-1 foi convertido para (unsigned int)-1, que vale 0xFFFFFFFF (considerando que o int tenha tamanho 32) ou 0xFFFFFFFFFFFFFFFF (em plataforma 64 bits).

Em ambos os casos, o valor de X será maior que o de Y, causando este comportamento estranho.


Bom, muito interessante. Se você não está convencido ainda, vou mostrar um exemplo prático de um problema que eu tive uma vez por causa destas comparações incorretas.

Em uma empresa X que eu trabalhei há muito tempo atrás, havia um código em produção que, de vez em quando, imprimia um monte de lixo na tela e travava. Este problema acontecia muito raramente, era difícil de se reproduzir e nunca foi encontrado nada de errado no código que pudesse causar este tipo erro. Para ajudar, alguns meses antes havia sido feito um trabalho bem-sucedido para remover todos os warnings do fonte. Desta forma, o código compilava bonito, com zero erros e zero warnings. Concluiu-se, então, que o problema devia ser de hardware.

Até que, um dia, mexendo em alguma coisa nada a ver, observei a seguinte linha de código:

for (unsigned int x = 0U; x < (unsigned int)str.GetLen(); ++x )

Aparentemente ok, mas aquele cast para unsigned int me deixou com uma pulga atrás da orelha. Olhei o histórico deste código no SVN e vi que, antes da Operação Zero Warnings, o código era da seguinte forma:

for (unsigned int x = 0U; x < str.GetLen(); ++x )

Este código faz exatamente o que o novo faz, porém, ao ser compilado, dá um alerta dizendo "signed/unsigned mismatch on line XXX" que, em outras palavras, é o compilador dizendo "Cuidado, você pode estar fazendo cagada".

Ao colocar aquele cast, o que o desenvolvedor fez foi dizer ao compilador "Tá bom, eu sei que eu tô fazendo cagada, não me encha mais o saco". Se este desenvolvedor tivesse prestado atenção no warning, em vez de simplesmente tentar removê-lo de qualquer forma, teria resolvido o bug milenar.


O problema é que a variável str era do tipo MyString, uma classe que era definida da seguinte forma:

class MyString
{
public:
  int GetLen()
  {
    int retorno = -1;
    if ( mpDados != NULL )
    {
      retorno = (int)strlen( mpDados );
    }
    return retorno;
  }
...
private:
  char* mpDados;
...
};

Olha só! A GetLen() retorna -1 quando mpDados aponta para NULL!
Isso fazia com que o loop fosse executado 4294967295 vezes quando GetLen() retornasse -1, fazendo com que um monte de lixo fosse impresso na tela, até o código travar de vez.

O correto, nesta situação, seria mudar o tipo de "x" para int, em vez de converter o retorno de GetLen() para unsigned.

Por isso, da próxima vez, olhem seus warnings com mais atenção :)

(P.s.: Sim, o cast de strlen para int também não é muito bacana, mas isso já é outra história...) .

terça-feira, 27 de setembro de 2011

Duff's Device

Estou postando este artigo a título de curiosidade. Embora você ainda possa encontrar uma situação prática hoje em dia em que o Duff's Device possa ser útil, em 99,9% das situações seu benefício é desprezível.

O Duff's Device é uma técnica criada por Tom Duff em 1983 para fazer uma cópia serial de dados (a mesma coisa que o memcpy faz) com um desempenho maior. Foi amplamente utilizada por progamadores de Assembly e programadores de C no começo dos tempos.

Basicamente, a maneira mais rápida de se copiar dados de uma região de memória para outra, em C, é:

void meu_memcpy( unsigned char* destino, unsigned char* origem, int quantidade )
{
     while( quantidade-- > 0 )
     {
         *destino++ = *origem++;
     }
}

O problema com esta solução é que temos que verificar se quantidade é maior que zero em cada iteração. Se estivermos copiando mil bytes, haverá mil comparações.
A solução encontrada por Duff foi diminuir este número de comparações, copiando os elementos de 8 em 8. Depois de copiar 8 elementos, ele fazia as devidas verificações e partia para o próximo bloco de cópia.
O algoritmo original do Duff era:

void meu_memcpy( unsigned char* destino, unsigned char* origem, int quantidade )
{
     register int n = (quantidade + 7) / 8;
     switch(quantidade % 8)
     {
         case 0: do { *destino++ = *origem++;
         case 7: *destino++ = *origem++;
         case 6: *destino++ = *origem++;
         case 5: *destino++ = *origem++;
         case 4: *destino++ = *origem++;
         case 3: *destino++ = *origem++;
         case 2: *destino++ = *origem++;
         case 1: *destino++ = *origem++;
             } while(--n > 0);
     }
}

Basicamente n é a quantidade de oitos que cabem em quantidade. Se der número quebrado, arredonda para cima. Ou seja, se quantidade vale 8, n vale 1. Se quantidade vale 10, n vale 2.
O switch pega o resto da divisão de quantidade por 8 para que esta quantidade seja copiada na primeira iteração. Nas demais, serão copiados 8 bytes.

Você deve ter estranhado o formato do do...while no meio dos cases, mas este código é perfeitamente válido em C.
Talvez você esteja se perguntando também porque foi usado o valor 8, e não outro qualquer.
A explicação disso é que os processadores calculam mais rapidamente qualquer divisão por valores 2^n.
Por exemplo, se quiser dividir um número por 2, basta rotacioná-lo para a direita 1 bit. Para dividir por 4, rotacione 2 bits. Para dividir por 8, 3 bits, e assim por diante.

O Duff ' Device teve sua vantagem no passado, mas hoje em dia são pouquíssimas as situações em que se vale a pena aplicá-lo. Primeiro que o código tem uma complexidade razoável perto do original. Um leigo com certeza terá dificuldades em entendê-lo.
Segundo que não necessariamente este código será mais rápido. Os compiladores atuais executam vários tipos de otimizações. Se o ganho de desempenho for justificável na sua aplicação para que você venha a adotar esta implementação, é interessante que você faça testes com o memcpy original e com a sua implementação do Duff's Device para ver se, na prática, ela realmente é mais rápida.

domingo, 25 de setembro de 2011

Aventuras com Heranças

Embora a herança em si seja um tema básico da orientação a objetos, ela possui uma série de artimanhas e regras (principalmente para quem programa em C++) que devem ser seguidas para não cair em futuras armadilhas.
Basicamente, existem três tipos de heranças em C++: a pública, a protegida e a privada. O primeiro tipo é o mais conhecido. Os outros dois não são muito utilizados e raramente são ensinados na faculdade ou nos cursos de C++. Além disso, a herança pode ser virtual ou não.
Vou explicar cada um destes tipos na sequência:

Herança pública:

De longe a mais conhecida e mais utilizada (embora muitas vezes utilizada de forma incorreta). A regra mais importante para esta herança é: quando você diz que B herda de A, você está dizendo que B é um A. Tenha isso em mente. Sempre. Só herde uma classe de outra se você puder dizer, no bom português, que a classe filha é um tipo da classe mãe. Nunca herde com o objetivo de economizar código ou coisa do tipo.
Por exemplo, você pode herdar Carro de Veiculo, Moto de Veiculo, Ferrari de Carro, Marte de Planeta, Carrefour de Supermercado, mas nunca deve herdar Moto de Carro, por exemplo. Mesmo que no seu código seja tentador fazer isso, pois a maioria das funções de Moto estão implementadas em Carro, não faça tal herança. O ideal é criar uma classe acima de Carro e Moto (como Veiculo) e colocar lá as funções em comum.
Vou citar um exemplo de um código que eu trabalhei há algum tempo que fazia o uso incorreto de herança pública.
Tínhamos uma classe chamada Thread, com um método virtual puro chamado Run, um Inicia e um SetDelay. Basicamente, se você queria ter uma thread no sistema, bastava herdar desta classe, implementar o método Run, e chamar o Inicia para que o conteúdo de Run ficasse sendo executado de acordo com o delay definido.
Como o código implementava uma comunicação por Gprs e esta comunicação deveria ficar sendo executada em uma thread separada no sistema, fizemos Gprs herdar de Thread. O problema é que Gprs não é uma Thread! Gprs pode ser considerado uma Rede, um Canal de Comunicação, mas definitivamente não é uma thread!
Apesar de parecer inofensiva, a decisão de herdar Gprs de Thread começou a causar dores de cabeça. O problema é que no código você acaba tendo algo do tipo:

Gprs* objGprs = new Gprs();
objGprs->SetDelay( 1000 );
objGprs->Inicia();

Agora, para uma pessoa que nunca mexeu no código, o que o Inicia faz? A pessoa pode imaginar que vai configurar IP, zerar as variáveis da classe, ligar o hardware do modem, o que for, mas não vai imaginar que este comando está iniciando a Thread.
Além disso, tínhamos a necessidade de ter uma função que iniciasse o modem e configurasse a rede, mas esta função não poderia se chamar Inicia porque o Inicia já está implementado em Thread.
Assim, o código acabou ficando uma beleza do tipo:

Gprs* objGprs = new Gprs();
objGprs->IniciaGprs( IP, PORTA );
objGprs->SetDelay( 1000 );
objGprs->Inicia();

O que beira a estupidez.

O correto, neste caso, seria ter criado duas classes: uma chamada ThreadGprs que herda de Thread, e outra chamada Gprs que não herda de ninguém (ou herda de CanalComunicacao, ou algo do tipo), e fazer com que ThreadGprs agregasse Gprs. Um código do tipo

ThreadGprs* objThreadGprs = new ThreadGprs();
objThreadGprs->SetDelay( 1000 );
objThreadGprs->Inicia();

Faz sentido pra qualquer novato que bata o olho nele.

Bom, dito isso, vamos ao que interessa: o que é uma herança pública?

O funcionamento é simples: a classe filha vai conseguir acessar os atributos e métodos públicos e protegidos da mãe, e as outras classes vão conseguir acessar os atributos e métodos públicos da filha e da mãe através de um objeto da filha.

Veja o exemplo:

class Base
{
public:
    void FuncaoUm()
    {
        printf( "Funcao numero 1\n" );
    }

    virtual void FuncaoDois()
    {
        printf( "Funcao numero 2 da base\n" );
    }
};

class Derivada: public Base
{
public:
    void FuncaoDois()
    {
        printf( "Funcao numero 2 da derivada!\n" );
    }
};

int main()
{
    Base* obj = new Derivada();

    obj->FuncaoUm();
    obj->FuncaoDois();

    delete obj;
    return 0;
}

O resultado disso vai ser:

Funcao numero 1
Funcao numero 2 da derivada!

(Se você não entende o que a palavra virtual faz, experimente rodar o código novamente sem ela e verá que o comportamento será diferente do esperado. Via de regra, sempre que você pretende reescrever uma função na derivada, declare a função da Base como virtual).

Função virtual pura

Se você quer obrigar a Derivada a reimplementar uma função, você pode declará-la como virtual pura. Esta função pode ou não ser implementada na Base, mas a Derivada deve obrigatoriamente implementá-la.
Uma função virtual pura é declarada da seguinte forma:

virtual void Funcao() = 0;

Veja o exemplo:

class Base
{
public:    
    //Função virtual pura não implementada.
    virtual void FuncaoPuraUm() = 0;

    //Função virtual pura implementada:
    virtual void FuncaoPuraDois() = 0
    {
        printf( "Funcao Pura Dois na base\n" );
    }
};

class Derivada: public Base
{
public:
    //Somos obrigados a implementas as funções
    //virtuais puras da base aqui, senão dá erro de
    //compilação.
    void FuncaoPuraUm()
    {
        printf( "Funcao Pura Um implementada na derivada\n" );
    }

    //Apesar de estar implementada na base, precisamos
    //implementá-la aqui também.
    void FuncaoPuraDois()
    {
        printf( "Funcao Pura Dois implementada na derivada\n" );

        //opcionalmente podemos chamar a implementação
        //da base aqui.
        Base::FuncaoPuraDois();
    }
};

int main()
{
    Base* obj = new Derivada();

    obj->FuncaoPuraUm();
    obj->FuncaoPuraDois();

    delete obj;
    return 0;
}

O resultado vai ser:

Funcao Pura Um implementada na derivada
Funcao Pura Dois implementada na derivada
Funcao Pura Dois na base

Tranquilo, né? Se você seguir corretamente a regra que eu comentei no começo, vai tirar um grande benefício com heranças, e dificilmente vai se meter em problemas.

Vamos para os tipos menos conhecidos:


Herança privada:

Um tipo de herança menos conhecido e que deve ser evitado. Diferente da pública, na herança privada, quando você diz que B herda de A, está dizendo que B tem A. Por exemplo, Carro poderia fazer uma herança privada de Motor.

A principal diferença entre esta herança e a pública, do ponto de vista técnico, é que quem possui um objeto da classe Derivada não irá conseguir chamar, por este objeto, uma função da classe Base.

Voltemos ao primeiro exemplo:

class Base
{
public:
    //Função virtual pura não implementada.
    virtual void FuncaoPuraUm() = 0;

    //Função virtual pura implementada:
    virtual void FuncaoPuraDois() = 0
    {
        printf("Funcao Pura Dois na base\n");
    }
};

class Derivada: private Base
{
public:
    //Somos obrigados a implementas as funções
    //virtuais puras da base aqui, senão dá erro de
    //compilação.
    void FuncaoPuraUm()
    {
        printf( "Funcao Pura Um implementada na derivada\n" );
    }

    //Apesar de estar implementada na base, precisamos
    //implementá-la aqui também.
    void FuncaoPuraDois()
    {
        printf( "Funcao Pura Dois implementada na derivada\n" );
        //opcionalmente podemos chamar a implementação
        //da base aqui.
        Base::FuncaoPuraDois();
    }
};

int main()
{
    //Base* obj = new Derivada();
    Derivada* obj = new Derivada();

    //obj->FuncaoPuraUm();
    obj->FuncaoPuraDois();

    delete obj;
    return 0;
}

Note as diferenças:

  1. A herança agora é private, e não mais public;
  2. Fazer um ponteiro do tipo Base apontar para um objeto alocado como o tipo Derivada já não funciona mais. (É possível utilizar um reinterpret_cast aqui para passar esta segurança, porém você já está hackeando o código e vai acabar com um comportamento inesperado mais cedo ou mais tarde);
  3. Não é mais possível chamar as funções que estão na Base através do objeto da Derivada. Assim, a linha obj->FuncaoPuraUm(); teve que ser comentada.

O mesmo vale caso você herde (independente da forma da herança) uma classe de Derivada (uma classe chamada DerivadaDaDerivada, vamos supor). DerivadaDaDerivada não conseguirá acessar nenhum método ou atributo de Base diretamente, independente se este método ou atributo é público, protegido ou privado.

Mas por que usar herança privada, e não composição ou agregação?

Difícil responder, até porque você deve utilizar composição ou agregação em vez de herança privada.
Existem duas razões para você optar por herança privada:

  1. Caso você precise que a sua classe derivada acesse os métodos e atributos protegidos da base. Isso não seria possível caso fosse utilizado composição ou agregação, a menos que você declarasse a classe agregada como friend da classe agregadora.
  2. Caso você queira declarar na base uma função virtual (tanto faz se é pura ou não) e queira que ela seja reimplementada na derivada.
Sinceramente não consigo pensar em nenhum exemplo prático bom para se utilizar este tipo de herança.
Uma situação, talvez, fosse no exemplo do Gprs que eu comentei acima. Fazer Gprs herdar de forma privada de Thread seria melhor do que fazer a herança pública, porque, desta forma, quem chamasse:

objGprs->Inicia();

Estaria, com certeza, chamando a Inicia do Gprs, e não da Thread. A diferença é que daí a responsabilidade de inicializar a thread, definir timeouts e tudo mais ficaria a cargo da classe Gprs.


Herança Protegida

Dos três tipos de herança, este é o pior. Ninguém sabe ao certo o que significa dizer que B faz uma herança protegida de A, mas está perto de ser um relacionamento do tipo B tem A, como na herança privada.

O funcionamento deste tipo de herança é bem semelhante ao da (herança) privada. Os métodos da Base continuam inacessíveis por quem tenta acessá-los por um objeto da Derivada. A diferença está em se você herdar de Derivada (o exemplo da DerivadaDaDerivada). Neste caso, a DerivadaDaDerivada conseguirá acessar os métodos e atributos públicos e protegidos da Base sem problemas.

Herança Virtual

Os três tipos citados acima podem ser virtuais ou não (embora para a herança privada eu não veja sentido).
As heranças virtuais são úteis para o caso das heranças do tipo diamante (quem programa em Java treme ao ouvir isso).
Uma herança do tipo diamante se forma quando você tem uma Base, duas (ou mais) derivadas e depois uma classe que faz herança múltipla dessas duas (ou mais) derivadas. Pense o seguinte:

class Base
{
protected:
    int x;
};

class DerivadaUm: public Base
{
};

class DerivadaDois: public Base
{
};

class Join: public DerivadaUm, public DerivadaDois
{
public:
    void Funcao()
    {
        x = 0;
    }
};

Isso vai dar um erro de compilação, dizendo que o acesso a “x” é ambíguo. Isso acontece porque Join enxerga dois x: um que veio de Base por meio da DerivadaUm e outro que veio de Base por DerivadaDois.

Para que este código funcione, é necessário que DerivadaUm e DerivadaDois façam herança virtual de Base.
Assim, o código fica da seguinte forma:

class Base
{
protected:
    int x;
};

class DerivadaUm: virtual public Base
{
};

class DerivadaDois: virtual public Base
{
};

class Join: public DerivadaUm, public DerivadaDois
{
public:
    void Funcao()
    {
        x = 0;
    }
};

Isso criará apenas uma instância de x para Join.

Bom, dizem que heranças do tipo virtual devem ser evitadas. O problema não está com as heranças virtuais, mas sim com a herança do tipo diamante. Se você chegar a um modelo que chegue a este tipo de configuração, evite-o. Modele de forma diferente. Mas, inevitavelmente, você vai acabar tendo que dar manutenção em um código que está mal modelado, e talvez esta seja a sua única saída...

sexta-feira, 23 de setembro de 2011

A palavra mágica é: explicit!

Observe a seguinte classe:

class NumeroEstupido
{
public:
    NumeroEstupido( int num ): mNumero( num ){}
    ~NumeroEstupido(){}
    int GetNumero() const { return mNumero; }
private:
    int mNumero;
};

Note algo incrível nela: o construtor recebe apenas um parâmetro!

Nossa, que incrível! ¬¬

Bom, o que eu quero mostrar com isso são as maneiras de iniciar este objeto. A mais tradicional é:

NumeroEstupido obj( 2 );

Mas, como possui apenas 1 parâmetro no construtor, ele também pode ser iniciado assim:


NumeroEstupido obj = 2;

Nossa, não sabia! Isso é d+!

Não, não é. Se você conviveu até hoje sem isso, pode conviver por mais tempo. A menos que você queira muito que a classe tenha este comportamento, você deve desabilitá-lo. E é para isso que serve o comando explicit.

Se você declarar o construtor da NumeroEstupido da seguinte forma:

explicit NumeroEstupido( int num ): mNumero( num ){}

A inicialização NumeroEstupido obj = 2; não funcionará. Via de regra, sempre que você declarar uma classe com um construtor que recebe apenas 1 parâmetro, você deve declará-lo como explicit.

Ora, mas por quê??

Essa inicialização esquisita é a causa de muitos problemas e comportamentos inesperados em códigos. Vou dar um exemplo simples. Imagine que você tem a seguinte função:

NumeroEstupido soma( const NumeroEstupido& num1, const NumeroEstupido& num2 )
{
   return NumeroEstupido( num1.GetNumero() + num2.GetNumero() );
}

E a chama da seguinte forma:

NumeroEstupido numSoma = soma( 200, 250 );

Basicamente, o compilador vai fazer:
num1 = 200
num2 = 250

E retornará a soma dos dois, fazendo com que numSoma valha 450. Até aí tudo bem. Você tem um código com 10 milhões de linhas rodando perfeitamente desta forma.

Até que um dia alguém resolve adicionar uma nova função ao código:

signed char soma( signed char num1, signed char num2 )
{
   return num1 + num2;
}

Nada demais, afinal C++ permite que você tenha funções com o mesmo nome e parâmetros diferentes.
O problema é que você roda o seu código agora, e na sua antiga chamada

NumeroEstupido numSoma = soma( 200, 250 );

numSoma passa a valer -62, e não mais 450! Como isso é possível?

É simples. Agora, quando você chama soma( 200, 250 ), o compilador vai chamar a função que recebe signed chars, e não mais a que recebe NumeroEstupido. Como esta função retorna um signed char, ocorre um overflow e o resultado é um undefined behavior.

Se o construtor de NumeroEstupido fosse explicit, esse problema seria evitado, pois no primeiro momento você seria obrigado a chamar a função soma definindo explicitamente os objetos, ou seja, algo do tipo:

soma( NumeroEstupido( 200 ), NumeroEstupido( 250 ) );

Assim, quando a função com signed char fosse criada, o comportamento da chamada com NumeroEstupido permaneceria o mesmo.

Consegue pensar em mais problemas que poderiam acontecer?

segunda-feira, 12 de setembro de 2011

Vocês praticam inspeção de código?

Se eu tivesse que dar uma palestra sobre qualidade de código e pudesse escolher apenas um tema, escolheria inspeção de código. Esta é uma das melhores técnicas para reduzir a quantidade de bugs no software, mas infelizmente é muito pouco utilizada pelas empresas. Isso porque parar um desenvolvedor por algumas horas para simplesmente “olhar o código de outro” parece uma perda de tempo muito grande para a maioria das empresas.
Se você também pensa assim, espero que tenha paciência de ler este post até o fim. Provavelmente mudará de ideia.
Para começar, vou colocar aqui dados de algumas empresas de porte razoável sobre o assunto:

  • Um defeito é encontrado a cada 4h de teste, contra 4,4 defeitos que são encontrados durante 1h de revisão (Hewlett-Packard);
  • Inspeções são 20x mais rápidas que testes (Russell);
  • 82% dos problemas são resolvidos antes de testar (IBM);
  • 1 defeito é encontrado a cada 3 minutos de inspeção (Shell);
  • Custo de manutenção dos códigos inspecionados é 1/10 do custo dos códigos não-inspecionados (Imperial Chemical).

Interessante, não?
A seguinte tabela foi extraída com base nos livros Software Defect-Removal Efficiency (Jones, 1996) e What we have learned about Fighting Defects (Shull 2002). Ela indica qual é, em média, a porcentagem de defeitos removidos utilizando cada uma das seguintes técnicas comuns de qualidade:

Técnica para encontrar defeitos
Qtde de Defeitos
Inspeções de design informais
35,00%
Inspeções de design formais
55,00%
Inspeções de código informais
25,00%
Inspeções de código formais
60,00%
Prototipação
65,00%
Unit-tests
30,00%
Testes de integração
35,00%
Testes de Regressão
25,00%
Testes do sistema
40,00%
Testes Beta (com menos de 10 usuários)
35,00%
Testes Beta (mais de mil usuários)
75,00%

Note que apenas a prototipação e os testes beta de alto volume encontram mais problemas do que a revisão de código.

Muitas empresas tentam aumentar a qualidade dos produtos aumentando a quantidade de testes do sistema. Isso é a mesma coisa que se pesar com mais frequência para ver se diminui o peso.
Não estou dizendo que os testes devem ser substituídos pelas revisões de código, mas sim que as duas técnicas devem ser utilizadas em conjunto.

Muito bonito na teoria, mas a minha equipe é pequena e não temos condições de parar desenvolvedores para fazer revisão de código!

Será?

Uma pesquisa mostra que a média de linhas de código desenvolvida por cada programador em um projeto de software é de 10 a 50 LOCs por dia. Isso parece absurdo em um primeiro momento, considerando que é possível escrever esta quantidade de linhas em poucos minutos, mas é preciso considerar também o tempo que os desenvolvedores perdem debugando, testando, tentando entender o código e corrigindo defeitos. Quantas vezes você já não passou um dia inteiro e não escreveu uma linha sequer?

O fato de você realizar uma inspeção de código irá diminuir a quantidade de defeitos no produto, aumentando a qualidade e diminuindo o tempo de manutenção. Assim, com esta prática, é possível entregar um produto de maior qualidade em um tempo igual ou menor de desenvolvimento.
Isso se prova com dados de campo: um estudo realizado pelo Laboratório de Engenharia de Software da NASA sobre 50 projetos indicou que, na média, os projetos com menos defeitos por linha de código tiveram um tempo de desenvolvimento menor do que os com maiores índices de defeito, porém com um custo igual (Code Complete, McConnell, 2004).

Outras opiniões?

  • A IBM revelou que cada hora investida em inspeção de código previne cerca de 100 horas de trabalho em testes e correções de defeitos;
  • Raytheon reduziu o custo de correções de defeitos de 40% do custo total do projeto para 20% através de uma iniciativa de inspeções (Haley 1996);
  • a HP reportou que seu programa de inspeção economiza em média 22 milhões de dólares por ano (Grady e Van Slack, 1994);
  • 11 programas foram desenvolvidos pela mesma equipe de desenvolvimento. 5 deles foram desenvolvidos sem utilizar revisão de código e tiveram uma média de 4,5 erros por 100 linhas de código. Os outros 6 foram inspecionados e e tiveram uma média de 0,82 erros por 100 linhas de código (Freedman e Weinberg, 1990).


Além disso, outra vantagem da revisão de código é a troca de conhecimento entre a equipe. Tanto quem desenvolveu quanto quem está revisando o código podem aprender bastante com o processo.

Mas que maravilha! E então, como é o processo de revisão de código?

Várias técnicas de revisão de código podem ser aplicadas. Atualmente, um dos modelos de desenvolvimento ágil mais conhecidos, o eXtreme Programming, utiliza o pair programming, que é um tipo de inspeção de código on-the-fly.

Vou explicar aqui um processo formal de revisão. Você pode adaptá-lo conforme a sua necessidade, porém não recomendaria grandes mudanças se sua empresa ainda não está habituada com a prática.


Processo de Revisão

Antes de explicar o processo, gostaria de ressaltar duas coisas:

  • Para sistemas simples, uma pessoa consegue revisar de 400 a 500 linhas de código por hora. Para sistemas críticos, complexos ou embarcados, esta média cai para cerca de 150 linhas por hora (nestas linhas não consideramos comentários ou linhas em branco);
  • Um revisor aguenta revisar um código por, no máximo, 2h seguidas.

Dito isso, vamos às pessoas envolvidas no processo:

Moderador: É o responsável por fazer o processo andar. Ele deve mover a revisão a uma velocidade que seja rápida o suficiente para ser produtiva, porém não tão rápida a ponto de que os defeitos não sejam encontrados. Também é o responsável por organizar as reuniões e definir quem deve revisar o quê.

Autor: A pessoa que desenvolveu o código. Participa das reuniões e deve ser o responsável por posteriormente aplicar as correções.

Revisor: É a pessoa que irá revisar o código (ou o design, se for o caso). Se for fazer revisão de código, deve ser um programador qualificado, de preferência que esteja envolvido com o processo, mas, obviamente, que NÃO seja o autor!

Escrituário (tá, foi a melhor tradução que eu encontrei para scribe!): pessoa responsável por escrever um relatório com os problemas encontrados durante a reunião. Esta pessoa pode ser o próprio revisor, mas é comum utilizar outra pessoa para liberar o revisor para suas outras atividades. É uma prática comum utilizar estagiários para este fim, com o intuito de que eles, desta forma, adquiram conhecimento e experiência (ok, as empresas utilizam estagiários porque são mais baratos do que os desenvolvedores, mas eu não queria chegar neste ponto...).

Não é comum que gerentes participem das revisões de código, pois isso pode constranger os outros participantes, principalmente se o gerente da equipe for um idiota (se você é gerente, você deve saber se a sua equipe te considera um idiota ou não). Os gerentes podem e devem acompanhar os resultados da revisão, mas não devem acompanhar o processo para que o foco da prática não passe de técnico para político.

As fases da revisão são:

Planejamento: O autor entrega o código (ou seja lá o que for ser revisado) para o moderador. Este decide quem deve realizar a revisão e quando. Dependendo da quantidade de trabalho, é até aconselhável que mais de um revisor seja convocado.
O moderador deve imprimir o código e reunir quaisquer outros documentos que considere importante.

Overview: Quando os revisores não são familiarizados com o projeto, é possível marcar uma reunião com os revisores e o autor, para que este dê uma visão geral do código que eles irão revisar. Esta reunião deve durar no máximo 1h, mas deve ser evitada se possível.

Preparação: Cada revisor trabalha sozinho eu seu código ou modelo que estiver revisano. É interessante que a empresa possua um checklist para facilitar o trabalho, ainda mais se a empresa segue um padrão de desenvolvimento.
Os revisores devem estar cientes das duas coisas que ressaltei no começo do tópico (quantidade de linhas para revisar por hora e tempo máximo que deve durar uma revisão).

Reunião de Inspeção: Nesta reunião, o moderador escolhe alguém (além do autor) para ler o código e explicá-lo. É interessante que o código esteja projetado na parede ou em um quadro, para que todos possam vê-lo. Assim que o código é passado, os erros encontrados são levantados e discutidos com a equipe. O escrituário é responsável por registrá-los. O moderador é responsável por manter a reunião produtiva e evitar que a equipe comece a discutir assuntos irrelevantes à revisão.
Esta reunião não deve durar mais do que duas horas.

Relatório de Inspeção: Um relatório com os problemas encontrados deve ser formalizado e enviado para todos os participantes. O relatório deve conter a lista dos defeitos e a severidade de cada um. Este relatório pode ser um documento de texto enviado para todos por e-mail ou um registro formal em algum bugtracker, não importa.

Retrabalho: O moderador distribui os defeitos encontrados para a equipe. Em geral o autor é responsável por corrigi-los, mas não necessariamente.

Follow-up: O moderador é responsável por acompanhar o andamento da correção dos defeitos encontrados. Dependendo da quantidade e da complexidade dos defeitos encontrados, um novo processo de revisão pode ser necessário na sequência.


É isso pessoal. Uma coisa que se deve ter em mente é que a revisão de código é apenas um processo de qualidade de software, assim como os testes do sistema, e NÃO deve ser utilizado para outros fins além deste. Um erro comum que leva o processo ao fracasso é utilizar o resultado das revisões para julgar ou humilhar o autor. Ou seja, dar um chapéu de burro porque alguém cometeu um erro besta é mais besta ainda.

Espero que tenha convencido vocês da importância deste processo, e boa revisão para todos!