¿Por qué no se incluyen macros en la mayoría de los lenguajes de programación modernos?

31

Sé que se implementan de manera extremadamente insegura en C / C ++. ¿No se pueden implementar de una manera más segura? ¿Son las desventajas de las macros realmente lo suficientemente malas como para superar la potencia masiva que proporcionan?

Casebash
fuente
44
¿Exactamente qué potencia proporcionan las macros que no pueden lograrse fácilmente de otra manera?
Chinmay Kanchi
2
En C #, se necesitó una extensión de lenguaje central para envolver la creación de una propiedad simple y un campo de respaldo en una declaración. Y esto requiere más que macros léxicas: ¿cómo puede crear una función correspondiente al WithEventsmodificador de Visual Basic ? Necesitarías algo como macros semánticas.
Jeffrey Hantin
3
El problema con las macros es que, como todos los mecanismos poderosos, permite que un programador rompa las suposiciones que otros han hecho o harán. Los supuestos son la clave del razonamiento y, sin la capacidad de razonar de manera factible sobre la lógica, el progreso se vuelve prohibitivo.
dan_waterworth
2
@Chinmay: las macros generan código. No hay ninguna característica en el lenguaje Java con ese poder.
Kevin Cline
1
@Chinmay Kanchi: las macros permiten ejecutar (evaluar) el código en tiempo de compilación en lugar de en tiempo de ejecución.
Giorgio

Respuestas:

57

Creo que la razón principal es que las macros son léxicas . Esto tiene varias consecuencias:

  • El compilador no tiene forma de verificar que una macro está semánticamente cerrada, es decir, que representa una "unidad de significado" como lo hace una función. (Considere #define TWO 1+1: ¿qué significa TWO*TWOigual? 3.)

  • Las macros no se escriben como las funciones. El compilador no puede verificar que los parámetros y el tipo de retorno tengan sentido. Solo puede verificar la expresión expandida que usa la macro.

  • Si el código no se compila, el compilador no tiene forma de saber si el error está en la macro o en el lugar donde se usa la macro. El compilador informará el lugar equivocado la mitad del tiempo, o tendrá que informar ambos, aunque uno de ellos probablemente esté bien. (Considere #define min(x,y) (((x)<(y))?(x):(y)): ¿Qué debe hacer el compilador si los tipos de xy yno coinciden o no se implementan operator<?)

  • Las herramientas automatizadas no pueden trabajar con ellas de formas semánticamente útiles. En particular, no puede tener cosas como IntelliSense para macros que funcionan como funciones pero se expanden a una expresión. (De nuevo, el minejemplo).

  • Los efectos secundarios de una macro no son tan explícitos como lo son con las funciones, causando una posible confusión para el programador. (Considere nuevamente el minejemplo: en una llamada de función, sabe que la expresión para xse evalúa solo una vez, pero aquí no se puede saber sin mirar la macro).

Como dije, todas estas son consecuencias del hecho de que las macros son léxicas. Cuando intentas convertirlos en algo más apropiado, terminas con funciones y constantes.

Timwi
fuente
32
No todos los lenguajes macro son léxicos. Por ejemplo, las macros de Scheme son sintácticas y las plantillas de C ++ son semánticas. Los macro sistemas léxicos tienen la distinción de que pueden agregarse a cualquier idioma sin ser conscientes de su sintaxis.
Jeffrey Hantin
8
@Jeffrey: Creo que mis "macros son léxicas" es realmente una abreviatura de "cuando las personas se refieren a las macros en un lenguaje de programación, generalmente piensan en macros léxicas". Es lamentable que Scheme use este término cargado para algo que es fundamentalmente diferente. Sin embargo, las plantillas de C ++ no se conocen ampliamente como macros, presumiblemente precisamente porque no son completamente léxicas.
Timwi
25
Creo que el uso de Scheme del término macro se remonta a Lisp, lo que probablemente significa que es anterior a la mayoría de los otros usos. IIRC, el sistema C / C ++ originalmente se llamaba preprocesador .
Bevan
16
@Bevan tiene razón. Decir que las macros son léxicas es como decir que los pájaros no pueden volar porque el pájaro con el que estás más familiarizado es un pingüino. Dicho esto, la mayoría (pero no todos) de los puntos que plantea también se aplican a las macros sintácticas, aunque quizás en menor grado.
Laurence Gonsalves
13

Pero sí, las macros se pueden diseñar e implementar mejor que en C / C ++.

El problema con las macros es que son efectivamente un mecanismo de extensión de sintaxis de lenguaje que reescribe su código en otra cosa.

  • En el caso de C / C ++, no existe una comprobación de cordura fundamental. Si tienes cuidado, las cosas están bien. Si comete un error, o si usa en exceso las macros, puede tener grandes problemas.

    Agregue a esto que muchas cosas simples que puede hacer con macros (estilo C / C ++) se pueden hacer de otras maneras en otros idiomas.

  • En otros idiomas, como varios dialectos de Lisp, las macros están mejor integradas con la sintaxis del lenguaje principal, pero aún puede tener problemas con las declaraciones en una macro "fuga". Esto se aborda mediante macros higiénicas .


Breve contexto histórico

Las macros (abreviatura de macroinstrucciones) aparecieron por primera vez en el contexto del lenguaje ensamblador. Según Wikipedia , las macros estaban disponibles en algunos ensambladores de IBM en la década de 1950.

El LISP original no tenía macros, pero se introdujeron por primera vez en MacLisp a mediados de la década de 1960: https://stackoverflow.com/questions/3065606/when-did-the-idea-of-macros-user-defined-code -transformación-aparecen . http://www.csee.umbc.edu/courses/331/resources/papers/Evolution-of-Lisp.pdf . Antes de eso, "fexprs" proporcionaba una funcionalidad tipo macro.

Las primeras versiones de C no tenían macros ( http://cm.bell-labs.com/cm/cs/who/dmr/chist.html ). Estos se agregaron alrededor de 1972-73 a través de un preprocesador. Antes de eso, C solo soportaba #includey #define.

El macroprocesador M4 se originó en circa 1977.

Los lenguajes más recientes aparentemente implementan macros donde el modelo de operación es sintáctico en lugar de textual.

Entonces, cuando alguien habla de la primacía de una definición particular del término "macro", es importante tener en cuenta que el significado ha evolucionado con el tiempo.

Stephen C
fuente
99
C ++ está bendecido (?) Con DOS macro sistemas: el preprocesador y la expansión de la plantilla.
Jeffrey Hantin
Creo que afirmar que la expansión de plantilla es una macro está estirando la definición de macro. El uso de su definición hace que la pregunta original no tenga sentido y simplemente esté equivocada, ya que la mayoría de los lenguajes modernos tienen macros (de acuerdo con su definición). Sin embargo, usar macro como lo usaría la abrumadora mayoría de los desarrolladores lo hace una buena pregunta.
Dunk
@Dunk, la definición de una macro como en Lisp es anterior a la comprensión tonta "moderna" como en el preprocesador patético de C.
SK-logic
@SK: ¿Es mejor ser "técnicamente" correcto y ofuscar la conversación o es mejor ser entendido?
Dunk
1
@Dunk, la terminología no es propiedad de las hordas ignorantes. Su ignorancia nunca debe tenerse en cuenta, de lo contrario conducirá a entorpecer todo el dominio de la informática. Si alguien entiende "macro" solo como una referencia a un preprocesador C, no es más que una ignorancia. Dudo que haya una proporción significativa de personas que nunca han oído hablar de Lisp o, incluso, de "macros" de VBA en Office.
SK-logic
12

Las macros pueden, como señala Scott , permitirle ocultar la lógica. Por supuesto, también lo hacen funciones, clases, bibliotecas y muchos otros dispositivos comunes.

Pero un poderoso sistema macro puede ir más allá, permitiéndole diseñar y utilizar sintaxis y estructuras que normalmente no se encuentran en el lenguaje. De hecho, esta puede ser una herramienta maravillosa: lenguajes específicos de dominio, generadores de código y más, todo dentro de la comodidad de un entorno de lenguaje único ...

Sin embargo, puede ser abusado. Puede hacer que el código sea más difícil de leer, comprender y depurar, aumentar el tiempo necesario para que los nuevos programadores se familiaricen con una base de código y generar errores y demoras costosas.

Entonces, para los lenguajes destinados a simplificar la programación (como Java o Python), dicho sistema es un anatema.

Shog9
fuente
3
Y luego Java se salió del camino agregando foreach, anotaciones, afirmaciones, genéricos por borrado, ...
Jeffrey Hantin
2
@ Jeffrey: y no olvidemos, muchos generadores de código de terceros . Pensándolo bien, olvidemos esos.
Shog9
44
Otro ejemplo de cómo Java era simplista en lugar de simple.
Jeffrey Hantin
3
@JeffreyHantin: ¿Qué hay de malo en foreach?
Casebash el
1
@Dunk Esta analogía puede ser exagerada ... pero creo que la extensibilidad del lenguaje es algo así como el plutonio. Costoso de implementar, muy difícil de mecanizar en las formas deseadas y notablemente peligroso si se usa incorrectamente, pero también increíblemente poderoso cuando se aplica bien.
Jeffrey Hantin
6

Las macros se pueden implementar de manera muy segura en algunas circunstancias: en Lisp, por ejemplo, las macros son solo funciones que devuelven código transformado como una estructura de datos (expresión-s). Por supuesto, Lisp se beneficia significativamente del hecho de que es homoicónico y del hecho de que "el código es información".

Un ejemplo de lo fácil que pueden ser las macros es este ejemplo de Clojure que especifica un valor predeterminado que se utilizará en el caso de una excepción:

(defmacro on-error [default-value code]
  `(try ~code (catch Exception ~'e ~default-value)))

(on-error 0 (+ nil nil))               ;; would normally throw NullPointerException
=> 0                                   ;l; but we get the default value

Sin embargo, incluso en Lisps, el consejo general es "no use macros a menos que sea necesario".

Si no está utilizando un lenguaje homoicónico, las macros se vuelven mucho más complicadas y las otras opciones tienen algunas dificultades:

  • Macros basadas en texto, por ejemplo, el preprocesador de C, fáciles de implementar pero muy difíciles de usar correctamente, ya que necesita generar la sintaxis de origen correcta en forma de texto, incluidas las peculiaridades sintácticas
  • DSLS basados ​​en macros , por ejemplo, el sistema de plantillas C ++. El complejo, que puede resultar en una sintaxis complicada, puede ser extremadamente complejo para que los compiladores y los escritores de herramientas lo manejen correctamente, ya que introduce una nueva complejidad significativa en la sintaxis y la semántica del lenguaje.
  • API de manipulación de AST / bytecode , por ejemplo, reflexión de Java / generación de bytecode, en teoría muy flexible, pero puede ser muy detallado: puede requerir mucho código para hacer cosas bastante simples. Si se necesitan diez líneas de código para generar el equivalente de una función de tres líneas, entonces no ha ganado mucho con sus esfuerzos de metaprogramación ...

Además, todo lo que una macro puede hacer se puede lograr de alguna otra manera en un lenguaje completo (incluso si esto significa escribir muchas repeticiones). Como resultado de todo este engaño, no es sorprendente que muchos lenguajes decidan que las macros realmente no valen la pena.

mikera
fuente
Ugh No más basura "el código es información es algo bueno". El código es código, los datos son datos y no segregarlos adecuadamente es un agujero de seguridad. Se podría pensar que la aparición de la inyección de SQL como una de las clases de vulnerabilidad más grandes que existen habría desacreditado la idea de hacer que el código y los datos sean fácilmente intercambiables de una vez por todas.
Mason Wheeler
10
@Mason - No creo que entiendas el concepto de código es datos. Todo el código fuente del programa C también son datos, simplemente se expresan en formato de texto. Los Lisps son iguales, excepto que expresan el código en estructuras prácticas de datos intermedios (expresiones s) que permiten que las macros lo manipulen y transformen antes de la compilación. En ambos casos, enviar entradas no confiables al compilador podría ser un agujero de seguridad, pero eso es difícil de hacer accidentalmente y sería su culpa por hacer algo estúpido, no el compilador.
mikera
El óxido es otro interesante sistema macro seguro. Tiene reglas / semánticas estrictas para evitar los tipos de problemas asociados con las macros léxicas C / C ++, y utiliza un DSL para capturar partes de la expresión de entrada en variables macro para insertarlas más tarde. No es tan poderoso como la capacidad de Lisps para llamar a cualquier función en la entrada, pero proporciona un gran conjunto de macros de manipulación útiles que se pueden llamar desde otras macros.
zstewart
Ugh No más basura de seguridad . De lo contrario, culpe a todos los sistemas con la arquitectura von Neumann incorporada, por ejemplo, todos los procesadores IA-32 con capacidad de código auto modificable. Sospecho que puedes llenar el agujero físicamente ... De todos modos, tienes que enfrentar el hecho en general: el isomorfismo entre el código y los datos es la naturaleza del mundo de varias maneras . Y es (probablemente) usted, el programador, el que tiene el deber de mantener la invariabilidad de los requisitos de seguridad, que no es lo mismo que aplicar artificialmente la segregación prematura en cualquier lugar.
FrankHB
4

Para responder a sus preguntas, piense para qué macros se usan predominantemente (Advertencia: código compilado por el cerebro).

  • Macros utilizadas para definir constantes simbólicas #define X 100

Esto se puede reemplazar fácilmente con: const int X = 100;

  • Las macros solían definir (esencialmente) funciones independientes de tipo en línea #define max(X,Y) (X>Y?X:Y)

En cualquier lenguaje que admita la sobrecarga de funciones, esto se puede emular de una manera mucho más segura mediante la sobrecarga de funciones del tipo correcto o, en un lenguaje que admita genéricos, mediante una función genérica. La macro felizmente intentará comparar cualquier cosa, incluidos punteros o cadenas, que podrían compilarse, pero es casi seguro que no es lo que quería. Por otro lado, si hizo que las macros sean seguras, no ofrecen beneficios ni conveniencia sobre las funciones sobrecargadas.

  • Las macros solían especificar accesos directos a elementos de uso frecuente. #define p printf

Esto se reemplaza fácilmente por una función p()que hace lo mismo. Esto está bastante involucrado en C (que requiere que use la va_arg()familia de funciones), pero en muchos otros lenguajes que admiten números variables de argumentos de función, es mucho más simple.

El soporte de estas características dentro de un lenguaje en lugar de en un lenguaje macro especial es más simple, menos propenso a errores y mucho menos confuso para otros que leen el código. De hecho, no puedo pensar en un solo caso de uso para macros que no se pueda duplicar fácilmente de otra manera. El único lugar donde las macros son realmente útiles es cuando están vinculadas a construcciones de compilación condicional como #if(etc.).

En ese punto, no discutiré con usted, ya que creo que las soluciones no preprocesadoras para la compilación condicional en lenguajes populares son extremadamente engorrosas (como la inyección de código de bytes en Java). Pero lenguajes como D han creado soluciones que no requieren un preprocesador y no son más engorrosos que el uso de condicionales de preprocesador, aunque son mucho menos propensos a errores.

Chinmay Kanchi
fuente
1
si tiene que #definir max, ponga corchetes alrededor de los parámetros, para que no haya efectos inesperados de la precedencia del operador ... como #define max (X, Y) ((X)> (Y)? (X) :( Y))
foo
Te das cuenta de que era simplemente un ejemplo ... La intención era ilustrar.
Chinmay Kanchi
+1 para este tratamiento sistemático del asunto. Me gusta agregar que la compilación condicional puede convertirse fácilmente en una pesadilla. Recuerdo que había un programa llamado "unifdef" (??) cuyo propósito era hacer visible lo que quedaba después del procesamiento posterior.
Ingo
77
In fact, I can't think of a single use-case for macros that can't easily be duplicated in another way: en C al menos, no puede formar identificadores (nombres de variables) usando la concatenación de tokens sin usar macros.
Charles Salvia
Las macros permiten garantizar que ciertas estructuras de código y datos permanezcan "paralelas". Por ejemplo, si hay una pequeña cantidad de condiciones con mensajes asociados, y uno necesitará persistirlas en un formato conciso, intentar usar un enumpara definir las condiciones y un conjunto de cadenas constante para definir los mensajes podría generar problemas si el enum y la matriz se desincronizan. El uso de una macro para definir todos los pares (enum, string) y luego incluir esa macro dos veces con otras definiciones apropiadas en el alcance cada vez permitiría colocar cada valor de enum junto a su cadena.
supercat
2

El mayor problema que he visto con las macros es que, cuando se usan mucho, pueden hacer que el código sea muy difícil de leer y mantener, ya que le permiten ocultar la lógica en la macro que puede o no ser fácil de encontrar (y puede o no ser trivial )

Scott Dorman
fuente
¿No debería documentarse la macro?
Casebash
@Casebash: Por supuesto, la macro debe documentarse como cualquier otro código fuente ... pero en la práctica rara vez lo he visto hecho.
Scott Dorman
3
¿Alguna vez depuró la documentación? Simplemente no es suficiente cuando se trata de código mal escrito.
JeffO
OOP también tiene algunos de esos problemas ...
aoeu256
2

Para empezar, tenga en cuenta que los MACRO en C / C ++ son muy limitados, propensos a errores y no son realmente tan útiles.

Los MACRO implementados en, por ejemplo, LISP, o el lenguaje ensamblador de z / OS pueden ser confiables e increíblemente útiles.

Pero debido al abuso de la funcionalidad limitada en C, tienen una mala reputación. Entonces, ya nadie implementa macros, en su lugar obtienes cosas como Plantillas que hacen algunas de las cosas simples que solían hacer las macros y cosas como las anotaciones de Java que hacen algunas de las cosas más complejas que solían hacer las macros.

James Anderson
fuente