Detalhes importantes sobre aritmética inteira

Em uns artigos eu falei pra vocês sobre algumas armadilhas relativas ao uso de ponto flutuante. A ideia de que você lida com valores no domínio dos números “reais”, por exemplo, deve ser compreendida com muito cuidado… Bem… e não é que existem erros de interpretação por ai a respeito da aritmética inteira também?

Vejamos uma otimização muito comum:

#include <stdio.h>

void main( void )
{
  int a = -1;
  int b = a >> 1;
  int c = a / 2;

  printf("%d, %d\n", b, c );
}

Olhando para o código acima o leitor pode supor que ambos os valores impressos serão iguais. Afinal, parece que estamos apenas dividindo por 2 o valor de a, em ambas as expressões. No entanto, em arquiteturas onde existe a distinção entre shifts lógicos e shifts aritméticos, o resultado em b será -1, enquanto o resultado em c será, necessariamente, zero!

Ambas as expressões continuam usando shifts, por debaixo dos panos. Uma implementação possível, por parte do compilador, poderia ser essa:

main:
  ...
  mov  edx,-1   ; EAX = a = -1

  mov  ecx,edx
  sar  ecx,1    ; ECX = b = (a >> 1)

  mov  eax,edx  ; EDX = c (EAX é um temporário aqui).
  shr  eax,31
  add  edx,eax  ; if (b < 0) c++
  sar  edx,1    ; c >>= 1
  ...

Esse pequeno shift lógico e a adição “condicional” corrigem o problema do shift aritmético com relação ao valor -1. Note que todos os outros valores inteiros negativos continuarão funcionando corretamente.

Isso quer dizer que devemos tomar muito cuidado com deslocamentos para a direita. Afinal, a própria especificação da linguagem C (leia a ISO 9989:1999 em diante) afirma que deslocamentos para a direita de objetos integrais sinalizados é “dependente de implementação”.

Outro problema que é vastamente ignorado é o calculo de valores absolutos de um valor inteiro. A libc implementa a função abs() que deveria ser usada para esse propósito, mas é bem comum ver fragmentos de código como o abaixo:

int _abs( int x )
{
  if ( x < 0 )
    x = -x;

  return x;
}

Isso parece correto, mas considere o seguinte: Se tentarmos inverter o sinal do valor 0 (zero), obteremos o mesmo zero. Isso pode ser interpretado de duas maneiras. Ou o valor zero é “especial” e não tem sinal, ou existem dois zeros diferentes (um +0 e um -0). No caso dos valores inteiros sinalizados o msb define o sinal do valor… Assim, -0 não existe e, de fato, zero é um valor com características especiais. Mas, vamos assumir que ele possa ser interpretado como valor sinalizado, de acordo com a função acima… Existe outro valor com a mesma característica (ou seja, que possa ser negativo e positivo ao mesmo tempo)?

No caso do tipo int, o msb é o bit 31 e, como aprendemos sobre “complemento 2”, para obter o valor representado temos que inverter todos os bits e somar 1… 0xffffffff, por exemplo, representa -1. Mas, em 32 bits, existe um valor diferente de zero especial: 0x80000000 (o bit 31 setado e os demais zerados). Ao invertê-lo e somar um obtemos o mesmo valor, ou seja, existe um -2^{31} e um +2^{31}. Mas, como o msb é quem define o sinal, ao informar o argumento 0x80000000 para x, na chamada da função acima, obteremos o mesmíssimo valor.

A rotina acima, obviamente está errada! Já que o valor absoluto de -2147483648 não pode ser o mesmo valor! Infelizmente isso não tem uma solução fácil porque, com 32 bits, usando um objeto integral sinalizado, não há como obter o valor positivo oposto a -2147483648, dada a assimetria dos valores extremos dessa representação.