quarta-feira, 3 de agosto de 2011

Acertando com assert!

(Esse trocadilho foi horrível)


Como vocês já devem conhecer, o assert é um comando bastante útil para ajudar a encontrar problemas no código durante a depuração. Basicamente, sua função é fazer o programa travar em debug caso seja passado false como parâmetro. Em release (ou melhor, se a macro NDEBUG estiver definida), o comando simplesmente é ignorado.

Um exemplo simples de uso seria se você fosse fazer uma aplicação para ler a velocidade de um carro popular. Supondo que o seu carro consiga atingir no máximo uma velocidade de 150km/h, é possível afirmar que qualquer leitura acima deste valor indica um erro (seja do sensor ou do código em si). Você poderia usar o assert nesta situação:



#include <stdio.h>
#include <assert.h>

unsigned char getVelocidade()
{
...
}

int main()
{
    unsigned char velocidade = 0U;

    velocidade = getVelocidade();

    assert( velocidade < 160U );    //damos uma margem de erro

    printf("A velocidade lida foi %d", velocidade );

    return 0;
}



Alá! Se você retornar alguma coisa maior ou igual a 160 na função getVelocidade() (lembrando que ela é um unsigned char e só pode retornar até 255!), o código irá travar no assert em caso de debug. Em release, a linha do assert será ignorada.

Esta função é muito útil para a qualidade de software, porém foi desenvolvida subestimando a capacidade do usuário de fazer besteira.

Suponha que o brilhante programador resolveu simplificar (note que este é um exemplo real):



 int main()
{
    unsigned char velocidade = 0U;

          //Eu sou muito bom 
    //e economizei 1 linha
    //de codigo
    assert( ( velocidade = getVelocidade() ) < 160U ); 
    printf("A velocidade lida foi %d", velocidade );

    return 0;
}


Você debuga por horas e horas e tudo está funcionando perfeitamente. Então compila a versão release, acelera o carro a 50 por hora e o seu programa mostra:

A velocidade lida foi 0.

"Mas não é possível! Eu testei até agora e estava funcionando!"

O que aconteceu?

Bom, para entender, vamos dar uma olhada na definição do "assert" no Visual Studio:


#ifdef  NDEBUG
    #define assert(_Expression)     ((void)0)
#else
    #define assert(_Expression) (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) )

#endif


Note que, se a macro NDEBUG estiver definida, seu comando é substituído por um (void)0, ou seja, nada.
Do contrário, ele vai executar a operação || com a sua expressão à esquerda e o comando _wassert à direita. Como esta operação é realizada da esqueda para a direita, caso a sua expressão (_Expression) seja verdadeira, a da direita será ignorada. Se a sua expressão for falsa, _wassert será executada, e é ela a responsável por fazer o seu código travar. (Se você estiver se perguntando o porquê do "!!" antes de _Expression, eu explico do final).


"Microsoft é coisa de n00b, eu uso Linux!"


Ok, ok. Segue a definição do assert no GCC:



#ifdef NDEBUG           /* required by ANSI standard */
    #define assert(p)      ((void)0)
#else
    #define assert(e)       ((e) ? (void)0 : __assert(__FILE__, __LINE__, #e))
#endif



Quase a mesma coisa, não é? Se NDEBUG estiver definido, seu código é simplesmente ignorado.


"Legal. Gostaria de começar a usar isso na minha empresa, mas o pessoal lá é meio burro e vai acabar usando errado. Além disso, em vez de travar eu gostaria de chamar uma função minha. E agora, quem poderá me ajudar??"


Está bem, vamos desenvolver nosso próprio ASSERT!

Desenvolvendo meu próprio assert:


Vamos começar com o cabeçalho da função que iremos definir em nosso código. Você pode passar os parâmetros que quiser para dentro dela. Por conveniência, iremos passar a expressão que deu problema, o nome do arquivo e o número da linha:




void ChamadaSeAssertFalhar( const char* expressao, const char* arquivo, const int linha )
{
    printf("Deu pau na expressao %s, que esta na linha %d do arquivo %s", expressao, linha, arquivo );
    //execute o que mais você quiser aqui dentro.
}

 

Agora só precisamos definir uma macro que, se receber uma expressão válida, simplesmente executa aquela expressão. Do contrário, chama a nossa função. Vamos lá:



#define MEU_ASSERT( e ) if ( !e ) ChamadaSeAssertFalhar( #e, __FILE__, __LINE__ )
 

Primeira coisa: o que são as macros __FILE__ e __LINE__?
Estas são macros pré-definidas. Quando o compilador encontra estas macros no código, ele as substitui, respectivamente, pelo nome do arquivo e pela linha do código. Se quiser testar, faça um programa qualquer e coloque um printf("%s %d", __FILE__, __LINE__ ); no meio do código.

Segunda: Por que usar um #define, e não uma função?
Se usássemos uma função, as macros __FILE__ e __LINE__ iriam possuir os valores referentes a esta função, e não à posição do código onde ela foi chamada.

Beleza. Agora temos o nosso aquivo "MeuAssert.h" da seguinte forma:



#if !defined(_MEU_ASSERT_H_)
#define _MEU_ASSERT_H_

#include <stdio.h>

static void ChamadaSeAssertFalhar( const char* expressao, const char* arquivo, const int linha )
{
    printf("Deu pau na expressao %s, que esta na linha %d do arquivo %s", expressao, linha, arquivo );
    //execute o que mais você quiser aqui dentro.
}

#define MEU_ASSERT( e ) if ( !e ) ChamadaSeAssertFalhar( #e, __FILE__, __LINE__ )

#endif



Perfeito. Vamos aos testes:


MEU_ASSERT( true );    //Beleza! Não chamou a função.
MEU_ASSERT( false );   //Maravilha! Chamou! Está funcionando!



Aí você resolve fazer um último teste:

MEU_ASSERT( true || true );    //Uh! Chamou a função!



Bom, o que aconteceu foi simples. Ele substituiu a sua macro por:



if ( !true || true ) ChamadaSeAssertFalhar( "true || true", __FILE__, __LINE__ );
 

E (!true || true ) é uma condição válida para entrar no "if"

"É fácil arrumar. Só colocar parentesis".
Vamos lá. Mudamos a macro para:


#define MEU_ASSERT( e ) if ( !(e) ) ChamadaSeAssertFalhar( #e, __FILE__, __LINE__ )


Isso resolve o problema do "true || true", mas você ainda tem mais coisa pela frente.

Suponha que você faça algo do tipo:


if ( x > 0 )
    MEU_ASSERT( z );
else

    ++x;
 

Seu código será substituído por:


if ( x > 0 )
    if ( !(z) )
        ChamadaSeAssertFalhar( "z", __FILE__, __LINE__ );
    else 

        ++x;


O que não é de fato o que você quer.

O ideal aqui é usar uma lógica semelhante à utilizada pelo Visual Studio ou pelo GCC. Podemos fazer algo como:


#define MEU_ASSERT( e ) ( e ) ? (void)0 : ChamadaSeAssertFalhar( #e, __FILE__, __LINE__ )


Se você quiser desabilitar o MEU_ASSERT em modo release, pode fazer algo como:


#if defined ( NDEBUG )
    #define MEU_ASSERT( e ) (void)(e)
#else
    #define MEU_ASSERT( e ) ( e ) ? (void)0 : ChamadaSeAssertFalhar( #e, __FILE__, __LINE__ )
#endif



Para ficar mais interessante, o ideal é que você não implemente a função "ChamadaSeAssertFalhar" aqui, mas sim que deixe ela como "extern". Assim, você poderá reutilizar o código onde quiser e definir cada programa com sua própria função.
Opcionalmente pode criar outra macro para desabilitar o MEU_ASSERT:


//MeuAssert.h

#if !defined(_MEU_ASSERT_H_)
#define _MEU_ASSERT_H_

extern void ChamadaSeAssertFalhar( const char* expressao, const char* arquivo, const int linha );

#if defined ( NDEBUG ) || defined ( DESABILITA_ASSERT )
    #define MEU_ASSERT( e ) (void)(e)
#else
    #define MEU_ASSERT( e ) ( e ) ? (void)0 : ChamadaSeAssertFalhar( #e, __FILE__, __LINE__ )
#endif

#endif



E é isso pessoal! Até a próxima!




P.s.: Se você leu até aqui para entender o porquê dos !! na definição da Microsoft:


#define assert(_Expression) (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) )
 

É difícil dizer o que o cara tinha na cabeça quando implementou, mas vou dar um motivo em que isso pode ser útil:

Suponha que você tem uma classe que sobrescreve o operador !:


class X
{
public:
    const bool operator!() const
    {
        return true;
    }
};
 

Se você chamar:


X x;
if ( x ) //vai dar erro de compilação, porque não foi definida 

         //a conversão para o operador bool.
{
}
 

Se chamar


if (!!x)  //Ok, vai chamar a sobrecarga do operador "!" e 
          //depois negar o valor retornado.
{
}

Nenhum comentário:

Postar um comentário