quarta-feira, 10 de agosto de 2011

x = x++


Neste tópico vamos abordar um dos assuntos que assusta muita gente em C e C++, principalmente quem veio de outras linguagens. É o famoso undefined behavior!

O que é um undefined behavior?

Imagine que você está jogando um jogo de tabuleiro qualquer com seu filho (ou seu pai, sua namorada, sei lá) e o dado cai no chão. O que você faz? Usa o valor que caiu ou pega o dado e joga de novo? Como a regra do jogo não diz nada, você vai acabar querendo fazer o que mais valer a pena (se deu um valor bom, usa ele. Se não, joga de novo). Provavelmente o adversário vai reclamar, mas como você é o dono do jogo você faz o que você quiser.
O famoso undefined behavior é mais ou menos isso aí. Você escreve um código que cai em alguma regra não prevista pelo padrão C ou C++ e o compilador faz o que ele quiser (sim, ele é o dono do jogo).

Então C++ é uma bosta, que não prevê estas situações?”

Não. Isso é uma característica da linguagem. Tanto C quando C++ são conhecidos por sua flexibilidade e seu ótimo desempenho, e isto se deve à sua simplicidade. Algumas situações não são previstas por suas regras e outras são explicitamente definidas como undefined behavior.

Por exemplo:

x = 0;
x = x++;

O resultado disso pode ser x == 0, x == 1, x == 50 ou o seu HD sendo formatado.
Não se espera que nenhum programador em sã consciência faça esse tipo de coisa, mas existem situações em que você pode cair em um undefined behavior sem saber.

Vamos a mais exemplos?
(para os exemplos, vamos assumir que os tipos de dados tem os seguintes tamanhos:
char – 1 byte
short – 2 bytes
int – 4 bytes
Note que, dependendo do compilador e da plataforma utilizada, eles podem ter tamanhos diferentes. Vou falar mais sobre isso em um próximo post)

Soma do Capeta

Imagine o seguinte código:

unsigned char a = 250U;
unsigned char b = 253U;
unsigned short x = a + b;

Qual será o valor de “x”?
Parece simples. Sabemos que um “unsigned char” pode contar de ZERO a 255. Já um “unsigned short” pode contar de zero a 65535. Ora, como o resultado será 503, este valor cabe em “x” e não há nada de errado com a soma, correto?
Errado! Quem define o tipo do retorno e a base em que a operação será realizada são os operandos, e não o resultado. Portanto, não estranhe se compilar o código acima e o resultado de x for 247 (também não estranhe se retornar o valor correto, já que este é um comportamento indefinido).

E também não adianta fazer:

x = (unsigned short)( a + b );
 ou 
 x = static_cast< unsigned short >( a + b );

O jeito correto de realizar esta operação e colocar pelo menos um dos operandos na base 16. Ou seja:

x = (unsigned short)a + b;
Ou, em c++:

x = static_cast< unsigned short >( a ) + b;

Integer Overflow

Já viu algum código assim?

if ( x + 1 > x ) …?

Pode parecer absurdo à primeira vista, mas este código é bastante usado para detectar se ocorreu overflow na contagem do número ou não.

Sabemos que um signed char conta desde -128 até +127.

Então qual será o valor de “x” no final da seguinte operação?

signed char x = 127;
++x;

Você testa no Visual Studio e o resultado é -128. Testa com o gcc no Linux e o resultado é -128. Testa no Mac e o resultado é -128.
Bom, isso acontece porque a maioria dos compiladores simplesmente converte o incremento em código assembly sem verificar se houve overflow ou não, e a maioria dos processadores se comporta desta forma. Mas o overflow de inteiros é um undefined behavior e deve ser evitado. Uma nova versão do gcc pode optar por manter o resultado antigo ou causar um assert quando ocorrer esta situação.

Quer saber se o número chegou ao seu valor máximo? Não faça if (x + 1 > x ). Em vez disso, utilize a biblioteca limits.h e use suas macros:
CHAR_MAX, CHAR_MIN, INT_MAX, INT_MIN, LONG_MAX, LONG_MIN, etc.

Argumentos das Funções

int FuncA()
{
    printf(“Hello “);
    return 0;
}

int FuncB()
{
    printf(“World”);
    return 0;
}

void FuncC( int a, int b )
{
    printf(“!!!!”);
}

int main()
{
    FuncC( FuncA(), FuncB() );
    return 0;
}

Este código pode tando imprimir “Hello World!!!!” quanto “WorldHello !!!!”, porque a norma não diz nada quanto a ordem em que os argumentos de uma função são executados.


Alterando uma variável const

Recentemente alguém postou o seguinte código em um grupo do LinkedIn:



int main()
{
    const int num = 5;
    int &ref = (int &)num;
    ref = ref + 2;
    printf("num addr:%x ref addr:%x,\n",&num,&ref);
    printf("num value=%x ref value=%x\n",num,ref);
}

O resultado “surpreendente” foi:

num addr:bfb403b4 ref addr:bfb403b4,
num value=5 ref value=7

Mesmo endereço mas valores diferentes!! Uau!!

É o que dá tentar alterar o valor de uma variável const. O fato é que o compilador é livre para substituir no código os locais que utilizam uma variável const em tempo de compilação (igual como faz com os defines). Por isso, se uma variável foi declarada como const, não tente modificá-la.


Desalocando um ponteiro NULL com free()

Ao contrário do que muitos pensam, tentar desalocar um ponteiro que aponta para NULL com free() é, sim, um undefined behavior. O mesmo não acontece com a função delete.

Portanto:

delete NULL; //ok, nada vai acontecer.
free( NULL ); //Undefined behavior.

Dividir um número por ZERO

int x = 3/0;

Não, o resultado não será nenhum número tendendo a infinito ou coisa parecida. O resultado disso poderá ser qualquer coisa.
Portanto, para evitar problemas, o ideal é sempre conferir o valor do divisor antes de realizar uma divisão.

Retornar ponteiro ou referência para um objeto que não existe mais

char* Funcao()
{
    char dados[20];
    strcpy( dados, “ola mundo!” );
    return dados;
}


int main()
{
    char* olaMundo = Funcao();
    printf( olaMundo );
    return 0;
}

No exemplo acima, “Funcao()” cria um array temporário chamado dados e retorna uma referência para ele.
Se este array tivesse sido criado dinamicamente, isso não seria problema (apesar que quem chama a função precisaria lembrar de deletá-lo), mas no exemplo mostrado, a memória ocupada por dados é liberada assim que “Funcao” é finalizada. Por isso, olaMundo está apontando para uma área de memória que pode conter qualquer coisa.

Bom pessoal, é isso. Listei aqui alguns dos erros mais comuns que já vi o pessoal cometendo.
Se tiverem dúvidas ou tiverem interesse em mais exemplos de comportamento indefinido, só pedir por comentários ou por e-mail.

Até a próxima!

Nenhum comentário:

Postar um comentário