Constexpr vs macros

92

¿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;
Tom Dorone
fuente
4
AFAIK constexpr proporciona más seguridad de tipos
Code-Apprentice
13
Fácil: constexr, siempre.
n. 'pronombres' m.
Podría responder algunas de sus preguntas stackoverflow.com/q/4748083/540286
Ortwin Angermeier

Respuestas:

146

¿No son básicamente iguales?

No absolutamente no. Ni siquiera cerca.

Aparte del hecho de que su macro es una inty su constexpr unsignedes una unsigned, existen diferencias importantes y las macros solo tienen una ventaja.

Alcance

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_heightporque 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_HEIGHTdel 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_NAMESpara 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 #undefhacerlo:

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 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_HEIGHTno es una variable, por lo que la llamada a std::maxun temporal intdebe ser creada por el compilador. La referencia que devuelve std::maxpodría referirse a ese temporal, que no existe después del final de esa declaración, por lo que return haccede 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 hno, const int& hpero 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 entienda su valor, para su uso en #ifcondiciones, 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 #includey #definey #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>;
Jonathan Wakely
fuente
3
Una constexprvariable 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 ' enumtruco' debido a una idea equivocada de que un trivial constexprque no requiere almacenamiento ocupará algunos.
underscore_d
3
Su sección "Una ubicación de memoria real" es incorrecta: 1. Está regresando por valor (int), por lo que se realiza una copia, el temporal no es un problema. 2. Si hubiera regresado por referencia (int &), entonces su int heightserí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.
PoweredByRice
4
@underscore_d verdadero, pero eso no cambia el argumento. La variable no requerirá almacenamiento a menos que haya un uso odr de la misma. El punto es que cuando se requiere una variable real con almacenamiento, la variable constexpr hace lo correcto.
Jonathan Wakely
1
@PoweredByRice 1. el problema no tiene nada que ver con el valor de retorno de 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.
Jonathan Wakely
3
@PoweredByRice suspiro, realmente no necesitas explicarme cómo funciona C ++. Si tiene const int& h = max(x, y);y maxdevuelve 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.
Jonathan Wakely
11

En términos generales, debe usar constexprsiempre que pueda, y macros solo si no es posible otra solución.

Razón fundamental:

Las macros son un simple reemplazo en el código y, por esta razón, a menudo generan conflictos (por ejemplo, maxmacros 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.

Adrian Maire
fuente
1
¿En qué caso es inevitable utilizar una macro?
Tom Dorone
3
Compilación condicional usando, por #ifejemplo, 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.
Jonathan Wakely
Excepto el uso de macros variadic, principalmente el uso de macros para conmutadores del compilador, pero intentar reemplazar las declaraciones de macro actuales (como condicional, conmutadores de cadena literal) que tratan con declaraciones de código real con constexpr es una buena idea.
Yo diría que los cambios de compilador tampoco son una buena idea. Sin embargo, entiendo completamente que es necesario algunas veces (también macros), especialmente cuando se trata de código multiplataforma o incrustado. Para responder a su pregunta: si ya está tratando con un preprocesador, usaría macros para mantener claro e intuitivo qué es el preprocesador y el tiempo de compilación. También sugeriría comentar mucho y hacer que su uso sea lo más breve y local posible (evite que las macros se extiendan o 100 líneas # si). Quizás la excepción sea la típica protección #ifndef (estándar para #pragma once) que se comprende bien.
Adrian Maire
3

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 constyconstexpr 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 Triggeragregar 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:

  1. Secciones de código específicas de plataforma y hardware
  2. Mayor verbosidad se construye

Entonces, si bien puede, en el caso del OP, lograr el mismo objetivo de definir un int con constexpro 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
kayleeFrye_onDeck
fuente