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


92

Де мені краще віддати перевагу використанню макросів, а де - constexpr ? Хіба вони в основному не однакові?

#define MAX_HEIGHT 720

проти

constexpr unsigned int max_height = 720;

4
AFAIK constexpr забезпечує більшу безпеку типу
Code-Apprentice

13
Легко: constexr, завжди.
п. 'займенники' m.

Міг би відповісти на деякі ваші запитання stackoverflow.com/q/4748083/540286
Ortwin Angermeier

Відповіді:


147

Хіба вони в основному не однакові?

Ні. Абсолютно ні. Навіть близько не.

Окрім того, що ваш макрос є 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).

Якщо вам потрібна константа, яка може бути зрозумілою препроцесору, тоді вам слід використовувати препроцесор для її визначення. Якщо вам потрібна константа для звичайного коду С ++, використовуйте звичайний код С ++.

Наведений вище приклад - лише для демонстрації стану препроцесора, але навіть цей код може уникнути використання препроцесора:

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 проти std::max). Крім того, макрос, який працює, може бути легко використаний по-іншому, що потім може викликати дивні помилки компіляції. (наприклад, Q_PROPERTYвикористовується для членів структури)

Через усі ці невизначеності, добре уникати стилю коду, щоб уникати макросів, точно так само, як зазвичай уникати gotos.

constexpr є семантично визначеним, і, отже, зазвичай породжує набагато менше проблем.


1
У якому випадку використання макросу не уникнути?
Tom Dorone

3
Умовна компіляція з використанням #if речей, для яких препроцесор насправді корисний. Визначення константи - не одна з речей, для якої корисний препроцесор, якщо ця константа не повинна бути макросом, оскільки вона використовується в умовах препроцесора #if. Якщо константа призначена для використання в звичайному коді С ++ (не директивах препроцесора), тоді використовуйте звичайну змінну С ++, а не макрос препроцесора.
Джонатан Уейклі

За винятком використання варіативних макросів, переважно використання макросів для комутаторів компілятора, але спроба замінити поточні оператори макросів (наприклад, умовні, рядкові літеральні комутатори), що мають справу із справжніми операторами коду, constexpr - це гарна ідея?

Я б сказав, що перемикачі компілятора теж не є хорошою ідеєю. Однак я цілком розумію, що це потрібно кілька разів (також макроси), особливо коли йдеться про крос-платформний або вбудований код. Відповісти на ваше запитання: Якщо ви вже маєте справу з препроцесором, я б використовував макроси, щоб чітко та інтуїтивно зрозуміти, що таке препроцесор, а що час компіляції. Я б також запропонував робити потужні коментарі та робити їх як можна коротшими та локальними (уникайте розповсюдження макросів або 100 рядків #if). Можливо, виняток становить типовий захист #ifndef (стандарт для #pragma один раз), який добре розуміється.
Адріан Мейр,

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.