Por que as macros do pré-processador são malignas e quais são as alternativas?

Eu sempre perguntei isso, mas nunca recebi uma resposta realmente boa; Eu acho que quase qualquer programador antes mesmo de escrever o primeiro “Hello World” encontrou uma frase como “macro nunca deve ser usada”, “macro são malignas” e assim por diante, minha pergunta é: por quê? Com o novo C ++ 11 existe uma alternativa real depois de tantos anos?

A parte fácil é sobre macros como # #pragma , que são específicas da plataforma e específicas do compilador, e na maioria das vezes têm falhas graves como #pragma once que é propenso a erros em pelo menos 2 situações importantes: mesmo nome em caminhos diferentes e com alguns configurações de rede e filesystems.

Mas, em geral, e as macros e alternativas ao seu uso?

    Macros são como qualquer outra ferramenta – um martelo usado em um assassinato não é mal porque é um martelo. É mal no modo como a pessoa a usa dessa maneira. Se você quer marcanvasr pregos, um martelo é uma ferramenta perfeita.

    Existem alguns aspectos para as macros que os tornam “ruins” (vou expandir mais tarde e sugerir alternativas):

    1. Você não pode depurar macros.
    2. A expansão macro pode levar a efeitos colaterais estranhos.
    3. Macros não têm “namespace”, então se você tem uma macro que se choca com um nome usado em outro lugar, você obtém substituições de macro onde você não quer, e isso geralmente leva a mensagens de erro estranhas.
    4. Macros podem afetar coisas que você não percebe.

    Então vamos expandir um pouco aqui:

    1) As macros não podem ser depuradas. Quando você tem uma macro que traduz para um número ou uma seqüência de caracteres, o código-fonte terá o nome da macro e muitos depuradores, você não pode “ver” o que a macro se traduz. Então você não sabe realmente o que está acontecendo.

    Substituição : use enum ou const T

    Para macros “semelhantes a funções”, como o depurador funciona em um nível “por linha de origem em que você está”, sua macro atuará como uma única instrução, não importando se é uma instrução ou uma centena. Torna difícil descobrir o que está acontecendo.

    Substituição : Use funções – inline, se precisar ser “rápido” (mas cuidado com o fato de que inline demais não é bom)

    2) Expansões macro podem ter efeitos colaterais estranhos.

    O famoso é #define SQUARE(x) ((x) * (x)) e o uso x2 = SQUARE(x++) . Isso leva a x2 = (x++) * (x++); , o que, mesmo que fosse um código válido [1], quase certamente não seria o que o programador queria. Se fosse uma function, seria bom fazer x ++, e x só incrementaria uma vez.

    Outro exemplo é “if else” em macros, digamos que temos isto:

     #define safe_divide(res, x, y) if (y != 0) res = x/y; 

    e depois

     if (something) safe_divide(b, a, x); else printf("Something is not set..."); 

    Na verdade, torna-se completamente errado …

    Substituição : funções reais.

    3) Macros não têm namespace

    Se tivermos uma macro:

     #define begin() x = 0 

    e nós temos algum código em C ++ que usa begin:

     std::vector v; ... stuff is loaded into v ... for (std::vector::iterator it = myvector.begin() ; it != myvector.end(); ++it) std::cout < < ' ' << *it; 

    Agora, que erro você acha que tem, e aonde você procura um erro [supondo que você tenha esquecido completamente - ou sequer soubesse - a macro inicial que vive em algum arquivo de header que alguém escreveu? [e ainda mais divertido se você incluiu essa macro antes da inclusão - você estaria se afogando em erros estranhos que não faz absolutamente nenhum sentido quando você olha para o código em si.

    Substituição : Bem, não existe uma substituição como uma "regra" - use apenas nomes em maiúsculas para macros e nunca use todos os nomes em maiúsculas para outras coisas.

    4) Macros têm efeitos que você não percebe

    Tome esta function:

     #define begin() x = 0 #define end() x = 17 ... a few thousand lines of stuff here ... void dostuff() { int x = 7; begin(); ... more code using x ... printf("x=%d\n", x); end(); } 

    Agora, sem olhar para a macro, você pensaria que begin é uma function, que não deveria afetar x.

    Esse tipo de coisa, e eu vi exemplos muito mais complexos, pode realmente estragar o seu dia!

    Substituição : não use uma macro para definir x ou passar x como um argumento.

    Há momentos em que o uso de macros é definitivamente benéfico. Um exemplo é envolver uma function com macros para passar informações de arquivo / linha:

     #define malloc(x) my_debug_malloc(x, __FILE__, __LINE__) #define free(x) my_debug_free(x, __FILE__, __LINE__) 

    Agora podemos usar my_debug_malloc como o malloc regular no código, mas ele tem argumentos extras, então quando chega ao fim e nós digitalizamos "quais elementos de memory não foram liberados", podemos imprimir onde a alocação foi feita. o programador pode rastrear o vazamento.

    [1] É um comportamento indefinido para atualizar uma variável mais de uma vez "em um ponto de sequência". Um ponto de seqüência não é exatamente o mesmo que uma declaração, mas para a maioria das intenções e propósitos, é disso que devemos considerá-lo. Então fazer x++ * x++ atualizará x duas vezes, o que é indefinido e provavelmente levará a valores diferentes em sistemas diferentes, e valores de resultados diferentes em x também.

    O ditado “macros são maus” geralmente se refere ao uso de #define, não #pragma.

    Especificamente, a expressão se refere a esses dois casos:

    • definindo números mágicos como macros

    • usando macros para replace expressões

    com o novo C ++ 11 existe uma alternativa real depois de tantos anos?

    Sim, para os itens da lista acima (números mágicos devem ser definidos com const / constexpr e expressões devem ser definidas com funções [normal / inline / modelo / modelo inline].

    Aqui estão alguns dos problemas introduzidos definindo números mágicos como macros e substituindo expressões por macros (em vez de definir funções para avaliar essas expressões):

    • Ao definir macros para números mágicos, o compilador não retém informações de tipo para os valores definidos. Isso pode causar avisos de compilation (e erros) e confundir pessoas depurando o código.

    • ao definir macros em vez de funções, os programadores que usam esse código esperam que funcionem como funções e não funcionam.

    Considere este código:

     #define max(a, b) ( ((a) > (b)) ? (a) : (b) ) int a = 5; int b = 4; int c = max(++a, b); 

    Você esperaria que a e c fossem 6 após a atribuição para c (como seria, usando std :: max em vez da macro). Em vez disso, o código executa:

     int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7 

    Além disso, as macros não suportam namespaces, o que significa que a definição de macros em seu código limitará o código do cliente em quais nomes eles podem usar.

    Isso significa que, se você definir a macro acima (para max), não será mais possível #include em qualquer código abaixo, a menos que você escreva explicitamente:

     #ifdef max #undef max #endif #include  

    Ter macros em vez de variables ​​/ funções também significa que você não pode obter o endereço delas:

    • Se uma macro-como-constante for avaliada como um número mágico, você não poderá passá-la pelo endereço

    • para uma macro-function, você não pode usá-la como um predicado ou pegar o endereço da function ou tratá-la como um functor.

    Edit: Como um exemplo, a alternativa correta para o #define max acima:

     template inline T max(const T& a, const T& b) { return a > b ? a : b; } 

    Isso faz tudo que a macro faz, com uma limitação: se os tipos de argumentos forem diferentes, a versão do modelo força você a ser explícito (o que na verdade leva a um código mais explícito e seguro):

     int a = 0; double b = 1.; max(a, b); 

    Se este max é definido como uma macro, o código será compilado (com um aviso).

    Se esta max é definida como uma function template, o compilador irá apontar a ambiguidade, e você terá que dizer max(a, b) ou max(a, b) (e assim declarar explicitamente sua intenção ).

    Um problema comum é este:

     #define DIV(a,b) a / b printf("25 / (3+2) = %d", DIV(25,3+2)); 

    Ele imprimirá 10, não 5, porque o pré-processador expandirá assim:

     printf("25 / (3+2) = %d", 25 / 3 + 2); 

    Esta versão é mais segura:

     #define DIV(a,b) (a) / (b) 

    As macros são valiosas especialmente para criar código genérico (os parâmetros da macro podem ser qualquer coisa), às vezes com parâmetros.

    Mais, este código é colocado (ou seja, inserido) no ponto da macro é usado.

    OTOH, resultados semelhantes podem ser obtidos com:

    • funções sobrecarregadas (diferentes tipos de parâmetros)

    • templates, em C ++ (tipos e valores de parâmetros genéricos)

    • funções embutidas (código do local onde são chamadas, em vez de saltar para uma definição de ponto único – no entanto, isso é uma recomendação para o compilador).

    edit: como por que a macro é ruim:

    1) nenhuma verificação de tipo dos argumentos (eles não têm nenhum tipo), então pode ser facilmente mal utilizado 2) às vezes expandir em código muito complexo, que pode ser difícil de identificar e entender no arquivo pré-processado 3) é fácil fazer erro -prone código em macros, como:

     #define MULTIPLY(a,b) a*b 

    e depois ligue

     MULTIPLY(2+3,4+5) 

    que se expande em

    2 + 3 * 4 + 5 (e não em: (2 + 3) * (4 + 5)).

    Para ter este último, você deve definir:

     #define MULTIPLY(a,b) ((a)*(b)) 

    Eu acho que o problema é que as macros não são bem otimizadas pelo compilador e são “feias” para ler e depurar.

    Geralmente, boas alternativas são funções genéricas e / ou funções inline.

    Eu não acho que haja algo errado em usar definições ou macros de pré-processador como você as chama.

    Eles são um conceito de (meta) linguagem encontrado em c / c ++ e, como qualquer outra ferramenta, podem tornar sua vida mais fácil se você souber o que está fazendo. O problema com as macros é que elas são processadas antes do seu código c / c ++ e geram um novo código que pode estar com defeito e causar erros no compilador que são óbvios. No lado positivo, eles podem ajudá-lo a manter seu código limpo e poupar bastante digitação, se usado adequadamente, de modo que se reduz a preferência pessoal.

    Macros em C / C ++ podem servir como uma ferramenta importante para o version control. O mesmo código pode ser entregue a dois clientes com uma configuração secundária de macros. Eu uso coisas como

     #define IBM_AS_CLIENT #ifdef IBM_AS_CLIENT #define SOME_VALUE1 X #define SOME_VALUE2 Y #else #define SOME_VALUE1 P #define SOME_VALUE2 Q #endif 

    Esse tipo de funcionalidade não é tão facilmente possível sem macros. As macros são, na verdade, uma ótima ferramenta de gerenciamento de configuração de software e não apenas uma maneira de criar atalhos para a reutilização de código. Definindo funções para fins de reutilização em macros pode definitivamente criar problemas.