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


31

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


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вычисляется только один раз, но здесь вы не можете узнать, не глядя на макрос.)

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


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), такая система является анафемой.


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-отражение / генерация байт-кода - теоретически очень гибки, но могут стать очень многословными: для выполнения довольно простых задач может потребоваться много кода. Если для создания эквивалента трехстрочной функции требуется десять строк кода, значит, вы не многого добились в своих усилиях по метапрограммированию ...

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


Тьфу. Не более "код это данные, это хорошая вещь" мусор. Код - это код, данные - это данные, и неспособность правильно разделить их - это дыра в безопасности. Можно подумать, что появление 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, которые выполняют некоторые более сложные вещи, используемые макросами.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.