Почему макросы препроцессора злые и каковы альтернативы?
Я всегда спрашивал об этом, но я никогда не получал действительно хорошего ответа; я думаю, что почти любой программист, прежде чем даже написать первый "Hello World", столкнулся с такой фразой, как "макрос никогда не должен использоваться", "макрос-зло" и т. д., мой вопрос: почему? С нового C++11 есть ли реальная альтернатива после стольких лет?
легкая часть о макросах, как #pragma
, которые специфичны для платформы и компилятора, и в большинстве случаев они имеют серьезные недостатки как #pragma once
это ошибка, подверженная по крайней мере 2 важной ситуации: одно и то же имя в разных путях и с некоторыми сетевыми настройками и файловыми системами.
но в целом, как насчет макросов и альтернатив их использованию?
7 ответов:
макросы так же, как и любой другой инструмент - молоток, используемый в убийстве, не является злом, потому что это молоток. Это зло в том, как человек использует его таким образом. Если вы хотите забивать гвозди, молоток-идеальный инструмент.
есть несколько аспектов макросов, которые делают их "плохими" (я расскажу о каждом позже и предложу альтернативы):
- вы не можете отлаживать макросы.
- расширение макроса может привести к странным побочным эффектам.
- макросы не имеют "пространства имен", поэтому если у вас есть макрос, который конфликтует с именем, используемым в другом месте, вы получаете замену макросов там, где вы этого не хотели, и это обычно приводит к странным сообщениям об ошибках.
- макросы могут повлиять на то, что вы не понимаете.
Итак, давайте немного расширим здесь:
1) макросы не могут быть отлажены. Когда у вас есть макрос, который преобразуется в число или строку, исходный код будет иметь имя макроса и многие отладчики, вы не можете "видеть", что макрос переводит. Так что вы на самом деле не знаете, что происходит.
замена используйте
enum
илиconst T
для" функциональных "макросов, поскольку отладчик работает на уровне" на исходную строку, где вы находитесь", ваш макрос будет действовать как один оператор, независимо от того, один это оператор или сто. Это затрудняет понимание того, что происходит.
замена: используйте функции-inline, если это должно быть "быстро" (но будьте осторожны, что слишком много inline не очень хорошо)
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++, который использует 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;
теперь, что ошибка messge как вы думаете, вы получаете, и где вы ищете ошибку [предполагая, что вы полностью забыли - или даже не знали о - макрос begin, который живет в каком-то заголовочном файле, который написал кто-то другой? [и еще больше удовольствия, если вы включили этот макрос перед включением-вы бы утонули в странных ошибках, которые не имеют абсолютно никакого смысла, когда вы смотрите на сам код.
замена: Ну там не так много, как замена ,как "правило" - только использовать имена в верхнем регистре для макросов и никогда не используйте все имена в верхнем регистре для других вещей.
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/пользователем и выражения должны быть определены с [нормальный/встроенные/шаблон/встроенного шаблона] функции.
вот некоторые из проблем, возникающих при определении магических чисел как макросов и замене выражений макросами (вместо определения функций для оценки этих выражений):
при определении макросов для магических чисел компилятор не сохраняет информацию о типе для определенных значений. Это может вызвать предупреждения компиляции (и ошибки) и запутывают людей, отлаживающих код.
при определении макросов вместо функций программисты, использующие этот код, ожидают, что они будут работать как функции, и они этого не делают.
рассмотрим этот код:
#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>
наличие макросов вместо переменных / функций также означает, что вы не можете взять их адрес:
если макрос-как-постоянное значение магическое число, вы не можете передать его по адресу
макрос-как-функции, вы не можете использовать его в качестве предиката или взять адрес функции или рассматривать его как функтор.
Edit: в качестве примера, правильная альтернатива
#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)
(и таким образом явно заявить о своем намерении).
общая беда заключается в следующем:
#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++ (стандартные типы и значения параметров)
inline функции (поместите код, где они вызываются, вместо перехода к одноточечному определению-однако это скорее рекомендация для компилятора).
edit: что касается того, почему макрос плох:
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
такая функциональность не так легко возможна без макросов. Макросы на самом деле отличный инструмент управления конфигурацией программного обеспечения, а не просто способ создание ярлыков для повторного использования кода. Определение функций для целей повторное использование в макросах может определенно создать проблемы.