Daño macro para el código C ++

definir

El lenguaje C ++ ofrece amplias posibilidades para prescindir de macros. ¡Intentemos usar macros lo menos posible!

Inmediatamente haga una reserva de que no soy un fanático y no insto a abandonar las macros por razones idealistas. Por ejemplo, cuando se trata de generar manualmente el mismo tipo de código, puedo reconocer los beneficios de las macros y aceptarlas. Por ejemplo, me relaciono tranquilamente con las macros en programas antiguos escritos con MFC. No tiene sentido luchar con algo como esto:

BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT ) //{{AFX_MSG_MAP(efcDialog) ON_WM_CREATE() ON_WM_DESTROY() //}}AFX_MSG_MAP END_MESSAGE_MAP() 

Hay tales macros, y está bien. Realmente fueron creados para simplificar la programación.

Estoy hablando de otras macros con las que intentan evitar la implementación de una función completa o intentan reducir el tamaño de una función. Considere varios motivos para evitar tales macros.

Nota Este texto fue escrito como una publicación invitada para el blog Simplify C ++. Decidí publicar la versión rusa del artículo aquí. En realidad, estoy escribiendo esta nota para evitar una pregunta de lectores desatentos por qué el artículo no está marcado como "traducción" :). Y aquí, en realidad, una publicación invitada en inglés: " Macro Evil in C ++ Code ".

Primero: el código macro atrae errores


No sé cómo explicar las razones de este fenómeno desde un punto de vista filosófico, pero lo es. Además, los errores relacionados con macro a menudo son muy difíciles de detectar cuando se realiza una revisión de código.

He descrito repetidamente tales casos en mis artículos. Por ejemplo, reemplazando la función isspace con esta macro:

 #define isspace(c) ((c)==' ' || (c) == '\t') 

El programador que usó isspace creía que estaba usando una función real que considera no solo espacios y tabulaciones como espacios en blanco, sino también LF, CR, etc. El resultado es que una de las condiciones siempre es verdadera y el código no funciona según lo previsto. Este error de Midnight Commander se describe aquí .

¿O qué le parece esta taquigrafía para escribir la función std :: printf ?

 #define sprintf std::printf 

Creo que el lector adivina que fue una macro muy infructuosa. Se encontró, por cierto, en el proyecto StarEngine. Lea más sobre esto aquí .

Se podría argumentar que los programadores tienen la culpa de estos errores, no de las macros. Eso es asi. Naturalmente, los programadores siempre tienen la culpa de los errores :).

Es importante que las macros causen errores. Resulta que las macros se deben usar con mayor precisión o nada.

Puedo dar ejemplos de defectos asociados con el uso de macros durante mucho tiempo, y esta nota agradable se convertirá en un documento de varias páginas pesado. Por supuesto, no haré esto, pero mostraré algunos otros casos para convencer.

La biblioteca ATL proporciona macros como A2W, T2W, etc. para convertir cadenas. Sin embargo, pocas personas saben que estas macros son muy peligrosas de usar dentro de los bucles. Dentro de la macro, se llama a la función alloca , que asignará memoria en la pila una y otra vez en cada iteración del bucle. Un programa puede pretender funcionar correctamente. Tan pronto como el programa comienza a procesar largas filas o aumenta el número de iteraciones en el bucle, la pila puede tomar y finalizar en el momento más inesperado. Puede leer más sobre esto en este mini libro (consulte el capítulo "No llame a la función alloca () dentro de los bucles").

Macros como A2W esconden el mal. Parecen funciones, pero, de hecho, tienen efectos secundarios que son difíciles de notar.

No puedo superar intentos similares de reducir el código usando macros:

 void initialize_sanitizer_builtins (void) { .... #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS) \ decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM, \ BUILT_IN_NORMAL, NAME, NULL_TREE); \ set_call_expr_flags (decl, ATTRS); \ set_builtin_decl (ENUM, decl, true); #include "sanitizer.def" if ((flag_sanitize & SANITIZE_OBJECT_SIZE) && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE)) DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size", BT_FN_SIZE_CONST_PTR_INT, ATTR_PURE_NOTHROW_LEAF_LIST) .... } 

Solo la primera línea de la macro se refiere a la declaración if . Las líneas restantes se ejecutarán independientemente de la condición. Podemos decir que este error es del mundo C, ya que lo encontré usando los diagnósticos V640 dentro del compilador GCC. El código GCC está escrito principalmente en C, y en este lenguaje las macros son difíciles de hacer. Sin embargo, debe admitir que este no es el caso. Aquí era bastante posible hacer una función real.

Segundo: la lectura del código se vuelve más complicada


Si se encuentra con un proyecto que está lleno de macros que consisten en otras macros, entonces comprende qué demonios es comprender dicho proyecto. Si no se ha encontrado, entonces tome una palabra, esto es triste. Como ejemplo de código que es difícil de leer, puedo citar el compilador GCC mencionado anteriormente.

Según la leyenda, Apple ha invertido en el desarrollo del proyecto LLVM como una alternativa a GCC debido a la complejidad del código GCC debido a estas macros. No recuerdo dónde lo leí, así que no habrá pruebas.

Tercero: escribir macros es difícil


Es fácil escribir una mala macro. Los encuentro en todas partes con las correspondientes consecuencias. Pero escribir una macro buena y confiable a menudo es más difícil que escribir una función similar.

Escribir una buena macro es difícil porque, a diferencia de una función, no puede considerarse como una entidad independiente. Se requiere considerar inmediatamente la macro en el contexto de todas las opciones posibles para su uso, de lo contrario es muy fácil identificar un problema de la forma:

 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]); 

Por supuesto, para tales casos, se han inventado soluciones temporales y la macro se puede implementar de manera segura:

 #define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; }) 

La única pregunta es, ¿necesitamos todo esto en C ++? No, en C ++ hay plantillas y otras formas de construir código eficiente. Entonces, ¿por qué sigo encontrando macros similares en los programas de C ++?

Cuarto: la depuración es complicada


Existe la opinión de que la depuración es para los débiles :). Esto, por supuesto, es interesante para discutir, pero desde un punto de vista práctico, la depuración es útil y ayuda a encontrar errores. Las macros complican este proceso y definitivamente ralentizan la búsqueda de errores.

Quinto: falsos positivos de los analizadores estáticos.


Muchas macros, debido a las características específicas de su dispositivo, generan múltiples falsos positivos a partir de analizadores de código estático. Puedo decir con seguridad que la mayoría de los falsos positivos al verificar el código C y C ++ están asociados con macros.

El problema con las macros es que los analizadores simplemente no pueden distinguir el código complicado correcto del código erróneo. El artículo sobre la comprobación de Chromium describe una de estas macros.

Que hacer


¡No usemos macros en programas C ++ a menos que sea absolutamente necesario!

C ++ proporciona herramientas ricas como funciones de plantilla, inferencia de tipo automática (auto, decltype), funciones constexpr.

Casi siempre, en lugar de una macro, puede escribir una función ordinaria. A menudo esto no se hace debido a la pereza ordinaria. Esta pereza es perjudicial, y debemos luchar contra ella. Un pequeño tiempo adicional dedicado a escribir una función completa dará sus frutos con intereses. El código será más fácil de leer y mantener. La probabilidad de disparar su propia pierna disminuirá, y los compiladores y analizadores estáticos producirán menos falsos positivos.

Algunos podrían argumentar que el código con una función es menos eficiente. Esto también es solo una "excusa".

Los compiladores ahora alinean perfectamente el código, incluso si no ha escrito la palabra clave en línea .

Si estamos hablando de calcular expresiones en la etapa de compilación, entonces aquí las macros no son necesarias e incluso dañinas. Con el mismo propósito, es mucho mejor y más seguro usar constexpr .

Lo explicaré con un ejemplo. Aquí hay un error macro clásico que tomé prestado del código del kernel de FreeBSD.

 #define ICB2400_VPOPT_WRITE_SIZE 20 #define ICB2400_VPINFO_PORT_OFF(chan) \ (ICB2400_VPINFO_OFF + \ sizeof (isp_icb_2400_vpinfo_t) + \ (chan * ICB2400_VPOPT_WRITE_SIZE)) // <= static void isp_fibre_init_2400(ispsoftc_t *isp) { .... if (ISP_CAP_VP0(isp)) off += ICB2400_VPINFO_PORT_OFF(chan); else off += ICB2400_VPINFO_PORT_OFF(chan - 1); // <= .... } 

El argumento chan se usa en una macro sin envolver entre paréntesis. Como resultado, la expresión ICB2400_VPOPT_WRITE_SIZE no multiplica la expresión (chan - 1) , sino solo una.

El error no aparecería si se escribiera una función ordinaria en lugar de una macro.

 size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

Es muy probable que el compilador moderno de C y C ++ realice de forma independiente la alineación de la función, y el código será tan eficiente como en el caso de una macro.

Al mismo tiempo, el código se ha vuelto más legible, así como libre de errores.

Si se sabe que el valor de entrada es siempre una constante, puede agregar constexpr y asegurarse de que todos los cálculos se realizarán en la etapa de compilación. Imagine que es C ++ y que chan siempre es una constante. Entonces es útil declarar la función ICB2400_VPINFO_PORT_OFF de esta manera:

 constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

Beneficio!

Espero haber logrado convencerte. ¡Buena suerte y menos macros en el código!

Source: https://habr.com/ru/post/444612/


All Articles