Por que as macros de pré-processador são ruins e quais são as alternativas?

92

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 ruins" 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 do compilador, e na maioria das vezes têm sérias falhas como #pragma onceessa propensa a erros em pelo menos 2 situações importantes: mesmo nome em caminhos diferentes e com algumas configurações de rede e sistemas de arquivos.

Mas, em geral, e quanto às macros e alternativas para seu uso?

user1849534
fonte
19
#pragmanão é macro.
FooF
1
@foof diretiva de pré-processador?
user1849534
6
@ user1849534: Sim, é isso mesmo ... e não se trata de conselhos sobre macros #pragma.
Ben Voigt
1
Você pode fazer muito com constexpr, inlinefunções e templates, mas boost.preprocessore chaosmostrar que as macros têm seu lugar. Sem mencionar as macros de configuração para compiladores, plataformas, etc.
Brandon
1
possível duplicata de Quando as macros C ++ são benéficas?
Aaron McDaid

Respostas:

161

As macros são como qualquer outra ferramenta - um martelo usado em um assassinato não é mau porque é um martelo. É mau da maneira como a pessoa o usa. Se você quer martelar pregos, um martelo é a ferramenta perfeita.

Existem alguns aspectos das macros que as tornam "ruins" (expandirei cada uma delas posteriormente e sugerirei alternativas):

  1. Você não pode depurar macros.
  2. A expansão macro pode levar a efeitos colaterais estranhos.
  3. As macros não têm "espaço de nomes", portanto, se você tiver uma macro que entre em conflito com um nome usado em outro lugar, receberá substituições de macro onde não deseja, e isso geralmente leva a mensagens de erro estranhas.
  4. As 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 se traduz em um número ou string, o código-fonte terá o nome da macro, e muitos depuradores, você não pode "ver" para o que a macro se traduz. Então você realmente não sabe o que está acontecendo.

Substituição : Use enumouconst T

Para macros "semelhantes a funções", porque o depurador trabalha em um nível "por linha de origem onde você estiver", sua macro funcionará como uma única instrução, não importa se é uma instrução ou cem. Torna difícil descobrir o que está acontecendo.

Substituição : use funções - inline se precisar ser "rápido" (mas cuidado, pois muito inline não é uma coisa boa)

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++);que, mesmo que fosse um código válido [1], quase certamente não seria o que o programador queria. Se fosse uma função, seria bom fazer x ++ e x só aumentaria 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 temos algum código em C ++ que usa begin:

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

Agora, que mensagem de erro você acha que recebeu e onde você procura um erro [presumindo que você tenha esquecido completamente - ou nem mesmo sabido - a macro inicial que reside em algum arquivo de cabeçalho que outra pessoa escreveu? [e ainda mais divertido se você incluir essa macro antes da inclusão - você estará se afogando em erros estranhos que não fazem absolutamente nenhum sentido quando você olha para o próprio código.

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

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

Faça esta função:

#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 função, que não deve afetar x.

Esse tipo de coisa, e já vi exemplos muito mais complexos, pode REALMENTE bagunçar o seu dia!

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

Há momentos em que usar macros é definitivamente benéfico. Um exemplo é envolver uma função 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_malloccomo o malloc regular no código, mas ele tem argumentos extras, então quando chegar ao final e verificarmos "quais elementos de memória não foram liberados", podemos imprimir onde a alocação foi feita para que o o programador pode rastrear o vazamento.

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

Mats Petersson
fonte
6
Os if elseproblemas podem ser resolvidos envolvendo o corpo da macro dentro do { ... } while(0). Este comporta-se como seria de esperar com relação a ife fore outras questões de controle de fluxo potencialmente arriscados. Mas sim, uma função real geralmente é uma solução melhor. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
Aaron McDaid
11
@AaronMcDaid: Sim, existem algumas soluções alternativas que resolvem alguns dos problemas expostos nessas macros. O objetivo do meu post não foi mostrar como fazer macros bem, mas "como é fácil errar as macros", onde há uma boa alternativa. Dito isso, há coisas que as macros resolvem com muita facilidade e há momentos em que as macros também são a coisa certa a fazer.
Mats Petersson,
1
No ponto 3, os erros não são mais um problema. Compiladores modernos como o Clang dirão algo semelhante note: expanded from macro 'begin'e mostrarão onde beginestá definido.
kirbyfan64sos
5
As macros são difíceis de traduzir para outros idiomas.
Marco van de Voort
1
@FrancescoDondi: stackoverflow.com/questions/4176328/… (um pouco abaixo nessa resposta, fala sobre i ++ * i ++ e outros semelhantes.
Mats Petersson
21

O ditado "macros são ruins" geralmente se refere ao uso de #define, não #pragma.

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

  • definindo números mágicos como macros

  • usando macros para substituir expressões

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

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

Aqui estão alguns dos problemas introduzidos pela definição de 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 compilação (e erros) e confundir as pessoas ao depurar 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 aec fosse 6 após a atribuição a 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 oferecem suporte a namespaces, o que significa que definir 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 capaz de #include <algorithm>em qualquer um dos códigos abaixo, a menos que você escreva explicitamente:

#ifdef max
#undef max
#endif
#include <algorithm>

Ter macros em vez de variáveis ​​/ funções também significa que você não pode pegar seus endereços:

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

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

Edit: Por exemplo, a alternativa correta ao #define maxanterior:

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

Isso faz tudo o 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 seguro e explícito):

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

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

Se este máximo for definido como uma função de modelo, o compilador apontará a ambigüidade e você terá que dizer max<int>(a, b)ou max<double>(a, b)(e assim declarar explicitamente sua intenção).

utnapistim
fonte
1
Não precisa ser específico do c ++ 11; você pode simplesmente usar funções para substituir o uso de macros como expressões e [estático] const / constexpr para substituir o uso de macros como constantes.
utnapistim
1
Até mesmo C99 permite o uso de const int someconstant = 437;, e pode ser usado quase de todas as maneiras que uma macro seria usada. Da mesma forma para pequenas funções. Existem algumas coisas onde você pode escrever algo como uma macro que não funcionará em uma expressão regular em C (você pode fazer algo que calcule a média de um array de qualquer tipo de número, o que C não pode fazer - mas C ++ tem modelos por isso). Embora o C ++ 11 adicione mais algumas coisas que "você não precisa de macros para isso", a maioria já foi resolvida no C / C ++ anterior.
Mats Petersson
Fazer um pré-incremento enquanto passa um argumento é uma prática de codificação terrível. E qualquer um que codifique em C / C ++ deve não presumir que uma chamada semelhante a uma função não seja uma macro.
StephenG
Muitas implementações colocam os identificadores voluntariamente entre parênteses maxe minse eles forem seguidos por um parêntese esquerdo. Mas você não deve definir tais macros ...
LF
14

Um problema comum é este:

#define DIV(a,b) a / b

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

Irá imprimir 10, não 5, porque o pré-processador irá expandir desta forma:

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

Esta versão é mais segura:

#define DIV(a,b) (a) / (b)
faazon
fonte
2
exemplo interessante, basicamente são apenas tokens sem semântica
user1849534
Sim. Eles são expandidos da maneira como são fornecidos à macro. A DIVmacro pode ser reescrita com um par de () ao redor b.
phaazon
2
Quer dizer #define DIV(a,b), não #define DIV (a,b), o que é muito diferente.
rici
6
#define DIV(a,b) (a) / (b)não é bom o suficiente; como prática geral, sempre adicione colchetes externos, como este:#define DIV(a,b) ( (a) / (b) )
PJTraill
3

As macros são valiosas especialmente para a criação de 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 em que a macro é usada.

OTOH, resultados semelhantes podem ser obtidos com:

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

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

  • funções embutidas (coloque o código onde são chamados, em vez de pular para uma definição de ponto único - no entanto, esta é uma recomendação para o compilador).

editar: quanto ao motivo pelo qual a macro é ruim:

1) sem verificação de tipo dos argumentos (eles não têm tipo), então podem ser facilmente mal utilizados 2) às vezes se expandem em um código muito complexo, que pode ser difícil de identificar e entender no arquivo pré-processado 3) é fácil cometer erros -prone código em macros, como:

#define MULTIPLY(a,b) a*b

e então ligar

MULTIPLY(2+3,4+5)

que se expande em

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

Para ter o último, você deve definir:

#define MULTIPLY(a,b) ((a)*(b))
user1284631
fonte
3

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

Eles são um conceito de (meta) linguagem encontrado em c / c ++ e, como qualquer outra ferramenta, podem facilitar sua vida 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 do compilador que são quase óbvios. Pelo lado positivo, eles podem ajudá-lo a manter seu código limpo e economizar muita digitação, se usado de maneira adequada, portanto, tudo se resume à preferência pessoal.

Sandi Hrvić
fonte
Além disso, como apontado por outras respostas, definições de pré-processador mal projetadas podem produzir código com sintaxe válida, mas significado semântico diferente, o que significa que o compilador não reclamará e você introduziu um bug em seu código que será ainda mais difícil de encontrar.
Sandi Hrvić
3

Macros em C / C ++ podem servir como uma ferramenta importante para controle de versão. 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. Macros são, na verdade, uma ótima ferramenta de gerenciamento de configuração de software e não apenas uma forma de criar atalhos para reutilização de código. Definir funções com o propósito de reutilização em macros pode definitivamente criar problemas.

Indiangarg
fonte
Definir valores de macro no cmdline durante a compilação para construir duas variantes de uma base de código é muito bom. Com moderação.
kevinf
1
De alguma perspectiva, esse uso é o mais perigoso: ferramentas (IDEs, analisadores estáticos, refatoração) terão dificuldade em descobrir os caminhos de código possíveis.
erenon
1

Acho que o problema é que as macros não são bem otimizadas pelo compilador e são "feias" para ler e depurar.

Freqüentemente, uma boa alternativa são funções genéricas e / ou funções inline.

Davide Icardi
fonte
2
O que o leva a acreditar que as macros não são bem otimizadas? Eles são simples substituições de texto e o resultado é otimizado tanto quanto o código escrito sem macros.
Ben Voigt
@BenVoigt, mas eles não consideram a semântica e isso pode levar a algo que pode ser considerado como "não ideal" ... pelo menos esta é minha primeira lição sobre stackoverflow.com/a/14041502/1849534
user1849534
1
@ user1849534: Não é isso que a palavra "otimizado" significa no contexto de compilação.
Ben Voigt
1
@BenVoigt Exatamente, macros são apenas substituições de texto. O compilador apenas duplica o código, não é um problema de desempenho, mas pode aumentar o tamanho do programa. Especialmente verdadeiro em alguns contextos onde você tem limitações de tamanho do programa. Alguns códigos estão tão cheios de macros que o tamanho do programa é o dobro.
Davide Icardi
1

Macros de pré-processador não são ruins quando são usadas para fins pretendidos, como:

  • Criação de versões diferentes do mesmo software usando construções do tipo #ifdef, por exemplo, a versão de janelas para regiões diferentes.
  • Para definir os valores relacionados ao teste de código.

Alternativas- Pode-se usar algum tipo de arquivo de configuração no formato ini, xml, json para fins semelhantes. Mas usá-los terá efeitos de tempo de execução no código que uma macro do pré-processador pode evitar.

Indiangarg
fonte
1
desde C ++ 17 constexpr if + um arquivo de cabeçalho que contém variáveis ​​constexpr "config" pode substituir os # ifdef's.
Enhex