c++ c++11 const constexpr

Constantes globales en C++ 11



c++11 constexpr (3)

¿Cuáles son las mejores maneras de declarar y definir constantes globales en C ++? Estoy más interesado en el estándar C ++ 11 ya que se corrige mucho en este aspecto.

[EDITAR (aclaración)]: en esta pregunta, "constante global" denota variable constante o función que se conoce en el momento de la compilación en cualquier ámbito. La constante global debe ser accesible desde más de una unidad de traducción. No es necesariamente una constante de estilo constexpr; puede ser algo como const std::map<int, std::string> m = { { 1, "U" }, { 5, "V" } }; o const std::map<int, std::string> * mAddr() { return & m; } const std::map<int, std::string> * mAddr() { return & m; } . No toco el alcance o el nombre del buen estilo preferible por constante en esta pregunta. Dejemos estos asuntos para otra pregunta. [END_EDIT]

Quiero saber las respuestas para todos los diferentes casos, por lo que asumamos que T es uno de los siguientes:

typedef int T; // 1 typedef long double T; // 2 typedef std::array<char, 1> T; // 3 typedef std::array<long, 1000> T; // 4 typedef std::string T; // 5 typedef QString T; // 6 class T { // unspecified amount of code }; // 7 // Something special // not mentioned above? // 8

Creo que no hay una gran diferencia semántica (no discuto bien la denominación o el estilo de alcance aquí) entre los 3 ámbitos posibles:

// header.hpp extern const T tv; T tf(); // Global namespace Nm { extern const T tv; T tf(); // Namespace } struct Cl { static const T tv; static T tf(); // Class };

Pero si elegir una mejor manera de las alternativas a continuación depende de la diferencia entre los ámbitos de declaración anteriores, por favor, señálelo.

Considere también el caso cuando la llamada a la función se usa en definición constante, por ejemplo, <some value>==f(); . ¿Cómo influiría una función en una inicialización constante en la elección entre alternativas?

  1. Consideremos primero T con constexpr constructor. Alternativas obvias son:

    // header.hpp namespace Ns { constexpr T A = <some value>; constexpr T B() { return <some value>; } inline const T & C() { static constexpr T t = <some value>; return t; } const T & D(); } // source.cpp const T & Ns::D() { static constexpr T t = <some value>; return t; }

    Creo que A y B son más adecuados para T pequeña (de modo que tener múltiples instancias o copiarlas en tiempo de ejecución no es un problema), por ejemplo, 1-3 , a veces 7 . C y D son mejores si T es grande, por ejemplo, 4 , a veces 7 .

  2. T sin constexpr constructor. Alternativas:

    // header.hpp namespace Ns { extern const T a; inline T b() { return <some value>; } inline const T & c() { static const T t = <some value>; return t; } const T & d(); } // source.cpp extern const T Ns::a = <some value>; const T & Ns::d() { static const T t = <some value>; return t; }

    Normalmente no usaría a fiasco de orden de inicialización estática. Por lo que sé, b , d son perfectamente seguros, incluso de subprocesos desde C ++ 11. b no parece ser una buena opción a menos que T tenga un constructor muy barato, lo cual es poco común para los constructores no constexpr. Puedo nombrar una ventaja de c sobre d : ninguna llamada de función (rendimiento en tiempo de ejecución); una ventaja de d sobre c : menos recompilación cuando se cambia el valor de la constante (estas ventajas también se aplican a C y D ). Estoy seguro de que me perdí un montón de razonamiento aquí. Proporcionar otras consideraciones en las respuestas por favor.

Si desea modificar / probar el código anterior, puede usar mis archivos de prueba (solo header.hpp, source.cpp con versiones compilables de los fragmentos de código anteriores y main.cpp que imprime constantes de header.hpp): https://docs.google.com/uc?export=download&id=0B0F-aqLyFk_PVUtSRnZWWnd4Tjg


Creo que no hay una gran diferencia entre las siguientes ubicaciones de declaración:

Esto está mal de muchas maneras.

La primera declaración contamina el espacio de nombres global; Usted ha tomado el nombre de "tv" y nunca más se usó sin la posibilidad de malentendidos. Esto puede causar advertencias de seguimiento, puede causar errores en el enlazador, puede causar todo tipo de confusión a cualquiera que use su encabezado. También puede causar problemas a alguien que no usa su encabezado, al provocar una colisión con otra persona que también usa el nombre de su variable como global.

Este tipo de enfoque no se recomienda en C ++ moderno, pero es ubicuo en C y, por lo tanto, conduce a un gran uso de la palabra clave estática para variables "globales" en un archivo .c (alcance del archivo).

El segundo declara contamina un espacio de nombres; este es un problema mucho menor, ya que los espacios de nombres se renombran libremente y se pueden hacer sin costo alguno. Mientras dos proyectos utilicen su propio espacio de nombres relativamente específico, no se producirán colisiones. En el caso en el que se produzcan dichas colisiones, se puede cambiar el nombre de los espacios de nombres de cada uno para evitar cualquier problema.

Esto es más moderno, al estilo C ++ 03, y C ++ 11 expande esta táctica considerablemente con el cambio de nombre de las plantillas.

El tercer enfoque es una estructura, no una clase; tienen diferencias, especialmente si desea mantener la compatibilidad con C. Los beneficios de un compuesto de ámbito de clase en el ámbito de espacio de nombres; no solo puede encapsular múltiples cosas fácilmente y usar un nombre específico, sino que también puede aumentar la encapsulación mediante métodos y ocultación de información, ampliando enormemente la utilidad de su código. Este es principalmente el beneficio de las clases, independientemente de los beneficios del alcance.

Es casi seguro que no debe usar el primero, a menos que sus funciones y variables sean muy amplias y similares a STL / STD, o su programa sea muy pequeño y no sea probable que se incruste o reutilice.

Veamos ahora sus casos.

  1. El tamaño del constructor, si devuelve una expresión constante, no es importante; Todo el código debería ser ejecutable en tiempo de compilación. Esto significa que la complejidad no es significativa; siempre se compilará en un único valor de retorno constante. Es casi seguro que nunca debes usar C o D; todo lo que hace es hacer que las optimizaciones de constexpr no funcionen. Yo usaría cualquiera de A y B parece más elegante, probablemente una tarea simple sería A, y una expresión constante compleja sería B.

  2. Ninguno de estos es necesariamente seguro para subprocesos; el contenido del constructor determinaría la seguridad de subprocesos y excepciones, y es bastante fácil hacer que cualquiera de estas afirmaciones no sea segura para subprocesos. De hecho, es más probable que A sea seguro para subprocesos; siempre que no se acceda al objeto hasta que se llame a main, debe estar completamente formado; Lo mismo no se puede decir de ninguno de sus otros ejemplos. En cuanto a su análisis de B, en mi experiencia, la mayoría de los constructores (especialmente los de excepción segura) son baratos porque evitan la asignación. En tales casos, es poco probable que haya mucha diferencia entre cualquiera de sus casos.

Le recomendaría encarecidamente que deje de intentar microoptimizaciones como esta y quizás obtenga una comprensión más sólida de los modismos en C ++. Es poco probable que la mayoría de las cosas que intenta hacer aquí resulten en un aumento en el rendimiento.


1

En el caso A hay una diferencia entre el alcance global o del espacio de nombres (vinculación interna) y el alcance de la clase (vinculación externa). Asi que

// header.hpp constexpr T A = <some value>; // internal linkage namespace Nm { constexpr T A = <some value>; } // internal linkage class Cl { public: static constexpr T A = <some value>; }; // not enough!

Considere el siguiente uso:

// user.cpp std::cout << A << Nm::A << Cl::A; // ok std::cout << &A << &Nm::A; // ok std::cout << &Cl::A; // linker error: undefined reference to `Cl::A''

Al colocar la definición Cl::A en source.cpp (además de la declaración Cl::A ) se elimina este error:

// source.cpp constexpr T Cl::A;

El enlace externo significa que siempre habrá una sola instancia de Cl::A Entonces, Cl::A parece ser un muy buen candidato para T grande. Sin embargo, ¿podemos estar seguros de que el fiasco de orden de inicialización estática no se presentaría en este caso? Creo que la respuesta es , porque Cl::A se construye en tiempo de compilación.

He probado A , B , a alternativa con g ++ 4.8.2 y 4.9.0, clang ++ 3.4 en la plataforma GNU / Linux. Los resultados para tres unidades de traducción:

  • A ámbito de clase con definición en source.cpp fue inmune al fiasco y tenía la misma dirección en todas las unidades de traducción, incluso en tiempo de compilación.
  • A en el espacio de nombres o en el ámbito global tenía 3 direcciones diferentes, tanto para una matriz grande como para constexpr const char * A = "A"; (debido a la vinculación interna).
  • B ( std::array<long double, 100> ) en cualquier ámbito tenía 2 direcciones diferentes (la dirección era la misma en 2 unidades de traducción); Además, las 3 direcciones B sugirieron una ubicación de memoria diferente (eran mucho más grandes que otras direcciones). Sospecho que la matriz se copió en la memoria en tiempo de ejecución.
  • a cuando se usaba con los tipos T constexpr , por ejemplo, int , const char * , std::array , Y se inicializaba con la expresión constexpr en source.cpp , era tan bueno como A : inmune al fiasco y tenía la misma dirección en todas las unidades de traducción. Si la constante de constexpr tipo T se inicializa con constexpr , por ejemplo, std::time(nullptr) , y se usa antes de la inicialización, contendrá el valor predeterminado (por ejemplo, 0 para int ). Esto significa que el valor de la constante puede depender del orden de inicialización estática en este caso. Por lo tanto, no inicialice con a valor no constexpr !

La línea de fondo

  1. A mayoría de los casos, prefiero el alcance de clase A para cualquier constante constante, ya que combina seguridad perfecta, simplicidad, ahorro de memoria y rendimiento.
  2. Se debe usar a (inicializado con el valor constexpr en source.cpp !) si el ámbito del espacio de nombres es preferible o si es conveniente evitar la inicialización en header.hpp (para reducir las dependencias y el tiempo de compilación). a tiene una desventaja en comparación con A : se puede usar en expresiones de tiempo de compilación solo en source.cpp y solo después de la inicialización.
  3. B debe usarse para T pequeña en algunos casos: cuando el ámbito del espacio de nombres es preferible o la constante de compilación de la plantilla es necesaria (por ejemplo, pi ). También se puede usar B cuando el valor de la constante rara vez se usa o se usa solo en situaciones excepcionales, por ejemplo, mensajes de error.
  4. Casi nunca se deben usar otras alternativas, ya que rara vez se adaptarían mejor que las 3 formas antes mencionadas.
    • No se debe usar A en el ámbito del espacio de nombres, ya que potencialmente puede llevar a N instancias de constante, por lo tanto, consume el sizeof(T) * N bytes de memoria y causa la falta de memoria caché. Aquí N es igual al número de unidades de traducción que incluyen header.hpp . Como se señaló en esta propuesta , A en el ámbito del espacio de nombres puede violar la ODR si se usa en la función en línea.
    • C podría usarse para T grande ( B suele ser mejor para T pequeña) en 2 escenarios raros: cuando es preferible llamar a la función; cuando el espacio de nombres de ámbito Y la inicialización en el encabezado es preferible.
    • D podría usarse cuando la llamada a la función Y la inicialización en el archivo fuente es preferible.
    • El único defecto de C comparación con A y B : su valor de retorno no se puede usar en expresiones de tiempo de compilación. D sufre de la misma deficiencia y otra: función de penalización del rendimiento en tiempo de ejecución de la llamada (porque no puede estar en línea).

2

Evite utilizar no constexpr debido al fiasco de orden de inicialización estática. Considerar a solo en caso de cuello de botella seguro. De lo contrario, la seguridad es más importante que la pequeña ganancia de rendimiento. b , c y d son mucho más seguros. Sin embargo, c y d tienen 2 requisitos de seguridad:

for (auto f : { todas las funciones parecidas a c y d }) {

  • T constructor T no debe llamar a f porque si la inicialización de la variable local estática entra de manera recursiva en el bloque en el que se inicializa la variable, el comportamiento no está definido. Esto no es difícil de asegurar.
  • Para cada clase X , X::~X llama f y hay un objeto X estáticamente inicializado: X::X debe llamar f . La razón es que, de lo contrario, static const T de f podría construirse después y, por lo tanto, destruirse antes del objeto X global; entonces X::~X causaría UB. Este requisito es mucho más difícil de garantizar que el anterior. Por lo tanto, casi prohíbe las variables locales globales o estáticas con destructores complicados que usan constantes globales. Si el destructor de la variable estáticamente inicializada no es complicado, por ejemplo, utiliza f() para fines de registro, luego coloca f(); En el constructor correspondiente se garantiza la seguridad.

}

Nota: estos 2 requisitos no se aplican a C y D :

  • la llamada recursiva a f no compilaría;
  • static constexpr T constantes static constexpr T en C y D se construyen en tiempo de compilación, antes de que se construya cualquier variable no trivial, por lo que se destruyen después de la destrucción de todas las variables no triviales (los destructores se llaman en orden inverso).

Nota 2: C ++ FAQ sugiere una implementación diferente de c y d , que no impone el segundo requisito de seguridad. Sin embargo, en este caso, la constante estática nunca se destruye, lo que puede interferir con la detección de fugas de memoria, por ejemplo, el diagnóstico de Valgrind . Deben evitarse las pérdidas de memoria, por benignas que sean. Por lo tanto, estas versiones modificadas de c y d deben usarse solo en situaciones excepcionales.

Una alternativa más a considerar aquí es una constante con enlace interno:

// header.hpp namespace Ns { namespace { const T a1 = <some value>; } }

Este enfoque tiene el mismo gran inconveniente que A en el ámbito del espacio de nombres: la vinculación interna puede crear tantas copias de a1 como la cantidad de unidades de traducción que incluyen header.hpp . También puede violar la ODR de la misma manera que A Sin embargo, dado que otras opciones para no constexpr no son tan buenas como para constantes constexpr , esta alternativa podría tener un uso poco frecuente. PERO : esta "solución" todavía es propensa al fiasco de orden de inicialización estática en caso de que se use a1 en una función pública que a su vez se usa para la inicialización de un objeto global. Por lo tanto, la introducción de un enlace interno no resuelve el problema, simplemente lo oculta, lo hace menos probable, probablemente más difícil de localizar y solucionar.

La línea de fondo

  • c proporciona el mejor rendimiento y ahorra memoria porque facilita la reutilización de una instancia de T y puede estar en línea, por lo que debe utilizarse en la mayoría de los casos.
  • d es tan bueno como c para ahorrar memoria, pero es peor para el rendimiento, ya que nunca estaría en línea. Sin embargo d puede utilizarse para reducir el tiempo de compilación.
  • considere b para tipos pequeños o para constantes poco utilizadas (en el caso de constantes raramente usadas, su definición se puede mover a source.cpp para evitar la recompilación en el cambio). Además, b es la única solución si no se pueden satisfacer los requisitos de seguridad para c . Definitivamente, b no es bueno para T grande si la constante se usa a menudo, porque la constante se debe construir cada vez que se llama a b .

Nota: hay otro problema de compilación en tiempo de funciones y variables en línea inicializadas en header.hpp . Si la definición de la constante depende de otra constante declarada en un encabezado diferente bad.h , y el encabezado bad.h no debe incluirse en header.hpp , entonces D , d , a y b modificado (con la definición movida a source.cpp ) son los siguientes solo alternativas


No mencionaste una opción importante:

namespace { const T t = .....; };

Ahora no hay problemas de colisión de nombres.

Esto no es apropiado si T es algo que solo quieres construir una vez. Pero tener un gran objeto "global", constante o no, es algo que realmente desea evitar. Rompe la encapsulación y también introduce el fiasco de orden de inicialización estática en su código.

Nunca he tenido la necesidad de un gran objeto externo de const. Si necesito, por ejemplo, una tabla de búsqueda grande y codificada, entonces escribo una función (tal vez como miembro de la clase) que busca la tabla; y la tabla es local a la unidad con la implementación de esa función.

En mi código que parece requerir un gran objeto global no constante, en realidad tengo una función,

namespace MyStuff { T &get_global_T(); }

que construye el objeto en el primer uso. (En realidad, el objeto en sí está oculto en una unidad, y T es una clase auxiliar que especifica una interfaz; por lo tanto, puedo alterar los detalles del objeto y no alterar ningún código que lo esté usando).