¿Dónde debería preferir usar macros y dónde debería preferir constexpr ? ¿No son básicamente iguales?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
¿Dónde debería preferir usar macros y dónde debería preferir constexpr ? ¿No son básicamente iguales?
#define MAX_HEIGHT 720
vs
constexpr unsigned int max_height = 720;
Respuestas:
¿No son básicamente iguales?
No absolutamente no. Ni siquiera cerca.
Aparte del hecho de que su macro es una int
y su constexpr unsigned
es una unsigned
, existen diferencias importantes y las macros solo tienen una ventaja.
Una macro la define el preprocesador y simplemente se sustituye en el código cada vez que ocurre. El preprocesador es tonto y no comprende la sintaxis ni la 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 nada más 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 la clase y, por lo tanto, tiene un alcance diferente y es diferente de la que se encuentra en el alcance del espacio de nombres. Si intenta reutilizar el nombre MAX_HEIGHT
del miembro, el preprocesador lo cambiaría a esta tontería que no compilaría:
class Window {
// ...
int 720;
};
Es por eso que debe proporcionar macros UGLY_SHOUTY_NAMES
para asegurarse de que se destaquen y puede tener cuidado al nombrarlas 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 hacer eso 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 determinada parte de un archivo, debe volver a #undef
hacerlo:
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 variable constexpr es una variable, por lo que realmente 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 la llamada a std::max
un temporal int
debe ser creada por el compilador. La referencia que devuelve std::max
podría referirse a ese temporal, que no existe después del final de esa declaración, por lo que return h
accede a la 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 que int h
no, const int& h
pero el problema puede surgir en contextos más sutiles).
El único momento para preferir una macro es cuando necesita que el preprocesador entienda su valor, para su uso en #if
condiciones, p. Ej.
#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif
No puede usar una variable aquí, porque el preprocesador no entiende cómo referirse a las variables por su nombre. Solo comprende cosas muy básicas como la expansión macro y las directivas que comienzan con #
(como #include
y #define
y #if
).
Si desea una constante que el preprocesador pueda entender , debe usar el preprocesador para definirla. Si desea una constante para el código C ++ normal, utilice 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>;
constexpr
variable no necesita ocupar memoria hasta que se toma su dirección (un puntero / referencia); de lo contrario, se puede optimizar completamente (y creo que podría haber Standardese que lo garantice). Quiero enfatizar esto para que la gente no continúe usando el viejo e inferior ' enum
truco' debido a una idea equivocada de que un trivial constexpr
que no requiere almacenamiento ocupará algunos.
int height
sería tan problemático como la macro, ya que su alcance está vinculado a la función, esencialmente temporal también. 3. El comentario anterior, "const int & h extenderá la vida útil del temporal" es correcto.
limit
, el problema es el valor de retorno de std::max
. 2. sí, por eso no devuelve una referencia. 3. incorrecto, vea el enlace coliru arriba.
const int& h = max(x, y);
y max
devuelve por el valor, se extiende la vida útil de su valor de devolución. No por el tipo de retorno, sino por el const int&
que está vinculado. Lo que escribí es correcto.
En términos generales, debe usar constexpr
siempre que pueda, y macros solo si no es posible otra solución.
Las macros son un simple reemplazo en el código y, por esta razón, a menudo generan conflictos (por ejemplo, max
macros windows.h vs std::max
). Además, una macro que funciona puede usarse fácilmente de una manera diferente, lo que puede provocar extraños errores de compilación. (p.ejQ_PROPERTY
usado en miembros de estructura)
Debido a todas esas incertidumbres, es un buen estilo de código evitar las macros, exactamente como normalmente evitarías los gotos.
constexpr
se define semánticamente y, por lo tanto, generalmente genera muchos menos problemas.
#if
ejemplo, cosas para las que el preprocesador es realmente útil. Definir una constante no es una de las cosas para las que el preprocesador es útil, a menos que esa constante deba ser una macro porque se usa en condiciones de preprocesador usando #if
. Si la constante es para uso en código C ++ normal (no directivas de preprocesador), entonces use una variable C ++ normal, no una macro de preprocesador.
Gran respuesta de Jonathon Wakely . También te aconsejo que eches un vistazo a la respuesta de jogojapan en cuanto a cuál es la diferencia entre const
yconstexpr
antes de que consideres el uso de macros.
Las macros son tontas, pero en el buen sentido. Aparentemente, hoy en día son una ayuda de 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 eso significa es tomar el nombre de su macro, o mejor aún, llamémoslo ay Trigger
agregar cosas como /D:Trigger
,-DTrigger
, etc, para las herramientas de construcción que se utiliza.
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:
Entonces, si bien puede, en el caso del OP, lograr el mismo objetivo de definir un int con constexpr
o a MACRO
, es poco probable que los dos se superpongan cuando se usan las convenciones modernas. Aquí hay algunos usos comunes de macros que aún no se han eliminado.
#if defined VERBOSE || defined DEBUG || defined MSG_ALL
// Verbose message-handling code here
#endif
Como otro ejemplo de uso de macros, digamos que tiene algún hardware próximo para lanzar, o tal vez una generación específica del mismo que tiene algunas soluciones difíciles que los demás 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