Macro vs Função em C

101

Sempre vi exemplos e casos em que usar macro é melhor do que usar função.

Alguém poderia me explicar com um exemplo a desvantagem de uma macro em relação a uma função?

Kyrol
fonte
21
Vire a questão de cabeça para baixo. Em que situação uma macro é melhor? Use uma função real, a menos que você possa demonstrar que uma macro é melhor.
David Heffernan

Respostas:

114

As macros estão sujeitas a erros porque dependem da substituição textual e não executam a verificação de tipo. Por exemplo, esta macro:

#define square(a) a * a

funciona bem quando usado com um inteiro:

square(5) --> 5 * 5 --> 25

mas faz coisas muito estranhas quando usado com expressões:

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice

Colocar os argumentos entre parênteses ajuda, mas não elimina completamente esses problemas.

Quando as macros contêm várias instruções, você pode ter problemas com construções de fluxo de controle:

#define swap(x, y) t = x; x = y; y = t;

if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;

A estratégia usual para consertar isso é colocar as instruções dentro de um loop "do {...} while (0)".

Se você tiver duas estruturas que contenham um campo com o mesmo nome, mas semânticas diferentes, a mesma macro pode funcionar em ambos, com resultados estranhos:

struct shirt 
{
    int numButtons;
};

struct webpage 
{
    int numButtons;
};

#define num_button_holes(shirt)  ((shirt).numButtons * 4)

struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8

Finalmente, macros podem ser difíceis de depurar, produzindo erros de sintaxe estranhos ou erros de tempo de execução que você precisa expandir para entender (por exemplo, com gcc -E), porque os depuradores não podem percorrer as macros, como neste exemplo:

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */

As funções e constantes embutidas ajudam a evitar muitos desses problemas com macros, mas nem sempre são aplicáveis. Quando as macros são usadas deliberadamente para especificar o comportamento polimórfico, o polimorfismo não intencional pode ser difícil de evitar. C ++ tem uma série de recursos, como modelos para ajudar a criar construções polimórficas complexas de uma maneira segura de tipos, sem o uso de macros; consulte The C ++ Programming Language de Stroustrup para obter detalhes.

D Coetzee
fonte
43
O que há com o anúncio C ++?
Pacerier
4
Concordo, esta é uma questão C, não há necessidade de adicionar viés.
ideasman42 de
16
C ++ é uma extensão de C que adiciona (entre outras coisas) recursos destinados a lidar com essa limitação específica de C. Não sou fã de C ++, mas acho que está no assunto aqui.
D Coetzee
1
Macros, funções embutidas e modelos são freqüentemente usados ​​na tentativa de aumentar o desempenho. Eles são usados ​​demais e tendem a prejudicar o desempenho devido ao inchaço do código, que reduz a eficácia do cache de instruções da CPU. Podemos criar estruturas de dados genéricas rápidas em C sem usar essas técnicas.
Sam Watkins
1
De acordo com a ISO / IEC 9899: 1999 §6.5.1, "Entre o ponto de sequência anterior e o próximo um objeto deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão." (Formulação semelhante existe em padrões C anteriores e subsequentes.) Portanto, x++*x++não se pode dizer que a expressão aumenta xduas vezes; na verdade, ele invoca um comportamento indefinido , o que significa que o compilador está livre para fazer o que quiser - ele pode incrementar xduas vezes, ou uma vez, ou não aumentar ; ele pode abortar com um erro ou até mesmo fazer demônios voarem pelo seu nariz .
Psiconauta
40

Recursos macro :

  • Macro é pré - processado
  • Sem verificação de tipo
  • Aumento do comprimento do código
  • O uso de macro pode levar a um efeito colateral
  • Velocidade de execução é mais rápida
  • Antes de o nome da macro de compilação ser substituído pelo valor da macro
  • Útil onde pequenos códigos aparecem muitas vezes
  • Macro não verifica erros de compilação

Recursos da função :

  • A função é compilada
  • A verificação de tipo está concluída
  • O comprimento do código permanece o mesmo
  • Sem efeito colateral
  • Velocidade de execução é mais lenta
  • Durante a chamada de função, a transferência de controle ocorre
  • Útil onde um código grande aparece muitas vezes
  • Verificações de função, erros de compilação
zangw
fonte
2
É necessária uma referência de "velocidade de execução mais rápida". Qualquer compilador competente da última década funcionará em linha muito bem se achar que fornecerá um benefício de desempenho.
Voo
1
Não é isso, no contexto da computação MCU de baixo nível (AVRs, ou seja, ATMega32), as macros são a melhor escolha, já que não aumentam a pilha de chamadas, como fazem as chamadas de função?
hardyVeles
1
@hardyVeles Não é assim. Compiladores, mesmo para um AVR, podem codificar em linha de forma muito inteligente. Aqui está um exemplo: godbolt.org/z/Ic21iM
Edward
34

Os efeitos colaterais são importantes. Aqui está um caso típico:

#define min(a, b) (a < b ? a : b)

min(x++, y)

é expandido para:

(x++ < y ? x++ : y)

xé incrementado duas vezes na mesma instrução. (e comportamento indefinido)


Escrever macros multilinhas também é uma dor:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;

Eles exigem um \no final de cada linha.


As macros não podem "retornar" nada, a menos que você faça uma única expressão:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

Não posso fazer isso em uma macro a menos que você use a instrução de expressão do GCC. (EDITAR: Você pode usar um operador vírgula, embora ... esquecido ... Mas ainda pode ser menos legível.)


Ordem de operações: (cortesia de @ouah)

#define min(a,b) (a < b ? a : b)

min(x & 0xFF, 42)

é expandido para:

(x & 0xFF < 42 ? x & 0xFF : 42)

Mas &tem precedência menor do que <. Portanto, 0xFF < 42é avaliado primeiro.

Mysticial
fonte
5
e não colocar parênteses com argumentos de macro na definição de macro pode levar a problemas de precedência: por exemplo,min(a & 0xFF, 42)
ouah
Ah sim. Não vi seu comentário enquanto atualizava a postagem. Acho que vou mencionar isso também.
Mysticial 01 de
14

Exemplo 1:

#define SQUARE(x) ((x)*(x))

int main() {
  int x = 2;
  int y = SQUARE(x++); // Undefined behavior even though it doesn't look 
                       // like it here
  return 0;
}

enquanto que:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

Exemplo 2:

struct foo {
  int bar;
};

#define GET_BAR(f) ((f)->bar)

int main() {
  struct foo f;
  int a = GET_BAR(&f); // fine
  int b = GET_BAR(&a); // error, but the message won't make much sense unless you
                       // know what the macro does
  return 0;
}

Comparado com:

struct foo {
  int bar;
};

int get_bar(struct foo *f) {
  return f->bar;
}

int main() {
  struct foo f;
  int a = get_bar(&f); // fine
  int b = get_bar(&a); // error, but compiler complains about passing int* where 
                       // struct foo* should be given
  return 0;
}
Flexo
fonte
13

Em caso de dúvida, use funções (ou funções embutidas).

No entanto, as respostas aqui explicam principalmente os problemas com macros, em vez de ter uma visão simples de que macros são ruins porque acidentes bobos são possíveis.
Você pode estar ciente das armadilhas e aprender a evitá-las. Em seguida, use macros apenas quando houver um bom motivo para isso.

Existem certos casos excepcionais em que há vantagens em usar macros, incluindo:

  • Funções genéricas, conforme observado abaixo, você pode ter uma macro que pode ser usada em diferentes tipos de argumentos de entrada.
  • O número variável de argumentos pode ser mapeado para funções diferentes em vez de usar C's va_args.
    por exemplo: https://stackoverflow.com/a/24837037/432509 .
  • Eles podem opcionalmente incluir informações locais, tais como cordas de depuração:
    ( __FILE__, __LINE__, __func__). verifique se há condições pré / pós, assertem caso de falha, ou mesmo declarações estáticas para que o código não compile em uso impróprio (principalmente útil para compilações de depuração).
  • Inspecione argumentos de entrada. Você pode fazer testes em argumentos de entrada, como verificar seu tipo, sizeof, verificar se os structmembros estão presentes antes da conversão
    (pode ser útil para tipos polimórficos) .
    Ou verifique se uma matriz atende a alguma condição de comprimento.
    consulte: https://stackoverflow.com/a/29926435/432509
  • Embora seja notado que as funções fazem verificação de tipo, C irá forçar os valores também (ints / floats, por exemplo). Em casos raros, isso pode ser problemático. É possível escrever macros que são mais exatas do que uma função sobre seus argumentos de entrada. consulte: https://stackoverflow.com/a/25988779/432509
  • Seu uso como wrappers para funções, em alguns casos você pode querer evitar se repetir, por exemplo ... func(FOO, "FOO");, você pode definir uma macro que expande a string para vocêfunc_wrapper(FOO);
  • Quando você deseja manipular variáveis ​​no escopo local do chamador, passar um ponteiro para um ponteiro funciona normalmente, mas em alguns casos é menos problemático usar uma macro.
    (atribuições a múltiplas variáveis, para operações por pixel, é um exemplo de que você pode preferir uma macro em vez de uma função ... embora ainda dependa muito do contexto, já que inlinefunções podem ser uma opção) .

Admitidamente, alguns deles dependem de extensões de compilador que não são o padrão C. Significa que você pode acabar com código menos portável, ou ter que ifdefusá - los, então eles só serão aproveitados quando o compilador suportar.


Evitando instanciação de múltiplos argumentos

Observando isso, já que é uma das causas mais comuns de erros em macros (passagem, x++por exemplo, onde uma macro pode ser incrementada várias vezes) .

é possível escrever macros que evitam efeitos colaterais com múltiplas instanciações de argumentos.

C11 Genérico

Se você gosta de squaremacros que funcionem com vários tipos e suporte C11, você pode fazer isso ...

inline float           _square_fl(float a) { return a * a; }
inline double          _square_dbl(float a) { return a * a; }
inline int             _square_i(int a) { return a * a; }
inline unsigned int    _square_ui(unsigned int a) { return a * a; }
inline short           _square_s(short a) { return a * a; }
inline unsigned short  _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */

#define square(a)                        \
    _Generic((a),                        \
        float:          _square_fl(a),   \
        double:         _square_dbl(a),  \
        int:            _square_i(a),    \
        unsigned int:   _square_ui(a),   \
        short:          _square_s(a),    \
        unsigned short: _square_us(a))

Expressões de declaração

Esta é uma extensão do compilador suportada por GCC, Clang, EKOPath e Intel C ++ (mas não MSVC) ;

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

Portanto, a desvantagem das macros é que você precisa saber como usá-las para começar, e que elas não têm suporte tão amplo.

Um benefício é, neste caso, você pode usar a mesma squarefunção para muitos tipos diferentes.

homem de idéias 42
fonte
1
"... com suporte tão amplo ..." Aposto que a expressão de instrução que você mencionou não é compatível com cl.exe? (Compilador MS)
gideon
1
@gideon, resposta correta editada, embora para cada recurso mencionado, não tenho certeza se é necessário ter alguma matriz de suporte ao recurso do compilador.
ideasman42
12

Nenhuma verificação de tipo de parâmetros e código é repetida, o que pode levar ao inchaço do código. A sintaxe da macro também pode levar a qualquer número de casos extremos estranhos onde ponto-e-vírgula ou ordem de precedência podem atrapalhar. Aqui está um link que demonstra algum mal macro

Michael Dorgan
fonte
6

uma desvantagem das macros é que os depuradores leem o código-fonte, que não tem macros expandidas, portanto, executar um depurador em uma macro não é necessariamente útil. Desnecessário dizer que você não pode definir um ponto de interrupção dentro de uma macro como faz com as funções.

Jim Mcnamara
fonte
O ponto de interrupção é um negócio muito importante aqui, obrigado por apontá-lo.
Hans
6

As funções fazem a verificação de tipo. Isso oferece uma camada extra de segurança.

ncmathsadist
fonte
6

Adicionando a esta resposta ..

As macros são substituídas diretamente no programa pelo pré-processador (já que são basicamente diretivas do pré-processador). Portanto, eles inevitavelmente usam mais espaço de memória do que uma função respectiva. Por outro lado, uma função requer mais tempo para ser chamada e retornar resultados, e essa sobrecarga pode ser evitada usando macros.

Além disso, as macros têm algumas ferramentas especiais que podem ajudar na portabilidade do programa em diferentes plataformas.

As macros não precisam ser atribuídas a um tipo de dados para seus argumentos, em contraste com as funções.

No geral, eles são uma ferramenta útil na programação. E tanto macroinstruções quanto funções podem ser usadas dependendo das circunstâncias.

Nikos
fonte
3

Não notei, nas respostas acima, uma vantagem das funções sobre as macros que considero muito importante:

Funções podem ser passadas como argumentos, macros não.

Exemplo concreto: Você deseja escrever uma versão alternativa da função padrão 'strpbrk' que aceitará, em vez de uma lista explícita de caracteres para pesquisar dentro de outra string, uma função (ponteiro para a) que retornará 0 até que um caractere seja descobriu que passa em algum teste (definido pelo usuário). Uma razão pela qual você pode querer fazer isso é para poder explorar outras funções de biblioteca padrão: em vez de fornecer uma string explícita cheia de pontuação, você poderia passar 'ispunct' de ctype.h em vez disso, etc. Se 'ispunct' foi implementado apenas como uma macro, isso não funcionaria.

Existem muitos outros exemplos. Por exemplo, se sua comparação for realizada por macro ao invés de função, você não pode passá-la para 'qsort' de stdlib.h.

Uma situação análoga em Python é 'imprimir' na versão 2 vs. versão 3 (declaração não passável vs. função passável).

Sean Rostami
fonte
1
Obrigado por esta resposta
Kyrol
1

Se você passar a função como um argumento para a macro, ela será avaliada todas as vezes. Por exemplo, se você chamar uma das macros mais populares:

#define MIN(a,b) ((a)<(b) ? (a) : (b))

Curtiu isso

int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));

functionThatTakeLongTime será avaliado 5 vezes, o que pode diminuir significativamente o desempenho

Saffer
fonte