Constexpr vs macros

92

Où devrais-je préférer les macros et où devrais-je préférer constexpr ? Ne sont-ils pas fondamentalement les mêmes?

#define MAX_HEIGHT 720

contre

constexpr unsigned int max_height = 720;
Tom Dorone
la source
4
AFAIK constexpr offre plus de sécurité de type
Code-Apprentice
13
Facile: constexr, toujours.
n. «pronoms» m.
Peut répondre à certaines de vos questions stackoverflow.com/q/4748083/540286
Ortwin Angermeier

Réponses:

146

Ne sont-ils pas fondamentalement les mêmes?

Non, absolument pas. Pas même proche.

Outre le fait que votre macro est une intet votre constexpr unsignedest une unsigned, il existe des différences importantes et les macros n'ont qu'un seul avantage.

Portée

Une macro est définie par le préprocesseur et est simplement substituée dans le code à chaque fois qu'elle se produit. Le préprocesseur est stupide et ne comprend pas la syntaxe ou la sémantique C ++. Les macros ignorent les portées telles que les espaces de noms, les classes ou les blocs fonctionnels, vous ne pouvez donc pas utiliser de nom pour autre chose dans un fichier source. Ce n'est pas vrai pour une constante définie comme une variable C ++ appropriée:

#define MAX_HEIGHT 720
constexpr int max_height = 720;

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

C'est bien d'avoir une variable membre appelée max_heightcar c'est un membre de classe et a donc une portée différente, et est distincte de celle de la portée de l'espace de noms. Si vous essayez de réutiliser le nom MAX_HEIGHTdu membre, le préprocesseur le changera en ce non-sens qui ne compilerait pas:

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

C'est pourquoi vous devez donner des macros UGLY_SHOUTY_NAMESpour vous assurer qu'elles se démarquent et vous pouvez faire attention à les nommer pour éviter les conflits. Si vous n'utilisez pas de macros inutilement, vous n'avez pas à vous en soucier (ni à lire SHOUTY_NAMES).

Si vous voulez juste une constante à l'intérieur d'une fonction, vous ne pouvez pas le faire avec une macro, car le préprocesseur ne sait pas ce qu'est une fonction ou ce que cela signifie d'être à l'intérieur. Pour limiter une macro à une seule partie d'un fichier, vous en avez besoin à #undefnouveau:

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

Comparez avec le plus sensible:

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

Pourquoi préféreriez-vous le macro?

Un véritable emplacement mémoire

Une variable constexpr est une variable donc elle existe réellement dans le programme et vous pouvez faire des choses C ++ normales comme prendre son adresse et lui lier une référence.

Ce code a un comportement non défini:

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

Le problème est que ce MAX_HEIGHTn'est pas une variable, donc l'appel à std::maxun temporaire intdoit être créé par le compilateur. La référence retournée par std::maxpeut alors faire référence à ce temporaire, qui n'existe pas après la fin de cette instruction, donc return haccède à la mémoire non valide.

Ce problème n'existe tout simplement pas avec une variable appropriée, car il a un emplacement fixe en mémoire qui ne disparaît pas:

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

(En pratique, vous déclareriez probablement int hnon, const int& hmais le problème peut survenir dans des contextes plus subtils.)

Conditions du préprocesseur

Le seul moment pour préférer une macro est lorsque vous avez besoin que sa valeur soit comprise par le préprocesseur, pour une utilisation dans des #ifconditions, par exemple

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

Vous ne pouvez pas utiliser de variable ici, car le préprocesseur ne comprend pas comment faire référence aux variables par leur nom. Il ne comprend que les choses de base très basiques comme l'expansion de macro et les directives commençant par #(comme #includeet #defineet #if).

Si vous voulez une constante compréhensible par le préprocesseur, vous devez utiliser le préprocesseur pour la définir. Si vous voulez une constante pour le code C ++ normal, utilisez du code C ++ normal.

L'exemple ci-dessus est juste pour démontrer une condition de préprocesseur, mais même ce code pourrait éviter d'utiliser le préprocesseur:

using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
Jonathan Wakely
la source
3
Une constexprvariable n'a pas besoin d'occuper la mémoire jusqu'à ce que son adresse (un pointeur / référence) soit prise; sinon, il peut être complètement optimisé (et je pense qu'il pourrait y avoir Standardese qui le garantit). Je tiens à souligner cela afin que les gens ne continuent pas à utiliser l'ancien « enumhack» inférieur à partir d'une idée erronée qu'un trivial constexprqui ne nécessite pas de stockage en occupera néanmoins une partie.
underscore_d
3
Votre section "Un emplacement mémoire réel" est fausse: 1. Vous retournez par valeur (int), donc une copie est faite, le temporaire n'est pas un problème. 2. Si vous étiez retourné par référence (int &), alors votre int heightproblème serait tout aussi problématique que la macro, puisque sa portée est liée à la fonction, essentiellement temporaire aussi. 3. Le commentaire ci-dessus, "const int & h prolongera la durée de vie du temporaire" est correct.
PoweredByRice
4
@underscore_d true, mais cela ne change pas l'argument. La variable ne nécessitera pas de stockage à moins qu'il n'y ait une utilisation odr. Le fait est que lorsqu'une variable réelle avec stockage est requise, la variable constexpr fait ce qu'il faut.
Jonathan Wakely
1
@PoweredByRice 1. le problème n'a rien à voir avec la valeur de retour de limit, le problème est la valeur de retour de std::max. 2. oui, c'est pourquoi il ne renvoie pas de référence. 3. faux, voir le lien coliru ci-dessus.
Jonathan Wakely
3
@PoweredByRice soupire, vous n'avez vraiment pas besoin d'expliquer comment le C ++ fonctionne pour moi. Si vous avez const int& h = max(x, y);et maxrenvoie par la valeur la durée de vie de sa valeur de retour est prolongée. Pas par le type de retour, mais par le const int&auquel il est lié. Ce que j'ai écrit est correct.
Jonathan Wakely
11

De manière générale, vous devez utiliser constexprchaque fois que vous le pouvez et les macros uniquement si aucune autre solution n'est possible.

Raisonnement:

Les macros sont un simple remplacement dans le code, et pour cette raison, elles génèrent souvent des conflits (par exemple, maxmacro windows.h vs std::max). De plus, une macro qui fonctionne peut facilement être utilisée d'une manière différente qui peut alors déclencher d'étranges erreurs de compilation. (par exemple Q_PROPERTYutilisé sur les membres de la structure)

En raison de toutes ces incertitudes, c'est un bon style de code pour éviter les macros, exactement comme vous éviteriez habituellement les gotos.

constexpr est défini sémantiquement et génère donc généralement beaucoup moins de problèmes.

Adrian Maire
la source
1
Dans quel cas l'utilisation d'une macro est-elle inévitable?
Tom Dorone
3
Compilation conditionnelle utilisant #ifie choses pour lesquelles le préprocesseur est réellement utile. La définition d'une constante n'est pas l'une des choses pour lesquelles le préprocesseur est utile, sauf si cette constante doit être une macro car elle est utilisée dans des conditions de préprocesseur utilisant #if. Si la constante est destinée à être utilisée dans du code C ++ normal (et non dans des directives de préprocesseur), utilisez une variable C ++ normale, pas une macro de préprocesseur.
Jonathan Wakely
Sauf en utilisant des macros variadiques, principalement une utilisation de macro pour les commutateurs du compilateur, mais essayer de remplacer les instructions de macro actuelles (telles que les commutateurs conditionnels, littéraux de chaîne) traitant des instructions de code réel avec constexpr est une bonne idée?
Je dirais que les commutateurs de compilateur ne sont pas non plus une bonne idée. Cependant, je comprends parfaitement que cela soit parfois nécessaire (également des macros), en particulier pour le code multiplateforme ou intégré. Pour répondre à votre question: si vous avez déjà affaire à un préprocesseur, j'utiliserais des macros pour garder clair et intuitif ce qu'est le préprocesseur et ce qu'est le temps de compilation. Je suggérerais également de commenter fortement et de rendre son utilisation aussi courte et locale que possible (évitez que les macros s'étalent sur ou 100 lignes #if). Peut-être que l'exception est la garde #ifndef typique (standard pour #pragma une fois) qui est bien comprise.
Adrian Maire
3

Excellente réponse de Jonathon Wakely . Je vous conseillerais également de jeter un œil à la réponse de jogojapan quant à la différence entre constet constexpravant même d'envisager l'utilisation de macros.

Les macros sont stupides, mais dans le bon sens. Apparemment, de nos jours, ils sont une aide à la construction lorsque vous voulez que des parties très spécifiques de votre code ne soient compilées qu'en présence de certains paramètres de construction qui sont «définis». En général, tout ce moyen prend votre nom macro, ou mieux encore, permettent de faire appel de un Trigger, et en ajoutant des choses comme, /D:Trigger, -DTrigger, etc. , pour les outils de construction utilisés.

Bien qu'il existe de nombreuses utilisations différentes des macros, ce sont les deux que je vois le plus souvent qui ne sont pas de mauvaises pratiques / dépassées:

  1. Sections de code spécifiques au matériel et à la plate-forme
  2. Augmentation de la verbosité

Ainsi, bien que vous puissiez dans le cas de l'OP atteindre le même objectif de définir un int avec constexprou a MACRO, il est peu probable que les deux se chevauchent lors de l'utilisation des conventions modernes. Voici une macro-utilisation courante qui n'a pas encore été supprimée.

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

Comme autre exemple d'utilisation de macro, disons que vous avez du matériel à venir, ou peut-être une génération spécifique de celui-ci qui présente des solutions de contournement délicates dont les autres n'ont pas besoin. Nous définirons cette macro comme 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
la source