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...) .