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

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

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

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

7 ответов

Решение

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

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

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

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

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

Замена: использование enum или же const T

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

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

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

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

Другой пример - "если еще" в макросах, скажем, у нас есть это:

#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++, который использует начало:

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 также.

Выражение "макросы - это зло" обычно относится к использованию #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

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

Это означает, что если вы определите макрос выше (для макс.), Вы больше не сможете #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<int>(a, b) или же max<double>(a, b) (и, таким образом, четко заявить о своем намерении).

Общая проблема заключается в следующем:

#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)

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

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

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))

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

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

Макросы в 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

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

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

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

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

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

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

Другие вопросы по тегам