Macro vs función en C

101

Siempre vi ejemplos y casos en los que usar una macro es mejor que usar una función.

¿Alguien podría explicarme con un ejemplo la desventaja de una macro en comparación con una función?

Kyrol
fuente
21
Dale la vuelta a la pregunta. ¿En qué situación es mejor una macro? Utilice una función real a menos que pueda demostrar que una macro es mejor.
David Heffernan

Respuestas:

113

Las macros son propensas a errores porque se basan en la sustitución textual y no realizan verificación de tipos. Por ejemplo, esta macro:

#define square(a) a * a

funciona bien cuando se usa con un número entero:

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

pero hace cosas muy extrañas cuando se usa con expresiones:

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

Poner paréntesis alrededor de los argumentos ayuda, pero no elimina por completo estos problemas.

Cuando las macros contienen varias declaraciones, puede tener problemas con las construcciones de flujo de control:

#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;

La estrategia habitual para solucionar este problema es colocar las declaraciones dentro de un bucle "do {...} while (0)".

Si tiene dos estructuras que contienen un campo con el mismo nombre pero semántica diferente, la misma macro podría funcionar en ambas, con resultados extraños:

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, las macros pueden ser difíciles de depurar, produciendo extraños errores de sintaxis o errores de tiempo de ejecución que tienes que expandir para entenderlos (por ejemplo, con gcc -E), porque los depuradores no pueden recorrer las macros, como en este ejemplo:

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

Las funciones y constantes en línea ayudan a evitar muchos de estos problemas con las macros, pero no siempre son aplicables. Cuando las macros se utilizan deliberadamente para especificar el comportamiento polimórfico, el polimorfismo involuntario puede ser difícil de evitar. C ++ tiene una serie de características, como plantillas, para ayudar a crear construcciones polimórficas complejas de forma segura sin el uso de macros; consulte El lenguaje de programación C ++ de Stroustrup para obtener más detalles.

D Coetzee
fuente
43
¿Qué pasa con el anuncio de C ++?
Pacerier
4
De acuerdo, esta es una pregunta C, no es necesario agregar sesgos.
ideasman42
16
C ++ es una extensión de C que agrega (entre otras cosas) características destinadas a abordar esta limitación específica de C. No soy fanático de C ++, pero creo que está relacionado con el tema aquí.
D Coetzee
1
Las macros, las funciones en línea y las plantillas se utilizan a menudo en un intento por mejorar el rendimiento. Se usan en exceso y tienden a perjudicar el rendimiento debido al exceso de código, lo que reduce la efectividad de la caché de instrucciones de la CPU. Podemos crear estructuras de datos genéricas rápidas en C sin utilizar estas técnicas.
Sam Watkins
1
De acuerdo con ISO / IEC 9899: 1999 §6.5.1, "Entre el punto de secuencia anterior y el siguiente, un objeto tendrá su valor almacenado modificado como máximo una vez mediante la evaluación de una expresión". (Existe una redacción similar en los estándares C anteriores y posteriores). Por lo tanto, x++*x++no se puede decir que la expresión se incremente xdos veces; de hecho, invoca un comportamiento indefinido , lo que significa que el compilador es libre de hacer lo que quiera: podría incrementar xdos veces, una vez, o nada en absoluto; podría abortar con un error o incluso hacer que los demonios salgan volando de tu nariz .
Psychonaut
39

Funciones macro :

  • La macro está preprocesada
  • Sin verificación de tipo
  • Aumenta la longitud del código
  • El uso de macro puede provocar efectos secundarios
  • La velocidad de ejecución es más rápida
  • Antes de que el nombre de la macro de compilación se reemplace por el valor de la macro
  • Útil cuando el código pequeño aparece muchas veces
  • La macro no comprueba los errores de compilación

Características de la función :

  • La función está compilada
  • La verificación de tipo está hecha
  • La longitud del código sigue siendo la misma
  • Sin efectos secundarios
  • La velocidad de ejecución es más lenta
  • Durante la llamada de función, se realiza la transferencia de control
  • Útil cuando el código grande aparece muchas veces
  • Errores de compilación de comprobaciones de funciones
zangw
fuente
2
Se requiere la referencia "la velocidad de ejecución es más rápida". Cualquier compilador incluso algo competente de la última década funcionará bien en línea si cree que proporcionará un beneficio de rendimiento.
Voo
1
¿No es eso, en el contexto de la computación MCU de bajo nivel (AVR, es decir, ATMega32), las macros son una mejor opción, ya que no aumentan la pila de llamadas, como lo hacen las llamadas a funciones?
hardyVeles
1
@hardyVeles No es así. Los compiladores, incluso para un AVR, pueden codificar en línea de manera muy inteligente. Aquí hay un ejemplo: godbolt.org/z/Ic21iM
Edward
33

Los efectos secundarios son importantes. Este es un caso típico:

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

min(x++, y)

se expande a:

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

xse incrementa dos veces en la misma declaración. (y comportamiento indefinido)


Escribir macros de varias líneas también es un problema:

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

Requieren un \al final de cada línea.


Las macros no pueden "devolver" nada a menos que lo convierta en una sola expresión:

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

No se puede hacer eso en una macro a menos que use la declaración de expresión de GCC. (EDITAR: aunque puede usar un operador de coma ... lo pasó por alto ... pero aún podría ser menos legible).


Orden de operaciones: (cortesía de @ouah)

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

min(x & 0xFF, 42)

se expande a:

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

Pero &tiene menor precedencia que <. Entonces 0xFF < 42se evalúa primero.

Mística
fuente
5
y no poner paréntesis con argumentos macro en la definición de macro puede llevar a problemas de precedencia: por ejemplo,min(a & 0xFF, 42)
ouah
Ah, sí. No vi tu comentario mientras actualizaba la publicación. Supongo que también lo mencionaré.
Mysticial
14

Ejemplo 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;
}

mientras:

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

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

Ejemplo 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 con:

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
fuente
13

En caso de duda, utilice funciones (o funciones en línea).

Sin embargo, las respuestas aquí explican principalmente los problemas con las macros, en lugar de tener una visión simple de que las macros son malas porque los accidentes tontos son posibles.
Puede ser consciente de los peligros y aprender a evitarlos. Luego, use macros solo cuando haya una buena razón para hacerlo.

Hay ciertos casos excepcionales en los que el uso de macros tiene ventajas, entre los que se incluyen:

  • Funciones genéricas, como se indica a continuación, puede tener una macro que se puede utilizar en diferentes tipos de argumentos de entrada.
  • Número variable de argumentos puede asignar a diferentes funciones en lugar de utilizar C de va_args.
    por ejemplo: https://stackoverflow.com/a/24837037/432509 .
  • Ellos pueden opcionalmente incluir información local, tales como cadenas de depuración:
    ( __FILE__, __LINE__, __func__). verifique las condiciones previas / posteriores, asserten caso de falla o incluso afirmaciones estáticas para que el código no se compile en caso de uso incorrecto (principalmente útil para compilaciones de depuración).
  • Inspeccione los argumentos de entrada. Puede hacer pruebas en los argumentos de entrada, como verificar su tipo, tamaño de, verificar que los structmiembros estén presentes antes de lanzar
    (puede ser útil para tipos polimórficos) .
    O verifique que una matriz cumpla con alguna condición de longitud.
    ver: https://stackoverflow.com/a/29926435/432509
  • Si bien se observa que las funciones comprueban el tipo, C también coaccionará valores (ints / floats, por ejemplo). En casos raros, esto puede resultar problemático. Es posible escribir macros que sean más exigentes que una función sobre sus argumentos de entrada. ver: https://stackoverflow.com/a/25988779/432509
  • Su uso como envoltorios de funciones, en algunos casos es posible que desee evitar repetirse, por ejemplo ... func(FOO, "FOO");, podría definir una macro que expanda la cadena por ustedfunc_wrapper(FOO);
  • Cuando desee manipular variables en el ámbito local de las personas que llaman, pasar un puntero a un puntero funciona bien normalmente, pero en algunos casos es menos problemático usar una macro.
    (asignaciones a múltiples variables, para operaciones por píxel, es un ejemplo, puede que prefiera una macro a una función ... aunque todavía depende mucho del contexto, ya que las inlinefunciones pueden ser una opción) .

Es cierto que algunos de estos se basan en extensiones del compilador que no son estándar C. Lo que significa que puede terminar con un código menos portátil, o tener que hacerlo ifdef, por lo que solo se aprovechan cuando el compilador lo admite.


Evitar la instanciación de múltiples argumentos

Teniendo en cuenta esto, ya que es una de las causas más comunes de errores en las macros (pasando, x++por ejemplo, cuando una macro puede incrementarse varias veces) .

Es posible escribir macros que eviten efectos secundarios con múltiples instancias de argumentos.

C11 Genérico

Si desea tener una squaremacro que funcione con varios tipos y tenga soporte C11, puede hacer esto ...

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))

Expresiones de declaración

Esta es una extensión del compilador compatible con GCC, Clang, EKOPath e Intel C ++ (pero no MSVC) ;

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

Entonces, la desventaja de las macros es que necesita saber cómo usarlas para empezar, y que no son tan compatibles.

Un beneficio es que, en este caso, puede usar la misma squarefunción para muchos tipos diferentes.

ideasman42
fuente
1
"... soportado tan ampliamente ..." Apuesto a que la expresión de declaración que mencionaste no es compatible con cl.exe. (Compilador de MS)
gideon
1
@gideon, respuesta editada a la derecha, aunque para cada característica mencionada, no estoy seguro de que sea necesario tener alguna matriz de soporte de características del compilador.
ideasman42
12

No se repite la verificación de tipo de los parámetros y el código, lo que puede provocar que el código se sobrecargue. La sintaxis de macros también puede llevar a cualquier número de casos extremos extraños en los que el punto y coma o el orden de precedencia pueden interferir. Aquí hay un enlace que demuestra algo de maldad macro.

Michael Dorgan
fuente
6

Un inconveniente de las macros es que los depuradores leen el código fuente, que no tiene macros expandidas, por lo que ejecutar un depurador en una macro no es necesariamente útil. No hace falta decir que no puede establecer un punto de interrupción dentro de una macro como puede hacerlo con las funciones.

jim mcnamara
fuente
El punto de ruptura es un asunto muy importante aquí, gracias por señalarlo.
Hans
6

Las funciones comprueban el tipo. Esto le brinda una capa adicional de seguridad.

ncmathsadist
fuente
6

Añadiendo a esta respuesta ...

Las macros son sustituidas directamente en el programa por el preprocesador (ya que básicamente son directivas del preprocesador). Entonces, inevitablemente, usan más espacio de memoria que una función respectiva. Por otro lado, una función requiere más tiempo para ser llamada y devolver resultados, y esta sobrecarga se puede evitar usando macros.

Además, las macros tienen algunas herramientas especiales que pueden ayudar con la portabilidad del programa en diferentes plataformas.

No es necesario asignar a las macros un tipo de datos para sus argumentos en contraste con las funciones.

En general, son una herramienta útil en programación. Y tanto las macroinstrucciones como las funciones se pueden utilizar según las circunstancias.

Nikos
fuente
3

No noté, en las respuestas anteriores, una ventaja de las funciones sobre las macros que creo que es muy importante:

Las funciones se pueden pasar como argumentos, las macros no.

Ejemplo concreto: desea escribir una versión alternativa de la función estándar 'strpbrk' que aceptará, en lugar de una lista explícita de caracteres para buscar dentro de otra cadena, una función (puntero a a) que devolverá 0 hasta que un carácter sea encontró que pasa alguna prueba (definida por el usuario). Una razón por la que podría querer hacer esto es para poder explotar otras funciones de biblioteca estándar: en lugar de proporcionar una cadena explícita llena de puntuación, podría pasar 'ispunct' de ctype.h, etc. Si 'ispunct' se implementó solo como una macro, esto no funcionaría.

Hay muchos otros ejemplos. Por ejemplo, si su comparación se realiza mediante una macro en lugar de una función, no puede pasarla a 'qsort' de stdlib.h.

Una situación análoga en Python es 'imprimir' en la versión 2 frente a la versión 3 (declaración no aceptable frente a función aceptable).

Sean Rostami
fuente
1
Gracias por esta respuesta
Kyrol
1

Si pasa la función como un argumento a la macro, se evaluará cada vez. Por ejemplo, si llama a una de las macros más populares:

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

como eso

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

functionThatTakeLongTime se evaluará 5 veces, lo que puede reducir significativamente el rendimiento

Saffer
fuente