Où devrais-je préférer utiliser 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;
Ne sont-ils pas fondamentalement les mêmes?
Non, absolument pas. Pas même proche.
En dehors du fait que votre macro est un int
et votre constexpr unsigned
est un unsigned
, il y a des différences importantes et les macros ont seulement n avantage.
Une macro est définie par le préprocesseur et est simplement substituée dans le code chaque fois que cela se produit. Le préprocesseur est dumb 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 de fonction. Vous ne pouvez donc utiliser un nom pour rien d'autre 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 de membre appelée max_height
parce qu'il s'agit d'un membre de la classe et qu'il a donc une portée différente et est distinct de celui de la portée de l'espace de noms. Si vous avez essayé de réutiliser le nom MAX_HEIGHT
pour le membre, le préprocesseur le changerait en un 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'ils se démarquent et vous pouvez être prudent en les nommant pour éviter les conflits. Si vous n'utilisez pas de macros inutilement, vous n'avez pas à vous en préoccuper (et vous n'avez pas à lire SHOUTY_NAMES
).
Si vous voulez simplement une constante dans une fonction, vous ne pouvez pas le faire avec une macro, car le préprocesseur ne sait pas ce qu'est une fonction ni ce que signifie y être. Pour limiter une macro à une partie du fichier, vous devez #undef
encore
int limit(int height) {
#define MAX_HEIGHT 720
return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}
Comparez au plus raisonnable:
int limit(int height) {
constexpr int max_height = 720;
return std::max(height, max_height);
}
Pourquoi préféreriez-vous la macro?
Une variable constexpr est une variable . Elle existe donc dans le programme et vous pouvez faire des choses normales en C++, comme prendre son adresse et y lier une référence.
Ce code a un comportement indéfini:
#define MAX_HEIGHT 720
int limit(int height) {
const int& h = std::max(height, MAX_HEIGHT);
// ...
return h;
}
Le problème est que MAX_HEIGHT
n'est pas une variable, donc pour l'appel à std::max
un temporaire int
doit être créé par le compilateur. La référence renvoyée par std::max
pourrait alors faire référence à ce temporaire, qui n’existe plus à la fin de cette instruction, donc return h
accède à la mémoire invalide.
Ce problème n’existe tout simplement pas avec une variable appropriée, car elle 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
ne pas 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 pouvoir être utilisée dans #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 à des variables par leur nom. Il ne comprend que des choses de base, telles que le développement de macros et les directives commençant par #
(comme #include
et #define
et #if
).
Si vous voulez une constante qui puisse être comprise 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>;
De manière générale, vous devriez 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, windows.h max
macro vs std::max
). De plus, une macro qui fonctionne peut facilement être utilisée de manière différente, ce qui peut ensuite provoquer 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 le feriez habituellement.
constexpr
est défini sémantiquement et génère donc beaucoup moins de problèmes.
Grande réponse par Jonathon Wakely . Je vous conseillerais également de jeter un œil à 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 d'une manière bonne . Apparemment, de nos jours, ils constituent une aide à la construction lorsque vous souhaitez que des parties très spécifiques de votre code ne soient compilées que si certains paramètres de construction sont "définis". Habituellement, tout ce que cela signifie, c'est de prendre votre nom de macro ou, mieux encore, appelons-le un Trigger
et d'ajouter des éléments comme, /D:Trigger
, -DTrigger
, etc. aux outils de construction utilisés.
Bien qu'il existe de nombreuses utilisations différentes pour les macros, ce sont les deux pratiques que je vois le plus souvent qui ne sont pas mauvaises ou obsolètes:
Ainsi, bien que vous puissiez, dans le cas du PO, atteindre le même objectif de définition d'un int avec constexpr
ou d'un MACRO
, il est peu probable que les deux se chevauchent lors de l'utilisation de 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
Autre exemple d'utilisation de macros, supposons que vous disposiez d'un nouveau matériel à publier, ou peut-être d'une génération spécifique de celui-ci comportant des solutions de contournement complexes 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