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;
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;
Réponses:
Ne sont-ils pas fondamentalement les mêmes?
Non, absolument pas. Pas même proche.
Outre le fait que votre macro est une int
et votre constexpr unsigned
est une unsigned
, il existe des différences importantes et les macros n'ont qu'un seul avantage.
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_height
car 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_HEIGHT
du 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_NAMES
pour 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 à #undef
nouveau:
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?
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_HEIGHT
n'est pas une variable, donc l'appel à std::max
un temporaire int
doit être créé par le compilateur. La référence retournée par std::max
peut alors faire référence à ce temporaire, qui n'existe pas après la fin de cette instruction, donc return h
accè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 h
non, const int& h
mais le problème peut survenir dans des contextes plus subtils.)
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 #if
conditions, 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 #include
et #define
et #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>;
constexpr
variable 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 « enum
hack» inférieur à partir d'une idée erronée qu'un trivial constexpr
qui ne nécessite pas de stockage en occupera néanmoins une partie.
int height
problè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.
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.
const int& h = max(x, y);
et max
renvoie 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.
De manière générale, vous devez utiliser constexpr
chaque fois que vous le pouvez et les macros uniquement si aucune autre solution n'est possible.
Les macros sont un simple remplacement dans le code, et pour cette raison, elles génèrent souvent des conflits (par exemple, max
macro 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_PROPERTY
utilisé 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.
#if
ie 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.
Excellente réponse de Jonathon Wakely . Je vous conseillerais également de jeter un œil à la réponse de jogojapan quant à la différence entre const
et constexpr
avant 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:
Ainsi, bien que vous puissiez dans le cas de l'OP atteindre le même objectif de définir un int avec constexpr
ou 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