Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Для макросов "function-like", поскольку отладчик работает на уровне "на исходную строку, где вы находитесь", ваш макрос будет действовать как один оператор, независимо от того, является ли он одним утверждением или сотней. Мне сложно понять, что происходит.

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

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

и у нас есть код в С++, который использует 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] Поведение undefined для обновления одной переменной более одного раза "в точке последовательности". Точка последовательности не совсем то же самое, что и оператор, но для большинства целей и целей это то, что мы должны рассматривать. Поэтому x++ * x++ будет дважды обновлять x, что означает undefined и, вероятно, приведет к разным значениям в разных системах и другому результату результата в x.

Ответ 2

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

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

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

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

с новым С++ 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>

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

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

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

Изменить: в качестве примера, правильная альтернатива #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) (и, таким образом, явно указать свое намерение).

Ответ 3

Общей проблемой является следующее:

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

Ответ 4

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

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

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

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

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

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

edit: что касается макроса bad:

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

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