Альтернативы использованию #define в С++? Почему это осуждается?

Я занимаюсь разработкой C++ меньше года, но за это время я слышал, как несколько человек говорят о том, насколько ужасен #define. Теперь я понимаю, что он интерпретируется препроцессором, а не компилятором, и поэтому не может быть отлажен, но так ли это плохо?

Вот пример (непроверенный код, но общее представление вы поняли):

#define VERSION "1.2"

#include <string>

class Foo {
  public:
    string getVersion() {return "The current version is "+VERSION;}
};
  1. Почему этот код плохой?
  2. Есть ли альтернатива использованию #define?

person Joel    schedule 21.04.2012    source источник
comment
Обратное этому вопросу: Зачем использовать #define вместо переменной   -  person Matt Ball    schedule 21.04.2012
comment
define — это хитрые конструкции, которыми легко злоупотребить или неправильно использовать. но, как и goto, они могут быть очень полезными.   -  person Anycorn    schedule 21.04.2012
comment
возможный дубликат static const vs #define, в частности этот ответ должно быть достаточно.   -  person Anonymous    schedule 21.04.2012
comment
Сомневаюсь, что этот код компилируется. Без + было бы, но как есть? Неа.   -  person Matthieu M.    schedule 21.04.2012


Ответы (5)


Почему этот код плохой?

Потому что ВЕРСИЯ может быть перезаписана, и компилятор вам этого не скажет.

Есть ли альтернатива использованию #define?

const char * VERSION = "1.2";

or

const std::string VERSION = "1.2";
person Benjamin Lindley    schedule 21.04.2012
comment
На самом деле, компилятор, скорее всего, сообщит вам, когда вы переопределите макрос. Даже без каких-либо предупреждений gcc предупредит вас предупреждением: FOO переопределено... примечание: это расположение предыдущего определения. И он будет делать это только в том случае, если значения разные, поэтому вы не получите бесполезных предупреждений. - person Ambroz Bizjak; 21.04.2012
comment
Имеет ли const ту же область видимости, что и #define? Я предполагаю, что у #define действительно нет области применения — он может пойти куда угодно. Может ли const использоваться где угодно? - person Joel; 21.04.2012
comment
@Joel: Одно из самых больших преимуществ const x по сравнению с #define заключается в том, что он ограничен. - person Puppy; 21.04.2012
comment
@Joel: Нет, это ограничено. Обычно это рассматривается как преимущество. Вы имеете в виду случай, когда это не так? - person Benjamin Lindley; 21.04.2012
comment
В моем примере, если бы я заменил свое определение на константу, была бы константа в глобальной области видимости? Означает ли это, что я могу использовать константу в любом месте файла? - person Joel; 21.04.2012
comment
Негативные эффекты #define на самом деле во многом зависят от того, находится ли он в заголовочном файле или в исходном файле, который компилируется отдельно. В исходном файле вы можете позволить себе использовать гораздо более короткие идентификаторы, включая имена макросов, без риска конфликта, потому что вы видите весь код. Это также не будет конфликтовать с заголовками, которые вы включаете, при условии, что вы всегда используете более конкретные идентификаторы в заголовках (например, ставите перед ними префикс). - person Ambroz Bizjak; 21.04.2012
comment
@AmbrozBizjak: проблема в другом, компилятор (фактически препроцессор) заменит VERSION строкой везде, независимо от контекста. - person David Rodríguez - dribeas; 21.04.2012

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

Рассмотрим случай max, который иногда реализуется как макрос. Как следствие, вы не можете использовать идентификатор max где-либо в своем коде. Где. Но компилятор вам этого не скажет. Вместо этого ваш код будет работать ужасно неправильно, и вы понятия не имеете, почему.

Теперь, при некоторой осторожности, эту проблему можно свести к минимуму (если не полностью устранить). Но для большинства применений #define в любом случае есть лучшие альтернативы, поэтому расчет затрат/выгод становится искаженным: небольшой недостаток вместо отсутствия какой бы то ни было выгоды. Зачем использовать дефектную функцию, если она не дает никаких преимуществ?

Итак, вот очень простая схема:

  1. Нужна константа? Используйте константу (не определение)
  2. Нужна функция? Используйте функцию (не определение)
  3. Нужно что-то, что нельзя смоделировать с помощью константы или функции? Используйте определение, но делайте это правильно.

Делать это «правильно» — само по себе искусство, но есть несколько простых рекомендаций:

  1. Используйте уникальное имя. Все заглавные, всегда с префиксом уникального идентификатора библиотеки. max? Вне. VERSION? Вне. Вместо этого используйте MY_COOL_LIBRARY_MAX и MY_COOL_LIBRARY_VERSION. Например, библиотеки Boost, крупные пользователи макросов, всегда используют макросы, начинающиеся с BOOST_<LIBRARY_NAME>_.

  2. Остерегайтесь оценки. По сути, параметр в макросе — это просто заменяемый текст. Как следствие, #define MY_LIB_MULTIPLY(x) x * x сломан: его можно было использовать как MY_LIB_MULTIPLY(2 + 5), в результате чего получилось 2 + 5 * 2 + 5. Не то, что мы хотели. Чтобы избежать этого, всегда заключайте в скобки все использование аргументов (если только вы не знаете, точно что делаете – спойлер: скорее всего, вы не знаете, что делаете). т; даже эксперты тревожно часто ошибаются в этом).

    Правильная версия этого макроса будет:

     #define MY_LIB_MULTIPLY(x) ((x) * (x))
    

Но есть по-прежнему множество способов сделать макросы ужасно неправильными, и, повторяю, компилятор здесь вам не поможет.

person Konrad Rudolph    schedule 21.04.2012

#define не является плохим по своей сути, им просто легко злоупотреблять. Для чего-то вроде строки версии это работает нормально, хотя const char* было бы лучше, но многие программисты используют его для гораздо большего. Например, использование #define в качестве определения типа глупо, когда в большинстве случаев определение типа было бы лучше. Так что в операторах #define нет ничего плохого, и некоторые вещи невозможно сделать без них. Их необходимо оценивать в каждом конкретном случае. Если вы можете найти способ решить проблему без использования препроцессора, вы должны это сделать.

person Kyle    schedule 21.04.2012

Я бы не стал использовать #define для определения постоянного ключевого слова use static или еще лучше const int kMajorVer = 1; const int kMinorVer = 2; ИЛИ const std::string kVersion = "1.2";

У Херба Саттера есть отличная статья, в которой подробно описывается, почему #define — это плохо, и приводится несколько примеров, где действительно нет другого способа добиться того же самого: http://www.gotw.ca/gotw/032.htm.

В принципе, как и со многими другими вещами, все в порядке, если вы используете его правильно, но им легко злоупотреблять, а макро-ошибки особенно загадочны и неудобны для отладки.

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

person EdChum    schedule 21.04.2012

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

const char* VERSION = "1.2"

Однако есть случаи, когда невозможно сделать то, что вы хотите, без препроцессора:

#define Log(x) cout << #x << " = " << (x) << endl;
person Andrew Tomazos    schedule 21.04.2012
comment
Компиляция в C++ на самом деле занимает больше, чем два прохода. - person Konrad Rudolph; 21.04.2012
comment
Я не знаю, что вы подразумеваете под «логическими проходами». Вернее, я не понимаю, насколько это было бы небезопасно по своей сути. - person Konrad Rudolph; 21.04.2012
comment
Рассмотрим (A) исходный код, (B) предварительно обработанную промежуточную форму кода; и (C) аннотированное дерево синтаксического анализа. Два логических прохода, о которых я говорю, это предварительная обработка A->B и компиляция B->C. - person Andrew Tomazos; 21.04.2012
comment
Да, я так и думал. Почему это изначально небезопасно? Вы имеете в виду тот факт, что C не может содержать аннотации из A, так как они теряются при переходе A->B? Это не врожденная проблема, а только проблема с текущими реализациями. На самом деле Clang отлично с этим справляется (например, аннотированное дерево синтаксического анализа содержит всю информацию из необработанного источника), и, насколько я знаю, прилагаются усилия, чтобы GCC также справился с этим. - person Konrad Rudolph; 21.04.2012
comment
Это глубже - по спецификации они логически разделены - препроцессор разрешает все свои потенциально (по Тьюрингу полные) сложные изменения на источнике, а затем результат компилируется (снова). Независимо от того, сколько функций компилятора/IDE вы при этом используете, никогда не будет так просто увидеть, что происходит, как, например, в Java без препроцессора. - person Andrew Tomazos; 21.04.2012