Faculdade Perto

Por que macros podem repetir efeitos colaterais

Entenda como a substituição de tokens em macros C pode repetir efeitos colaterais e como #undef e ## delimitam a expansão.

Faculdade PertoAtualizado em 8 min de leitura

Uma macro semelhante a uma função pode produzir o mesmo efeito colateral mais de uma vez porque o pré-processador substitui tokens, e não porque compreende o argumento como uma operação que deveria ocorrer uma única vez. Quando um parâmetro aparece repetidamente na lista de substituição, os tokens fornecidos como argumento podem ser inseridos em cada uma dessas posições. Se esses tokens representam um efeito colateral, o código resultante pode conter esse efeito mais de uma vez.[1]

A pergunta decisiva, portanto, não é apenas o que o argumento significa. É também quantas vezes o parâmetro correspondente aparece na sequência de tokens que forma a expansão da macro.[1]

A substituição acontece antes da tradução para código-objeto

O pré-processador atua sobre o código-fonte antes da tradução para código-objeto. Nessa etapa, ele dispõe de pouca informação semântica e trabalha principalmente com tokens e diretivas iniciadas por #, entre elas #include, #define, #embed e #if. Essas diretivas são encerradas por uma quebra de linha.[1]

Esse funcionamento explica por que a aparência de chamada de função pode induzir a uma leitura inadequada. A sintaxe pode lembrar uma função, mas o mecanismo relevante continua sendo a substituição de uma sequência de tokens definida pela macro.[1]

Quando o argumento contém um efeito colateral, não há, nesse mecanismo textual, uma regra geral que reduza várias ocorrências do parâmetro a uma única ocorrência do argumento. A repetição na lista de substituição pode gerar repetição no código que seguirá para as etapas posteriores da tradução.[1]

Para você compreender melhor por que o mecanismo é textual: imagine que está recortando e colando automaticamente fragmentos de receita de bolo. Se a receita diz "misture o ingrediente A, depois o ingrediente A de novo", e você substitui "ingrediente A" pela instrução "quebre dois ovos", o resultado será quebrar ovos duas vezes, simplesmente porque aquela instrução aparece duas vezes no papel. O pré-processador funciona assim: não compreende o sentido da instrução, apenas vê a repetição textual e replica a sequência de caracteres conforme foi especificada.

Rastreamento de uma expansão compacta

Considere uma macro cuja lista de substituição contém duas ocorrências de acao e usa outro parâmetro para formar um identificador:

#define REPETE(acao, nome) acao; acao; registrar_##nome()
REPETE(contador++, saldo);

O rastreamento baseado em tokens produz esta forma conceitual:

contador++; contador++; registrar_saldo();

O parâmetro acao aparece duas vezes na lista de substituição. Por isso, os tokens do argumento contador++ também aparecem duas vezes na expansão, levando o efeito colateral a ser repetido no código resultante. Esse é o tipo de resultado inesperado associado ao uso de argumentos com efeitos colaterais em parâmetros repetidos.[1]

O parâmetro nome participa de outro mecanismo. O operador ## concatena tokens durante a expansão, de modo que registrar_ e saldo formam o token registrar_saldo.[1]

O ponto central do rastreamento não depende de imaginar que o pré-processador executa o incremento. Ele trabalha com os tokens antes da tradução para código-objeto; a observação importante é que a expansão deixa duas cópias dos tokens associados ao efeito colateral.[1]

Suponha que você escreva uma macro para incrementar um contador e registrar a ação em um histórico. Na prática, você poderia usá-la assim: REPETE(x++, tran). Ao compilar, o pré-processador lê a definição da macro, vê que acao aparece duas vezes e que nome será concatenado com o operador ##. Ele então expande textualmente: x++; x++; registrar_tran(). O problema salta aos olhos: você escreveu x++ uma única vez na sua chamada, mas o código gerado o executa duas vezes. Isso pode corromper o valor final de x e gerar um histórico incorreto com um incremento duplicado.

Antes de calcular: rastreie o valor e o caminho: infográfico ilustrado do curso C
Infográfico: Antes de calcular: rastreie o valor e o caminho · material do curso de C

Como reconhecer a repetição relevante

A análise pode começar pela lista de substituição, isto é, pela sequência de tokens associada ao nome da macro. Uma macro pode substituir um identificador por essa sequência, e os parâmetros presentes nela determinam onde os argumentos aparecem durante a expansão.[1]

Primeiro, identifique o parâmetro que receberá o argumento com efeito colateral. Depois, conte quantas vezes esse parâmetro aparece na lista de substituição. Se houver várias ocorrências, trate cada uma como uma possível inserção dos tokens do argumento. Essa leitura acompanha diretamente o risco descrito para parâmetros repetidos e argumentos com efeitos colaterais.[1]

Também é necessário observar a expansão inteira, e não somente a grafia da invocação. Na chamada do exemplo, contador++ aparece uma única vez no texto escrito pelo programador, mas duas vezes depois da substituição. A quantidade relevante para compreender o resultado é a quantidade de ocorrências deixadas pela expansão.[1]

Esse critério é deliberadamente estreito. Ele permite reconhecer a ligação entre repetição textual e repetição do efeito colateral, mas as fontes fornecidas não detalham todas as regras semânticas que poderiam afetar expressões mais complexas. Por isso, não cabe inferir delas uma classificação completa de todo resultado possível em C.

A prática segura é desenhar ou traçar mentalmente a expansão de cada macro que recebe argumentos com efeitos colaterais. Escreva o #define, copie a lista de substituição, substitua cada parâmetro pelo argumento e observe o resultado. Se o argumento contém um operador como ++, --, uma chamada de função ou uma atribuição, considere com atenção quantas vezes ele vai aparecer após a expansão. Consulte também a documentação ou o código-fonte se a macro for definida em uma biblioteca; às vezes, uma macro que parece simples esconde uma lista de substituição complexa com parâmetros repetidos ocultos.

O alcance não acompanha os blocos do programa

Uma definição de macro permanece aplicável até uma diretiva #undef correspondente ou até o fim da unidade de tradução. Esse alcance independe dos blocos do programa.[1]

Assim, chaves de função, comandos condicionais ou outros blocos não encerram, por si mesmos, a definição da macro. Para saber qual substituição está ativa em determinado ponto, é necessário acompanhar o fluxo das diretivas desde a definição até um eventual #undef ou até o fim da unidade de tradução.[1]

A macro também pode ser definida pela linha de comando do compilador. Nesse caso, sua existência pode não estar visível como uma diretiva #define no trecho de código examinado, embora seu alcance continue terminando por #undef correspondente ou pelo fim da unidade de tradução.[1]

Esse alcance importa para o problema dos efeitos colaterais porque a repetição só pode ser reconhecida corretamente depois de identificar qual definição está ativa no ponto da invocação.[1]

Leia também sobre quando o segfault aparece dentro da biblioteca para entender como depurar macros que vêm de código externo, e consulte por que uma string em C precisa de um byte além dos caracteres visíveis para ver outro exemplo de substituição textual que pode causar confusão se não for rastreada corretamente.

Compilou, mas a evidência ainda não terminou: infográfico ilustrado do curso C
Infográfico: Compilou, mas a evidência ainda não terminou · material do curso de C

O papel específico do operador ##

O operador ## concatena tokens durante a expansão de uma macro. Seu papel é formar um token a partir dos tokens posicionados ao redor do operador.[1]

Essa concatenação não elimina a repetição de outro parâmetro na lista de substituição. No exemplo, ## explica a formação de registrar_saldo, enquanto as duas ocorrências de acao explicam as duas inserções de contador++. São efeitos diferentes dentro da mesma expansão e devem ser rastreados separadamente.[1]

A leitura segura é, portanto, token por token: localizar os parâmetros repetidos, projetar cada substituição e, em outra etapa da análise, observar quais tokens são unidos por ##. Esse procedimento torna visível o vínculo exato entre a forma da macro e a duplicação do efeito colateral.[1]

Contexto curricular: conteúdo relacionado do curso.

O operador ## por si não causa a repetição de efeitos colaterais; ele apenas une tokens adjacentes em um novo token. No exemplo da seção anterior, registrar_##nome se torna registrar_saldo, e isso ocorre uma única vez. O risco de repetição vem dos parâmetros que aparecem múltiplas vezes na macro, não do operador ##. Contudo, é comum que macros avançadas que usam ## também usem parâmetros repetidos, criando uma combinação de dois mecanismos diferentes que precisam ser rastreados em paralelo. Por isso, ao revisar uma macro com ##, sempre verifique se os outros parâmetros também não estão repetidos.

Perguntas frequentes

Como saber se uma macro vai repetir um efeito colateral?

Verifique a lista de substituição da macro e conte quantas vezes o parâmetro que receberá o argumento aparece nela. Se aparecer mais de uma vez e o argumento contiver um efeito colateral (como x++ ou funcao()), o efeito ocorrerá repetidas vezes no código expandido.[1]

A macro pode ser definida pela linha de comando do compilador?

Sim. Quando uma macro é passada via -D no compilador, sua definição não aparece no arquivo .c ou .h, mas seu alcance continua até uma diretiva #undef correspondente ou até o fim da unidade de tradução, assim como qualquer outra definição.[1]

O operador ## pode causar a repetição de um efeito colateral?

Não. O operador ## apenas concatena tokens; não causa repetição por si. A repetição vem dos parâmetros que aparecem múltiplas vezes na lista de substituição da macro. Porém, macros que usam ## frequentemente usam também parâmetros repetidos, então ambos os mecanismos devem ser observados.[1]

Por que o pré-processador não compreende que o argumento já foi fornecido uma única vez?

O pré-processador trabalha apenas com tokens e diretivas, sem informação semântica sobre o significado ou efeitos de uma expressão. Se o parâmetro aparece três vezes na macro, ele substitui textualmente três vezes, independentemente da intenção do programador.[1]

Fontes consultadas

Obras e aulas usadas na redação deste guia. Os números no texto levam a elas.

  1. SEACORD, Robert C. Effective C: An Introduction to Professional C Programming. 2ª edição. Capítulo 16

Continue lendo