Почему макросы не включены в большинство современных языков программирования?

31

Я знаю, что они реализованы крайне небезопасно в C / C ++. Разве они не могут быть реализованы более безопасным способом? Действительно ли недостатки макросов достаточно серьезны, чтобы перевесить огромную мощность, которую они предоставляют?

Casebash
источник
4
Какую силу обеспечивают макросы, чего не так-то просто достичь другим способом?
Чинмай Канчи
2
В C # потребовалось расширение основного языка, чтобы обернуть создание простого свойства и вспомогательного поля в одно объявление. И это занимает больше, чем лексические макросы: как создать функцию, соответствующую WithEventsмодификатору Visual Basic ? Вам нужно что-то вроде семантических макросов.
Джеффри Хантин
3
Проблема с макросами заключается в том, что, как и все мощные механизмы, он позволяет программисту нарушать предположения, которые делали или будут делать другие. Предположения являются ключом к рассуждению, и без возможности обоснованно рассуждать о логике, прогресс становится непомерным.
dan_waterworth
2
@Chinmay: макросы генерируют код. В языке Java нет такой возможности.
Кевин Клайн
1
@Chinmay Kanchi: Макросы позволяют выполнять (оценивать) код во время компиляции, а не во время выполнения.
Джорджио

Ответы:

57

Я думаю, что главная причина в том, что макросы лексические . Это имеет несколько последствий:

  • Компилятор не может проверить, является ли макрос семантически закрытым, то есть он представляет «единицу смысла», как функция. (Подумайте #define TWO 1+1- что TWO*TWOравно? 3.)

  • Макросы не печатаются как функции. Компилятор не может проверить, что параметры и возвращаемый тип имеют смысл. Он может проверять только расширенное выражение, использующее макрос.

  • Если код не компилируется, компилятор не может узнать, находится ли ошибка в самом макросе или в месте, где используется макрос. Компилятор либо сообщит о неправильном месте половину времени, либо он должен сообщить обоим, даже если один из них, вероятно, в порядке. (Подумайте #define min(x,y) (((x)<(y))?(x):(y)): Что следует делать , если компилятор типов xи yне совпадают или не реализовать operator<?)

  • Автоматизированные инструменты не могут работать с ними семантически полезными способами. В частности, вы не можете иметь такие вещи, как IntelliSense для макросов, которые работают как функции, но расширяются до выражения. (Опять minпример.)

  • Побочные эффекты макроса не такие явные, как у функций, что может привести к путанице у программиста. (Рассмотрим снова minпример: при вызове функции вы знаете, что выражение для xвычисляется только один раз, но здесь вы не можете узнать, не глядя на макрос.)

Как я уже сказал, это все последствия того, что макросы лексические. Когда вы пытаетесь превратить их во что-то более правильное, вы получаете функции и константы.

Timwi
источник
32
Не все макроязыки являются лексическими. Например, макросы Scheme являются синтаксическими, а шаблоны C ++ семантическими. Лексические макросистемы отличаются тем, что их можно привязать к любому языку, не зная его синтаксиса.
Джеффри Хантин
8
@ Джеффри: Я думаю, что мои «макросы лексические» - это действительно сокращение от «когда люди ссылаются на макросы в языке программирования, который они обычно думают о лексических макросах». К сожалению, Схема использовала этот загруженный термин для чего-то принципиально другого. Шаблоны C ++, однако, широко не называются макросами, предположительно именно потому, что они не являются полностью лексическими.
Тимви
25
Я думаю, что использование в Scheme термина макроса восходит к Lisp, что, вероятно, означает, что оно предшествует большинству других применений. IIRC система C / C ++ изначально называлась препроцессором .
Беван
16
@ Беван прав. Сказать, что макросы лексические, это все равно, что сказать, что птицы не могут летать, потому что птица, с которой вы наиболее знакомы, это пингвин. Тем не менее, большинство (но не все) пунктов, которые вы поднимаете, также применимы к синтаксическим макросам, хотя, возможно, в меньшей степени.
Лоуренс Гонсалвес
13

Но да, макросы могут быть разработаны и реализованы лучше, чем в C / C ++.

Проблема с макросами заключается в том, что они по сути являются механизмом расширения синтаксиса языка , который переписывает ваш код во что-то другое.

  • В случае C / C ++ нет никакой фундаментальной проверки работоспособности. Если вы осторожны, все в порядке. Если вы допустили ошибку или злоупотребили макросами, вы можете столкнуться с большими проблемами.

    Добавьте к этому, что многие простые вещи, которые вы можете делать с макросами (в стиле C / C ++), можно выполнять другими способами на других языках.

  • В других языках, таких как различные диалекты Лисп, макросы лучше интегрированы с синтаксисом основного языка, но вы все равно можете столкнуться с проблемами при объявлении в макросе «утечка». Об этом говорят гигиенические макросы .


Краткий исторический контекст

Макросы (сокращение от макро-инструкций) впервые появились в контексте ассемблера. Согласно Википедии , макросы были доступны в некоторых ассемблерах IBM в 1950-х годах.

В оригинальном LISP не было макросов, но они были впервые представлены в MacLisp в середине 1960-х годов: https://stackoverflow.com/questions/3065606/when-did-the-idea-of-macros-user-defined-code -преобразование-появиться . http://www.csee.umbc.edu/courses/331/resources/papers/Evolution-of-Lisp.pdf . До этого fexprs предоставлял макроподобную функциональность.

Самые ранние версии C не имели макросов ( http://cm.bell-labs.com/cm/cs/who/dmr/chist.html ). Они были добавлены около 1972-73 через препроцессор. До этого C поддерживал только #includeи #define.

Макропроцессор M4 возник примерно в 1977 году.

Более поздние языки, по-видимому, реализуют макросы, в которых модель работы скорее синтаксическая, чем текстовая.

Поэтому, когда кто-то говорит о первичности конкретного определения термина «макро», важно отметить, что значение эволюционировало со временем.

Стивен С
источник
9
C ++ благословлен (?) ДВУМИ макросистемами: препроцессором и расширением шаблона.
Джеффри Хантин
Я думаю, утверждение, что расширение шаблона - это макрос, растягивает определение макроса. Использование вашего определения делает исходный вопрос бессмысленным и просто неправильным, поскольку в большинстве современных языков есть макросы (в соответствии с вашим определением). Однако использование макроса в качестве подавляющего большинства разработчиков делает его хорошим вопросом.
Данк
@Dunk, определение макроса, как в Лиспе, предшествует «современному» тупому пониманию, как в патетическом препроцессоре Си.
SK-logic
@SK: Лучше быть «технически» правильным и запутывать разговор, или это лучше понять?
Данк
1
@ Данк, терминология не принадлежит невежественным ордам. Их невежество никогда не следует принимать во внимание, в противном случае это приведет к затуханию всей области компьютерных наук. Если кто-то понимает «макрос» только как ссылку на препроцессор C, это не что иное, как невежество. Я сомневаюсь, что есть значительная доля людей, которые никогда не слышали о Лиспе или даже о, "pardonnez mon français", "макросах" VBA в Office.
SK-logic
12

Как отмечает Скотт , макросы могут скрывать логику. Конечно, так же как и функции, классы, библиотеки и многие другие распространенные устройства.

Но мощная система макросов может пойти дальше, позволяя вам разрабатывать и использовать синтаксис и структуры, обычно не встречающиеся в языке. Это действительно может быть замечательным инструментом: специфичные для предметной области языки, генераторы кода и многое другое, и все это в комфортной среде одного языка ...

Однако этим можно злоупотреблять. Это может затруднить чтение, понимание и отладку кода, увеличить время, необходимое новым программистам для ознакомления с базой кода, и привести к дорогостоящим ошибкам и задержкам.

Таким образом, для языков, предназначенных для упрощения программирования (таких как Java или Python), такая система является анафемой.

Shog9
источник
3
А затем Java сошел с рельсов, добавив foreach, аннотации, утверждения, обобщения-стирания, ...
Джеффри Хантин
2
@ Джеффри: и давайте не будем забывать, множество сторонних генераторов кода. Если подумать, давайте забудем о них.
Shog9
4
Еще один пример того, как Java была упрощенной, а не простой.
Джеффри Хантин
3
@JeffreyHantin: что не так с foreach?
Casebash
1
@Dunk Эта аналогия может быть натянутой ... но я думаю, что расширение языка похоже на плутоний. Дорогой в реализации, очень сложный для обработки желаемых форм, и удивительно опасный при неправильном использовании, но также удивительно мощный при правильном применении.
Джеффри Хантин
6

Макрос может быть реализован очень безопасно при некоторых обстоятельствах - например, в Лиспе макросы - это просто функции, которые возвращают преобразованный код в виде структуры данных (s-выражение). Конечно, Lisp значительно выигрывает от того, что он гомоичен, и от того, что «код - это данные».

Примером того, насколько простыми могут быть макросы, является пример Clojure, который задает значение по умолчанию, которое будет использоваться в случае исключения:

(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

Тем не менее, даже в Лиспе общий совет: «Не используйте макросы, если это не нужно».

Если вы не используете гомоиконический язык, то макросы становятся намного сложнее, а различные другие варианты имеют некоторые подводные камни:

  • Текстовые макросы - например, препроцессор C - простой в реализации, но очень сложный в использовании, так как вам нужно генерировать правильный исходный синтаксис в текстовой форме, включая любые синтаксические особенности
  • DSLS на основе макросов - например, система шаблонов C ++. Сложный, сам по себе может привести к некоторому хитрому синтаксису, может быть чрезвычайно сложным для правильной обработки компилятором и создателем инструментов, поскольку он вносит существенную новую сложность в синтаксис и семантику языка.
  • AST / API манипулирования байт-кодом - например, Java-отражение / генерация байт-кода - теоретически очень гибки, но могут стать очень многословными: для выполнения довольно простых задач может потребоваться много кода. Если для создания эквивалента трехстрочной функции требуется десять строк кода, значит, вы не многого добились в своих усилиях по метапрограммированию ...

Кроме того, все, что может сделать макрос, в конечном итоге может быть достигнуто каким-либо другим способом на языке полного набора (даже если это означает написание большого количества шаблонов). В результате всей этой хитрости неудивительно, что многие языки решают, что макросы действительно не стоят всех усилий, чтобы их реализовать.

mikera
источник
Тьфу. Не более "код это данные, это хорошая вещь" мусор. Код - это код, данные - это данные, и неспособность правильно разделить их - это дыра в безопасности. Можно подумать, что появление SQL-инъекции как одного из самых больших существующих классов уязвимостей привело бы к дискредитации идеи сделать код и данные легко взаимозаменяемыми раз и навсегда.
Мейсон Уилер
10
@ Мейсон - я не думаю, что вы понимаете концепцию кода-данных. Весь исходный код программы на C также является данными - он просто выражается в текстовом формате. Лиспы одинаковы, за исключением того, что они выражают код в практических промежуточных структурах данных (s-выражениях), которые позволяют ему манипулировать и преобразовывать макросы перед компиляцией. В обоих случаях отправка ненадежных входных данных компилятору может быть дырой в безопасности - но это трудно сделать случайно, и вы будете виноваты в том, что сделали что-то глупое, а не компилятор.
Микера
Rust - еще одна интересная безопасная макросистема. Он имеет строгие правила / семантику, чтобы избежать проблем, связанных с лексическими макросами C / C ++, и использует DSL для захвата частей входного выражения в макропеременные, которые будут позже вставлены. Это не так мощно, как способность Лиспса вызывать любую функцию на входе, но она предоставляет большой набор полезных макросов манипуляции, которые можно вызывать из других макросов.
zstewart
Тьфу. Не больше мусора безопасности . В противном случае обвините каждую систему со встроенной архитектурой фон Неймана, например, каждый процессор IA-32 с возможностью самоизменения кода. Я подозреваю, что вы можете заполнить эту дыру физически ... В любом случае, вы должны столкнуться с фактом в целом: изоморфизм между кодом и данными является природой мира в нескольких отношениях . И это (вероятно) вы, программист, обязаны поддерживать неизменность требований безопасности, что не то же самое, что искусственное применение преждевременной сегрегации в любом месте.
FrankHB
4

Чтобы ответить на ваши вопросы, подумайте, для чего преимущественно используются макросы (Предупреждение: скомпилированный мозг).

  • Макросы, используемые для определения символических констант #define X 100

Это можно легко заменить на: const int X = 100;

  • Макросы, используемые для определения (по существу) встроенных функций, не зависящих от типа #define max(X,Y) (X>Y?X:Y)

В любом языке, который поддерживает перегрузку функций, это можно эмулировать гораздо более безопасным для типов способом, перегружая функции правильного типа, или, в языке, который поддерживает обобщенные значения, обобщенной функцией. Макрос с радостью попытается сравнить что угодно, включая указатели или строки, которые могут скомпилироваться, но это почти не то, что вы хотели. С другой стороны, если вы сделали макросы безопасными по типу, они не дадут никаких преимуществ или удобства по сравнению с перегруженными функциями.

  • Макросы используются для указания ярлыков для часто используемых элементов. #define p printf

Это легко заменить функцией, p()которая делает то же самое. Это довольно сложно в C (требует использования va_arg()семейства функций), но во многих других языках, которые поддерживают переменное число аргументов функций, это намного проще.

Поддержка этих функций в языке, а не в специальном макроязыке, проще, менее подвержена ошибкам и намного меньше сбивает с толку других, читающих код. На самом деле, я не могу придумать ни одного варианта использования макросов, который нельзя легко скопировать другим способом. Только место , где макросы действительно полезны, когда они привязаны к условной компиляции конструкций , таких как #if( и т.д.).

По этому поводу я не буду спорить с вами, поскольку считаю, что непроцессорные решения условной компиляции в популярных языках чрезвычайно громоздки (например, внедрение байт-кода в Java). Но такие языки, как D, предлагают решения, которые не требуют препроцессора и являются не более громоздкими, чем использование условных выражений препроцессора, но при этом гораздо менее подвержены ошибкам.

Чинмай Канчи
источник
1
если вам нужно #define max, пожалуйста, заключите в скобки параметры, чтобы не было неожиданных эффектов от приоритета оператора ... как #define max (X, Y) ((X)> (Y)? (X) :( Y))
foo
Вы понимаете, что это был просто пример ... Цель состояла в том, чтобы проиллюстрировать.
Чинмай Канчи
+1 за систематическое рассмотрение вопроса. Я хотел бы добавить, что условная компиляция может легко стать кошмаром - я помню, что была программа с именем "unifdef" (??), целью которой было сделать ее видимой, что осталось после постобработки.
Инго
7
In fact, I can't think of a single use-case for macros that can't easily be duplicated in another way: по крайней мере, в C вы не можете формировать идентификаторы (имена переменных), используя конкатенацию токенов, не используя макросы.
Чарльз Сальвия,
Макросы позволяют гарантировать, что определенные структуры кода и данных остаются «параллельными». Например, если имеется небольшое количество условий со связанными сообщениями, и нужно будет сохранить их в кратком формате, попытка использовать enumдля определения условий и массив строк с константой для определения сообщений может привести к проблемам, если enum и массив вышли из синхронизации. Использование макроса для определения всех пар (enum, string) и последующее включение этого макроса дважды с другими правильными определениями в области видимости позволяет каждый раз помещать каждое значение перечисления рядом со своей строкой.
суперкат
2

Самая большая проблема, с которой я столкнулся при работе с макросами, заключается в том, что при интенсивном использовании они могут сделать код очень трудным для чтения и обслуживания, поскольку они позволяют скрыть логику в макросе, которую может или не может быть легко найти (и может быть, а может и нет) ).

Скотт Дорман
источник
Разве макрос не должен быть задокументирован?
Casebash
@Casebash: Конечно, макрос должен быть задокументирован, как и любой другой исходный код ... но на практике я редко видел, чтобы это было сделано.
Скотт Дорман
3
Всегда отладочная документация? Этого просто недостаточно при работе с плохо написанным кодом.
JeffO
У ООП тоже есть некоторые из этих проблем ...
aoeu256
2

Начнем с того, что отметим, что MACRO в C / C ++ очень ограничены, подвержены ошибкам и не очень полезны.

MACRO, реализованные в, скажем, LISP или языке ассемблера z / OS, могут быть надежными и невероятно полезными.

Но из-за злоупотребления ограниченным функционалом в C они получили плохую репутацию. Так что никто больше не реализует макросы, вместо этого вы получаете такие вещи, как шаблоны, которые делают некоторые простые вещи, используемые макросами, и такие вещи, как аннотации Java, которые выполняют некоторые более сложные вещи, используемые макросами.

Джеймс Андерсон
источник