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;
}
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!
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