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

Где я предпочитаю использовать макросы и где я должен предпочесть constexpr? Разве они в основном не одинаковы?

#define MAX_HEIGHT 720

против

constexpr unsigned int max_height = 720;

Ответ 1

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

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

Помимо того факта, что ваш макрос является 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 not 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>;

Ответ 2

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

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

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

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

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

Ответ 3

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

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

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

  1. Секции кода оборудования и платформы
  2. Увеличенные сборники

Таким образом, хотя вы можете в случае OP выполнить ту же цель определения int с constexpr или 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