Constexpr против макросов

92

Где мне лучше использовать макросы, а где constexpr ? Разве они не одинаковы?

#define MAX_HEIGHT 720

против

constexpr unsigned int max_height = 720;
Том Дороне
источник
4
AFAIK constexpr обеспечивает большую безопасность типов
Code-Apprentice
13
Легко: constexr, всегда.
п. 'местоимения' м.
Могу ответить на некоторые ваши вопросы stackoverflow.com/q/4748083/540286
Ортвин Ангермайер

Ответы:

146

Разве они не одинаковы?

Нет. Абсолютно нет. Даже не близко.

Помимо того факта, что ваш макрос - это, intа ваш constexpr unsigned- есть unsigned, существуют важные различия, и у макросов есть только одно преимущество.

Объем

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

#define MAX_HEIGHT 720
constexpr int max_height = 720;

class Window {
  // ...
  int max_height;
};

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

class Window {
  // ...
  int 720;
};

Вот почему вы должны давать макросы, UGLY_SHOUTY_NAMESчтобы они выделялись, и вы можете осторожно присваивать им имена, чтобы избежать конфликтов. Если вы не используете макросы без надобности, вам не о чем беспокоиться (и не нужно читать SHOUTY_NAMES).

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

int limit(int height) {
#define MAX_HEIGHT 720
  return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}

Сравните с гораздо более разумным:

int limit(int height) {
  constexpr int max_height = 720;
  return std::max(height, max_height);
}

Почему вы предпочли бы макрос?

Настоящая память

Переменная constexpr - это переменная, поэтому она действительно существует в программе, и вы можете делать обычные вещи C ++, например брать ее адрес и связывать с ним ссылку.

Этот код имеет неопределенное поведение:

#define MAX_HEIGHT 720
int limit(int height) {
  const int& h = std::max(height, MAX_HEIGHT);
  // ...
  return h;
}

Проблема в том, что MAX_HEIGHTэто не переменная, поэтому для вызова std::maxвременного intдолжен быть создан компилятором. Ссылка, которую возвращает, std::maxможет затем относиться к тому временному объекту, который не существует после конца этого оператора, поэтому return hобращается к недопустимой памяти.

Этой проблемы просто не существует с правильной переменной, потому что у нее есть фиксированное место в памяти, которое не исчезает:

int limit(int height) {
  constexpr int max_height = 720;
  const int& h = std::max(height, max_height);
  // ...
  return h;
}

(На практике вы, вероятно, заявили бы, что int hнет, const int& hно проблема может возникнуть в более тонких контекстах.)

Условия препроцессора

Единственный раз, когда нужно предпочесть макрос, - это когда вам нужно, чтобы его значение было понятно препроцессору для использования в #ifусловиях, например

#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif

Вы не можете использовать здесь переменную, потому что препроцессор не понимает, как обращаться к переменным по имени. Он понимает только базовые вещи, такие как расширение макросов и директивы, начинающиеся с #(вроде #includeи #defineи #if).

Если вам нужна константа, которую может понять препроцессор, вы должны использовать препроцессор для ее определения. Если вам нужна константа для обычного кода C ++, используйте обычный код C ++.

Приведенный выше пример просто демонстрирует условие препроцессора, но даже этот код может избежать использования препроцессора:

using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
Джонатан Уэйкли
источник
3
constexprПотребность переменная не не занимают память , пока его адрес (указатель / ссылка) принимается; в противном случае его можно полностью оптимизировать (и я думаю, что это может быть Standardese). Я хочу подчеркнуть это, чтобы люди не продолжали использовать старый, низший уровень « enumвзлома», исходя из ошибочной идеи, что тривиальный элемент constexpr, не требующий хранилища, тем не менее займет кое-что.
underscore_d
3
Ваш раздел «Настоящее место в памяти» неверен: 1. Вы возвращаетесь по значению (int), поэтому копия сделана, временная не проблема. 2. Если бы вы вернулись по ссылке (int &), тогда у вас int heightбыла бы такая же проблема, как и с макросом, поскольку его область действия привязана к функции, по сути, тоже временно. 3. Приведенный выше комментарий «const int & h продлит время жизни временного» верен.
PoweredByRice
4
@underscore_d правда, но это не меняет аргумент. Переменная не требует хранения, если ее не использовать отдельно. Дело в том, что когда требуется реальная переменная с хранилищем, переменная constexpr поступает правильно.
Джонатан
1
@PoweredByRice 1. проблема не связана с возвращаемым значением limit, проблема в возвращаемом значении std::max. 2. да, поэтому не возвращает ссылку. 3. Неправильно, см. Ссылку coliru выше.
Джонатан
3
@PoweredByRice вздохнул, тебе действительно не нужно объяснять мне, как работает C ++. Если у вас есть const int& h = max(x, y);и maxвозвращается по значению, время жизни его возвращаемого значения увеличивается. Не по возвращаемому типу, а по тому, к const int&чему он привязан. Я правильно написал.
Джонатан
11

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

Обоснование:

Макросы - это простая замена в коде, и по этой причине они часто вызывают конфликты (например, maxмакрос windows.h vs std::max). Кроме того, работающий макрос можно легко использовать по-другому, что может вызвать странные ошибки компиляции. (например, Q_PROPERTYиспользуется на элементах конструкции)

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

constexpr определяется семантически и поэтому обычно вызывает гораздо меньше проблем.

Адриан Мэр
источник
1
В каком случае использование макроса неизбежно?
Tom
3
Условная компиляция с использованием того, для #ifчего препроцессор действительно полезен. Определение константы - не то, для чего препроцессор полезен, если только эта константа не должна быть макросом, потому что она используется в условиях препроцессора #if. Если константа предназначена для использования в обычном коде C ++ (а не в директивах препроцессора), используйте обычную переменную C ++, а не макрос препроцессора.
Джонатан
За исключением использования вариативных макросов, в основном макрос используется для переключателей компилятора, но попытка заменить текущие операторы макроса (например, условные, строковые буквальные переключатели), работающие с операторами реального кода, с constexpr - хорошая идея?
Я бы сказал, что переключатели компилятора тоже не лучшая идея. Однако я полностью понимаю, что иногда это необходимо (также макросы), особенно при работе с кроссплатформенным или встроенным кодом. Чтобы ответить на ваш вопрос: если вы уже имеете дело с препроцессором, я бы использовал макросы, чтобы было понятно и интуитивно понятно, что такое препроцессор, а что время компиляции. Я бы также посоветовал подробно прокомментировать и использовать его как можно короче и локально (избегайте распространения макросов или 100 строк #if). Возможно, исключением является типичная защита #ifndef (стандартная для #pragma once), которая хорошо известна.
Адриан Мэр
3

Отличный ответ Джонатона Уэйкли . Я также советую вам взглянуть на ответ jogojapan относительно того, в чем разница, constи constexprпрежде чем вы даже начнете рассматривать использование макросов.

Макросы тупые, но в хорошем смысле. Якобы в настоящее время они помогают при сборке, когда вы хотите, чтобы очень конкретные части вашего кода компилировались только при наличии определенных параметров сборки. Как правило, все , что средства принимают ваше имя макроса, или еще лучше, давайте назовем это Trigger, и добавляющие вещи , как, /D:Trigger, -DTriggerи т.д. , для автоматической сборки используются.

Хотя существует множество различных применений макросов, я чаще всего вижу два неплохих / устаревших метода:

  1. Разделы кода для оборудования и платформы
  2. Повышенная многословность сборок

Таким образом, хотя вы можете в случае OP достичь той же цели по определению int с помощью constexprили a MACRO, маловероятно, что они будут пересекаться при использовании современных соглашений. Вот несколько распространенных макросов, которые еще не были прекращены.

#if defined VERBOSE || defined DEBUG || defined MSG_ALL
    // Verbose message-handling code here
#endif

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

#if defined GEN_3_HW && defined _WIN64
    // Windows-only special handling for 64-bit upcoming hardware
#elif defined GEN_3_HW && defined __APPLE__
    // Special handling for macs on the new hardware
#elif !defined _WIN32 && !defined __linux__ && !defined __APPLE__ && !defined __ANDROID__ && !defined __unix__ && !defined __arm__
    // Greetings, Outlander! ;)
#else
    // Generic handling
#endif
kayleeFrye_onDeck
источник