Почему макросы препроцессора - зло и каковы альтернативы?

94

Я всегда спрашивал об этом, но никогда не получал действительно хорошего ответа; Я думаю, что почти любой программист, даже до написания первого «Hello World», сталкивался с такими фразами, как «макрос никогда не должен использоваться», «макросы - это зло» и так далее. У меня вопрос: почему? Есть ли реальная альтернатива новому C ++ 11 по прошествии стольких лет?

Легкая часть связана с макросами, такими как #pragma, которые зависят от платформы и компилятора, и в большинстве случаев они имеют серьезные недостатки, такие как #pragma onceподверженность ошибкам как минимум в двух важных ситуациях: одно и то же имя в разных путях и с некоторыми сетевыми настройками и файловыми системами.

А вообще, как насчет макросов и альтернатив их использованию?

user1849534
источник
19
#pragmaэто не макрос.
FooF
1
Директива препроцессора @foof?
user1849534
6
@ user1849534: Да вот в чем дело ... и советы по поводу макросов не о чем #pragma.
Бен Фойгт
1
Вы можете сделать много с constexpr, inlineфункциями, и templates, но boost.preprocessorи chaosпоказать , что макросы имеют свое место. Не говоря уже о макросах конфигурации для разных компиляторов, платформ и т. Д.
Брэндон
1
возможный дубликат Когда макросы C ++ полезны?
Аарон МакДэйд

Ответы:

165

Макросы такие же, как и любой другой инструмент: молоток, использованный при убийстве, не является злом, потому что это молоток. Это зло в том, как человек использует это таким образом. Если вы хотите забить гвозди, молоток - идеальный инструмент.

Есть несколько аспектов макросов, которые делают их «плохими» (я подробно остановлюсь на каждом позже и предложу альтернативы):

  1. Вы не можете отлаживать макросы.
  2. Расширение макросов может привести к странным побочным эффектам.
  3. Макросы не имеют «пространства имен», поэтому, если у вас есть макрос, который конфликтует с именем, используемым в другом месте, вы получаете замену макроса там, где вы этого не хотели, и это обычно приводит к странным сообщениям об ошибках.
  4. Макросы могут повлиять на то, чего вы не понимаете.

Итак, давайте здесь немного расширимся:

1) Макросы нельзя отлаживать. Когда у вас есть макрос, который переводится в число или строку, исходный код будет иметь имя макроса, и многие отладчики не могут «увидеть», во что переводится макрос. Так что вы на самом деле не знаете, что происходит.

Замена : используйте enumилиconst T

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

Замена : используйте встроенные функции, если они должны быть "быстрыми" (но помните, что слишком много встроенных - это нехорошо)

2) Расширения макросов могут иметь странные побочные эффекты.

Знаменитый есть #define SQUARE(x) ((x) * (x))и польза x2 = SQUARE(x++). Это приводит к тому x2 = (x++) * (x++);, что, даже если бы это был действительный код [1], почти наверняка не было бы того, чего хотел программист. Если бы это была функция, было бы нормально выполнить x ++, и x увеличивался бы только один раз.

Другой пример - "if else" в макросе, допустим, у нас есть это:

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

а потом

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

На самом деле это становится совершенно неправильным ...

Замена : реальные функции.

3) Макросы не имеют пространства имен

Если у нас есть макрос:

#define begin() x = 0

и у нас есть код на C ++, который использует begin:

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

Итак, какое сообщение об ошибке вы думаете, что получаете, и где искать ошибку [при условии, что вы полностью забыли - или даже не знали - макрос начала, который находится в каком-то файле заголовка, который написал кто-то другой? [и даже веселее, если вы включите этот макрос перед включением - вы утонете в странных ошибках, которые не имеют абсолютно никакого смысла, когда вы смотрите на сам код.

Замена : Что ж, здесь не так много замены, как «правило» - используйте только прописные имена для макросов и никогда не используйте все заглавные имена для других вещей.

4) Макросы имеют эффекты, о которых вы не подозреваете

Возьмите эту функцию:

#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ... 
void dostuff()
{
    int x = 7;

    begin();

    ... more code using x ... 

    printf("x=%d\n", x);

    end();

}

Теперь, не глядя на макрос, можно подумать, что begin - это функция, которая не должна влиять на x.

Подобные вещи, а я видел гораздо более сложные примеры, могут ДЕЙСТВИТЕЛЬНО испортить ваш день!

Замена : либо не используйте макрос для установки x, либо передайте x в качестве аргумента.

Бывают случаи, когда использование макросов определенно полезно. Один из примеров - обернуть функцию макросами для передачи информации о файле / строке:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

Теперь мы можем использовать my_debug_mallocв коде как обычный malloc, но у него есть дополнительные аргументы, поэтому, когда дело доходит до конца и мы просканируем «какие элементы памяти не были освобождены», мы можем вывести, где было выполнено выделение, чтобы программист может отследить утечку.

[1] Обновление одной переменной более одного раза «в точке последовательности» является неопределенным поведением. Точка последовательности - это не совсем то же самое, что оператор, но для большинства намерений и целей мы должны рассматривать это как. Таким образом, x++ * x++будет выполнено обновление xдважды, что не определено и, вероятно, приведет к разным значениям в разных системах, а также к разному значению результата x.

Матс Петерссон
источник
6
Эти if elseпроблемы могут быть решены путем обертывания макро внутри тела do { ... } while(0). Это ведет себя , как и следовало ожидать , по ifи forи другим вопросам , потенциально рискованные потока управления. Но да, настоящая функция обычно является лучшим решением. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
Аарон МакДэйд
11
@AaronMcDaid: Да, есть обходные пути, которые решают некоторые проблемы, обнаруженные в этих макросах. Вся суть моего поста заключалась не в том, чтобы показать, как хорошо делать макросы, а в том, «как легко сделать макросы неправильно», когда есть хорошая альтернатива. Тем не менее, есть вещи, которые макросы решают очень легко, и бывают случаи, когда макросы тоже являются правильным решением.
Матс Петерссон
1
В пункте 3 ошибки больше не являются проблемой. Современные компиляторы, такие как Clang, скажут что-то вроде note: expanded from macro 'begin'и покажут, где beginэто определено.
kirbyfan64sos 05
5
Макросы сложно перевести на другие языки.
Марко ван де Вурт,
1
@FrancescoDondi: stackoverflow.com/questions/4176328/… (в этом ответе довольно мало, он говорит о i ++ * i ++ и т. Д.
Матс Петерссон,
21

Поговорка «макросы - это зло» обычно относится к использованию #define, а не #pragma.

В частности, выражение относится к этим двум случаям:

  • определение магических чисел как макросов

  • использование макросов для замены выражений

с новым C ++ 11 есть реальная альтернатива после стольких лет?

Да, для элементов в списке выше (магические числа должны быть определены с помощью const / constexpr, а выражения должны быть определены с помощью функций [normal / inline / template / inline template].

Вот некоторые из проблем, возникающих из-за определения магических чисел как макросов и замены выражений макросами (вместо определения функций для вычисления этих выражений):

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

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

Рассмотрим этот код:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);

Вы ожидаете, что a и c будут равны 6 после присвоения c (как и при использовании std :: max вместо макроса). Вместо этого код выполняет:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

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

Это означает, что если вы определите макрос выше (для max), вы больше не сможете использовать #include <algorithm>ни один из приведенных ниже кодов, если вы явно не напишете:

#ifdef max
#undef max
#endif
#include <algorithm>

Наличие макросов вместо переменных / функций также означает, что вы не можете получить их адрес:

  • если макрос как константа оценивается как магическое число, вы не можете передать его по адресу

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

Изменить: в качестве примера правильная альтернатива приведенному #define maxвыше:

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}

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

int a = 0;
double b = 1.;
max(a, b);

Если этот максимум определен как макрос, код будет компилироваться (с предупреждением).

Если этот max определен как функция шаблона, компилятор укажет на двусмысленность, и вы должны будете сказать либо max<int>(a, b)или max<double>(a, b)(и, таким образом, явно заявить о своем намерении).

утнапистим
источник
1
Он не обязательно должен быть специфичным для C ++ 11; вы можете просто использовать функции для замены использования макросов как выражений и [static] const / constexpr для замены использования макросов как констант.
utnapistim
1
Даже C99 допускает использование const int someconstant = 437;макроса, и его можно использовать почти во всех случаях. То же самое и для небольших функций. Есть несколько вещей, в которых вы можете написать что-то в виде макроса, который не будет работать в регулярном выражении в C (вы можете сделать что-то, что усредняет массив любого типа чисел, что C не может сделать, но у C ++ есть шаблоны для этого). В то время как C ++ 11 добавляет еще несколько вещей, которые «для этого вам не нужны макросы», в основном это уже решено в более ранних версиях C / C ++.
Матс Петерссон
Выполнение предварительного приращения при передаче аргумента - ужасная практика кодирования. И любой, кто пишет на C / C ++, не должен предполагать, что подобный функции вызов не является макросом.
StephenG
Многие реализации добровольно заключают идентификаторы в круглые скобки, maxа minесли за ними следуют левая скобка. Но не стоит определять такие макросы ...
LF
14

Распространенная проблема такая:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

Он напечатает 10, а не 5, потому что препроцессор расширит его следующим образом:

printf("25 / (3+2) = %d", 25 / 3 + 2);

Эта версия более безопасна:

#define DIV(a,b) (a) / (b)
фаазон
источник
2
интересный пример, в основном это просто токены без семантики
user1849534
Да. Они раскрываются так же, как и в макросе. DIVМакрос может быть переписан с парой () вокруг b.
phaazon
2
Вы имеете в виду #define DIV(a,b), что нет #define DIV (a,b), это совсем другое.
rici
6
#define DIV(a,b) (a) / (b)недостаточно хорошо; как правило, всегда добавляйте крайние скобки, например:#define DIV(a,b) ( (a) / (b) )
PJTraill
3

Макросы особенно ценны для создания универсального кода (параметры макроса могут быть любыми), иногда с параметрами.

Более того, этот код помещается (т.е. вставляется) в точку использования макроса.

OTOH, аналогичные результаты могут быть получены с помощью:

  • перегруженные функции (разные типы параметров)

  • шаблоны в C ++ (общие типы и значения параметров)

  • встроенные функции (поместите код там, где они вызываются, вместо перехода к одноточечному определению - однако это скорее рекомендация для компилятора).

изменить: что касается того, почему макрос плохой:

1) без проверки типов аргументов (у них нет типа), поэтому их можно легко использовать неправильно 2) иногда превращать в очень сложный код, который может быть трудно идентифицировать и понять в предварительно обработанном файле 3) легко сделать ошибку код в макросах, например:

#define MULTIPLY(a,b) a*b

а потом позвони

MULTIPLY(2+3,4+5)

что расширяется в

2 + 3 * 4 + 5 (а не в: (2 + 3) * (4 + 5)).

Чтобы иметь последнее, вы должны определить:

#define MULTIPLY(a,b) ((a)*(b))
user1284631
источник
3

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

Это (мета) языковая концепция, найденная в c / c ++, и, как и любой другой инструмент, они могут облегчить вашу жизнь, если вы знаете, что делаете. Проблема с макросами заключается в том, что они обрабатываются до вашего кода c / c ++ и генерируют новый код, который может быть ошибочным и вызывать ошибки компилятора, которые почти очевидны. С другой стороны, они могут помочь вам сохранить ваш код в чистоте и сэкономить много времени на вводе при правильном использовании, так что все сводится к личным предпочтениям.

Санди Хрвич
источник
Кроме того, как указано в других ответах, плохо разработанные определения препроцессора могут создавать код с допустимым синтаксисом, но с другим семантическим значением, что означает, что компилятор не будет жаловаться, и вы внесли ошибку в свой код, которую будет еще труднее найти.
Sandi Hrvić
3

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

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

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

Индиангарг
источник
Установка значений макроса в cmdline во время компиляции для создания двух вариантов из одной кодовой базы действительно хороша. умеренно.
kevinf
1
С некоторой точки зрения такое использование является наиболее опасным: инструментам (IDE, статическим анализаторам, рефакторингу) будет сложно определить возможные пути кода.
erenon
1

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

Часто хорошей альтернативой являются общие функции и / или встроенные функции.

Давиде Икарди
источник
2
Что заставляет вас думать, что макросы плохо оптимизированы? Это простая замена текста, и результат оптимизируется так же, как и код, написанный без макросов.
Бен Фойгт
@BenVoigt, но они не учитывают семантику, и это может привести к тому, что можно считать «неоптимальным» ... по крайней мере, это моя первая мысль об этом stackoverflow.com/a/14041502/1849534
user1849534
1
@ user1849534: Слово «оптимизированный» означает не это в контексте компиляции.
Бен Фойгт
1
@BenVoigt Точно, макросы - это просто подстановка текста. Компилятор просто дублирует код, это не проблема производительности, но может увеличить размер программы. Это особенно актуально в некоторых случаях, когда у вас есть ограничения по размеру программы. Некоторый код настолько заполнен макросами, что размер программы увеличивается вдвое.
Давиде Икарди
1

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

  • Создание разных выпусков одного и того же программного обеспечения с использованием конструкций типа #ifdef, например выпуск окон для разных регионов.
  • Для определения значений, связанных с тестированием кода.

Альтернативы. Для аналогичных целей можно использовать какие-то файлы конфигурации в формате ini, xml, json. Но их использование повлияет на время выполнения кода, чего можно избежать с помощью макроса препроцессора.

Индиангарг
источник
1
с C ++ 17 constexpr if + файл заголовка, содержащий переменные constexpr "config", может заменить # ifdef.
Enhex