Aquele do-while engraçado

Caloni, 2008-05-15 computer ccpp blog

Nesses últimos dias andei conversando com um amigo que está estudando sistemas operacionais na faculdade. Melhor ainda, vendo o código real de um sistema operacional em funcionamento. A conseqüência é que, além de aprender um bocado de como as coisas funcionam de verdade debaixo dos panos, acaba-se aprendendo alguns truquezinhos básicos e tradicionais da linguagem C.

Por exemplo, é um hábito conhecido o uso de construções do-while quando existe a necessidade de definir uma macro que possui mais de um comando em vez de usar a igualmente conhecida { construção de múltiplos comandos entre chaves }.

O que talvez não seja tão conhecido é o porquê das coisas serem assim.

Vamos imaginar uma macro de logue que é habilitada em compilações debug, mas é mantida em silêncio em compilações release:

#ifdef NDEBUG
#define MYTRACE(message) /*nada*/
#else
#define MYTRACE(message)  \
  { \
    char buffer[500]; \
    sprintf(buffer, \
      "DBG: %s(%d) %s\n", \
      __FILE__, \
      __LINE__, \
      message); \
    output(buffer); \
  }
#endif /* NDEBUG */

Nada de mais, e parece até funcionar. Porém, como veremos nas próximas linhas, esse é realmente um exemplo de código "buguento", já que uma chamada dentro de uma construção if-else simplesmente não funciona.

if( exploded() )
  MYTRACE("Oh, my God");
else
  MYTRACE("That's right");

error C2181: illegal else without matching if

Por que isso? Para responder a essa questão nós precisamos olhar um pouco mais de perto no resultado do preprocessador da linguagem, que apenas troca nossa macro pelo pedaço de código que ela representa:

if( exploded() )
{
  char buffer[500];
  sprintf(buffer,
      "DBG: %s(%d) %s\n",
      __FILE__,
      __LINE__,
      "Oh, my God");
  output(buffer);
};
else
{
  char buffer[500];
  sprintf(buffer,
      "DBG: %s(%d) %s\n",
      __FILE__,
      __LINE__,
      "That's right");
  output(buffer);
};

Dessa forma, podemos ver o porquê. Quando chamamos a macro, geralmente usamos a sintaxe de chamada de função, colocando um sinal de ponto-e-vírgula logo após a chamada. Essa é a maneira correta de se chamar uma função, mas no caso de uma macro, dessa macro, é um desastre, porque ela cria dois comandos em vez de um só (um ponto-e-vírgula vazio, apesar de não fazer nada, é um comando válido). Então, isso é o que o compilador faz:

if( instruction )
{
  /* um monte de comandos */

} /* aqui eu esperaria um else ou uma instrução nova */

; /* uma instrução nova! ok, sem else desa vez */

else /* espere ae! o que esse else está fazendo aqui sem um if?!?! */
{
  /* mais comandos */
}

Pense sobre o comando vazio como se ele fosse um comando real, o que é a maneira mais fácil de entender o erro de compilação que recebemos ao compilar o código abaixo:

if( error() )
{
  printf("error");
}
printf("here we go");
else /* llegal else without matching if! */
{
  printf("okay");
}

Por essa razão, a maneira tradicional de escapar desse erro comum é usar uma construção válida que peça de fato um ponto-e-vírgula no final. Felizmente nós, programadores C/C++, temos essa construção, e ela é... muito bem, o do-while!

do
{
  /* múltiplos comandos aqui */
}
while( expression )
  ; /* eu espero um ponto-e-vírgula aqui,
       para finalizar minha
       instrução do-while */

Assim nós podemos reescrever nossa macro de logue da maneira certa (e todas as 549.797 macros já escritas em nossa vida de programador). E, apesar de ser uma construção um tanto bizarra, ela funciona melhor do que nossa tentativa inicial:

#ifdef NDEBUG
#define MYTRACE(message) /*nada*/
#else
#define MYTRACE(message)  \
do  \
{ \
  char buffer[500]; \
  sprintf(buffer, \
    "DBG: %s(%d) %s\n", \
    __FILE__, \
    __LINE__, \
    message); \
  output(buffer); \
} \
while( 0 )
#endif /* NDEBUG */

Ao usar um do-while (com uma expressão que retorna falso dentro do teste, de maneira que o código seja executado apenas uma vez) a construção if-else consegue funcionar perfeitamente:

if( exploded() )
  do
  {
    char buffer[500];
    sprintf(buffer,
        "MYTRACE: %s(%d) %s\n",
        __FILE__,
        __LINE__,
        "Oh, my God");
    OutputDebugString(buffer);
  }
  while( 0 );
else
  do
  {
    char buffer[500];
    sprintf(buffer,
        "MYTRACE: %s(%d) %s\n",
        __FILE__,
        __LINE__,
        "That's right");
    OutputDebugString(buffer);
  }
  while( 0 );

// Comments

2008-05-16 Blabos:

Excelente artigo. Mas vc é um sacana, eu ia falar justo disso no meu blog, no artigo de sábado que vem :p

Tenho visto muito esse tipo de construção aqui no trabalho. São uns truquezinhos muito úteis e interessantes, que usam aquelas notas de rodapé dos bons livros.

Outro que eu achei muito interessante foi o uso do escopo de compilação para criar structs com dados privados. Espero ser mas rápido no gatilho desta vez...

No livro "Linux Kernel Development" tem uma porção dessas "mágicas".

Abraços


2008-06-06 Daniel:

quando vc falou sobre isso na palestra do seminário eu até achei que fosse brincadeira. :P


2008-05-16 Caloni:

Olá, Blabos.

Se eu disser que foi a única coisa que eu consegui pensar para essa "quinta-feira sem inspiração" você acredita? =)

Eu não acredito que só porque eu escrevi a respeito você não possa publicar sua visão da coisa. Isso apenas tende a enriquecer o assunto aqui no Brasil que, pelo que pude notar, ainda era intocado.

[]s


2008-06-07 Caloni:

Ehehehehehe. Bom, felizmente, ou infelizmente, não é. É apenas uma saída genial inventada por algum programador para um problema recorrente. E resolve de fato o problema =)

[]s

[aprendendo_conceitos_essenciais_do_windbg] [debug_user_mode_in_kernel_mode]