c++ - Constexpr vs macros
c++11 (3)
¿No son básicamente lo mismo?
No absolutamente no. Ni siquiera cerca.
Además del hecho de que su macro es un
int
y su
constexpr unsigned
es un
unsigned
, existen diferencias importantes y las macros solo tienen
una
ventaja.
Alcance
El preprocesador define una macro y simplemente se sustituye en el código cada vez que ocurre. El preprocesador es tonto y no entiende la sintaxis o semántica de C ++. Las macros ignoran ámbitos como espacios de nombres, clases o bloques de funciones, por lo que no puede usar un nombre para otra cosa en un archivo fuente. Eso no es cierto para una constante definida como una variable C ++ adecuada:
#define MAX_HEIGHT 720
constexpr int max_height = 720;
class Window {
// ...
int max_height;
};
Está bien tener una variable miembro llamada
max_height
porque es un miembro de clase y, por lo tanto, tiene un alcance diferente y es diferente de la del alcance del espacio de nombres.
Si trató de reutilizar el nombre
MAX_HEIGHT
para el miembro, entonces el preprocesador lo cambiaría a este sinsentido que no compilaría:
class Window {
// ...
int 720;
};
Esta es la razón por la que debe
UGLY_SHOUTY_NAMES
macros
UGLY_SHOUTY_NAMES
para asegurarse de que se destacan y puede tener cuidado al nombrarlos para evitar conflictos.
Si no usa macros innecesariamente, no tiene que preocuparse por eso (y no tiene que leer
SHOUTY_NAMES
).
Si solo desea una constante dentro de una función, no puede hacerlo con una macro, porque el preprocesador no sabe qué es una función o qué significa estar dentro de ella.
Para limitar una macro a solo una cierta parte de un archivo, debe
#undef
nuevamente:
int limit(int height) {
#define MAX_HEIGHT 720
return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}
Compare con el mucho más sensato:
int limit(int height) {
constexpr int max_height = 720;
return std::max(height, max_height);
}
¿Por qué preferirías el macro?
Una ubicación de memoria real
Una variable constexpr es una variable, por lo que en realidad existe en el programa y puede hacer cosas normales de C ++ como tomar su dirección y vincular una referencia a ella.
Este código tiene un comportamiento indefinido:
#define MAX_HEIGHT 720
int limit(int height) {
const int& h = std::max(height, MAX_HEIGHT);
// ...
return h;
}
El problema es que
MAX_HEIGHT
no es una variable, por lo que para la llamada a
std::max
el compilador debe crear un
int
temporal.
La referencia que devuelve
std::max
podría referirse a esa temporal, que no existe después del final de esa declaración, por lo que
return h
accede a memoria no válida.
Ese problema simplemente no existe con una variable adecuada, porque tiene una ubicación fija en la memoria que no desaparece:
int limit(int height) {
constexpr int max_height = 720;
const int& h = std::max(height, max_height);
// ...
return h;
}
(En la práctica, probablemente declararía
int h
no
const int& h
pero el problema puede surgir en contextos más sutiles).
Condiciones del preprocesador
El único momento para preferir una macro es cuando necesita que el preprocesador comprenda su valor, para usarlo en condiciones
#if
, p. Ej.
#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif
No podría usar una variable aquí, porque el preprocesador no entiende cómo referirse a las variables por su nombre.
Solo comprende cosas básicas muy básicas como la expansión de macros y las directivas que comienzan con
#
(como
#include
y
#define
y
#if
).
Si desea una constante que pueda ser entendida por el preprocesador, entonces debe usar el preprocesador para definirla. Si desea una constante para el código C ++ normal, use el código C ++ normal.
El ejemplo anterior es solo para demostrar una condición de preprocesador, pero incluso ese código podría evitar el uso del preprocesador:
using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
¿Dónde debería preferir usar macros y dónde debería preferir constexpr ? ¿No son básicamente lo mismo?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
En términos generales, debe usar constexpr siempre que pueda, y macros solo si no hay otra solución posible.
Racional:
Las macros son un reemplazo simple en el código, y por esta razón, generalmente generan conflictos (por ejemplo, windows.h max macro vs std :: max). Además, una macro que funciona puede usarse fácilmente de una manera diferente que luego desencadena errores de compilación extraños. (por ejemplo, Q_PROPERTY utilizado en miembros de la estructura)
Debido a todas esas incertidumbres, es un buen estilo de código para evitar macros, exactamente como generalmente se evitan los gotos.
constexpr se define semánticamente y, por lo tanto, genera muchos menos problemas.
Gran respuesta de
Jonathon Wakely
.
También le aconsejo que eche un vistazo a
la respuesta de jogojapan sobre
cuál es la diferencia entre
const
y
constexpr
antes de siquiera considerar el uso de macros.
Las macros son tontas, pero en el
buen
sentido.
Aparentemente hoy en día son una ayuda para la compilación para cuando desea que partes muy específicas de su código solo se compilen en presencia de ciertos parámetros de compilación que se "definen".
Por lo general, todo lo que significa es tomar su nombre de macro, o mejor aún, llamémoslo
Trigger
, y
-DTrigger
cosas como,
/D:Trigger
,
-DTrigger
, etc. a las herramientas de compilación que se utilizan.
Si bien hay muchos usos diferentes para las macros, estos son los dos que veo con más frecuencia que no son prácticas malas / desactualizadas:
- Secciones de código de hardware y plataforma específica
- Aumento de las construcciones de verbosidad
Entonces, si bien en el caso del OP puede lograr el mismo objetivo de definir un int con
constexpr
o un
MACRO
, es poco probable que los dos se superpongan al usar convenciones modernas.
Aquí hay algunos macro-usos comunes que aún no se han eliminado.
#if defined VERBOSE || defined DEBUG || defined MSG_ALL
// Verbose message-handling code here
#endif
Como otro ejemplo para el uso de macros, supongamos que tiene un hardware próximo para lanzar, o tal vez una generación específica que tenga algunas soluciones difíciles que los otros no requieren.
Definiremos esta macro como
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