c++ c++11 constexpr

c++ - ¿Por qué necesitamos marcar funciones como constexpr?



c++11 (4)

Evitar que el código del cliente espere más de lo que prometes

Digamos que estoy escribiendo una biblioteca y tengo una función allí que actualmente devuelve una constante:

awesome_lib.h:

inline int f() { return 4; }

Si no se requirió constexpr , usted, como autor del código del cliente, podría desaparecer y hacer algo como esto:

client_app.cpp:

#include <awesome_lib.h> int my_array[f()];

Entonces debería cambiar f() para decir devolver el valor de un archivo de configuración, su código de cliente se rompería, pero no tendría idea de que me arriesgué a romper su código. De hecho, podría ser solo cuando tengas algún problema de producción y veas a recompilar que encuentras este problema adicional frustrando tu reconstrucción.

Al cambiar solo la implementación de f() , habría cambiado efectivamente el uso que se podría hacer de la interfaz .

En cambio, C ++ 11 en adelante proporciona constexpr así que puedo denotar que el código del cliente puede tener una expectativa razonable de que la función siga siendo un constexpr , y usarla como tal. Conozco y apruebo dicho uso como parte de mi interfaz. Al igual que en C ++ 03, el compilador continúa garantizando que el código del cliente no está construido para depender de otras funciones que no sean constexpr para evitar el escenario de "dependencia no deseada / desconocida" anterior; eso es más que documentación: es la aplicación del tiempo de compilación.

Es digno de mención que esto continúa la tendencia C ++ de ofrecer mejores alternativas para los usos tradicionales de las macros de preprocesador (considere #define F 4 , y cómo el programador cliente sabe si el programador lib considera que es un juego justo cambiar para decir #define F config["f"] ), con sus conocidos" males ", como estar fuera del sistema de ámbito del espacio de nombres / clase del idioma.

¿Por qué no hay un diagnóstico para las funciones "obviamente" nunca-const?

Creo que la confusión aquí se debe a que no se garantiza proactivamente que exista ningún conjunto de argumentos cuyo resultado en realidad sea el tiempo de compilación. Más bien, requiere que el programador se responsabilice de eso (de lo contrario, §7.1.5 / 5 en el Standard considera que el programa está mal formado, pero no requiere que el compilador emita un diagnóstico). Sí, es desafortunado, pero no elimina la utilidad anterior de constexpr .

Entonces, tal vez la pregunta no debería ser "¿cuál es el punto de constexpr ", sino "por qué puedo compilar una función constexpr que en realidad nunca puede devolver un valor constante?". Respuesta: porque habría una necesidad de análisis de ramificación exhaustivo que podría implicar cualquier cantidad de combinaciones. Puede ser excesivamente costoso en tiempo de compilación y / o memoria, incluso más allá de la capacidad de cualquier hardware imaginable, diagnosticar. Además, incluso cuando es práctico tener que diagnosticar tales casos con precisión es una nueva y completa lata de gusanos para los escritores de compiladores (que ya están lo suficientemente ocupados con la implementación de C ++ 11). También habría implicaciones para el programa, como la definición de funciones llamadas desde dentro de la función constexpr que deben estar visibles cuando se realizó la validación (y las funciones que funcionan llamadas, etc.).

Mientras tanto, la falta de constexpr continúa prohibiendo el uso como un valor constante: el rigor está en el lado sin constexpr . Eso es útil como se ilustró anteriormente.

Comparación con las funciones miembro non-`const`

  • constexpr impide int x[f()] mientras que la falta de const previene const X x; xf(); const X x; xf(); - Ambos aseguran que el código del cliente no codifica la dependencia no deseada

  • en ambos casos, no le gustaría que el compilador determine const[expr] -ness automáticamente :

    • no querría que el código del cliente llamara a una función miembro en un objeto const cuando ya puede anticipar que la función evolucionará para modificar el valor observable, rompiendo el código del cliente

    • no querría que se utilizara un valor como parámetro de plantilla o dimensión de matriz si ya anticipaba que posteriormente se determinaría en tiempo de ejecución

  • difieren en que el compilador impone el uso const de otros miembros dentro de una función miembro miembro, pero no impone un resultado constante en tiempo de compilación con constexpr (debido a las limitaciones prácticas del compilador)

C ++ 11 permite que las funciones declaradas con el especificador constexpr se usen en expresiones constantes tales como argumentos de plantilla. Existen requisitos estrictos sobre lo que se permite que sea constexpr ; esencialmente, tal función encapsula solo una subexpresión y nada más. (Editar: esto está relajado en C ++ 14 pero la pregunta es válida).

¿Por qué requieren la palabra clave en absoluto? ¿Qué se gana?

Ayuda a revelar la intención de una interfaz, pero doesn''t valida esa intención, al garantizar que una función se pueda usar en expresiones constantes. Después de escribir una función constexpr , un programador debe:

  1. Escriba un caso de prueba o asegúrese de que se use en una expresión constante.
  2. Documente qué valores de parámetros son válidos en un contexto de expresión constante.

Contrariamente a la intención reveladora, decorar las funciones con constexpr puede agregar una falsa sensación de seguridad ya que las restricciones sintácticas tangenciales se verifican ignorando la restricción semántica central.

En resumen: ¿ constexpr algún efecto indeseable en el lenguaje si constexpr en declaraciones de funciones fuera meramente opcional? ¿O habría algún efecto en algún programa válido?


Cuando presioné a Richard Smith, un autor de Clang, me explicó:

La palabra clave constexpr tiene utilidad.

Afecta cuando se crea una instancia de especialización de plantilla de función (las especializaciones de plantilla de función constexpr pueden necesitar una instancia si se llaman en contextos no evaluados, lo mismo no es cierto para funciones no constexpr ya que una llamada a uno nunca puede ser parte de una constante expresión). Si elimináramos el significado de la palabra clave, tendríamos que instanciar un grupo de especializaciones más temprano, en caso de que la llamada sea una expresión constante.

Reduce el tiempo de compilación, al limitar el conjunto de llamadas a funciones que las implementaciones requieren para evaluar durante la traducción. (Esto es importante para contextos en los que se requieren implementaciones para probar la evaluación de expresión constante, pero no es un error si falla dicha evaluación, en particular, los inicializadores de objetos de duración de almacenamiento estático).

Todo esto no pareció convincente al principio, pero si trabajas a través de los detalles, las cosas se desenredan sin constexpr . No es necesario crear una instancia de una función hasta que se use ODR, lo que significa esencialmente que se usa en tiempo de ejecución. Lo especial de constexpr funciones de constexpr es que pueden violar esta regla y requerir una instanciación de todos modos.

La instanciación de funciones es un procedimiento recursivo. La instancia de una función da como resultado la creación de instancias de las funciones y clases que utiliza, independientemente de los argumentos para cualquier llamada en particular.

Si algo salió mal al crear este árbol de dependencias (potencialmente a un costo significativo), sería difícil tragar el error. Además, la creación de instancias de plantillas de clase puede tener efectos colaterales de tiempo de ejecución.

Dada una llamada de función en tiempo de compilación dependiente del argumento en una firma de función, la resolución de sobrecarga puede incurrir en la creación de instancias de definiciones de función meramente auxiliares a las del conjunto de sobrecarga, incluidas las funciones que ni siquiera reciben una llamada. Tales instancias pueden tener efectos secundarios, incluida la mala formación y el comportamiento en el tiempo de ejecución.

Es un caso de esquina para estar seguro, pero pueden ocurrir cosas malas si no requiere que las personas opten por funciones de constexpr .


Podemos vivir sin constexpr , pero en ciertos casos hace que el código sea más fácil e intuitivo.
Por ejemplo, tenemos una clase que declara una matriz con cierta longitud de referencia:

template<typename T, size_t SIZE> struct MyArray { T a[SIZE]; };

Convencionalmente podrías declarar MyArray como:

int a1[100]; MyArray<decltype(*a1), sizeof(a1)/sizeof(decltype(a1[0]))> obj;

Ahora mira cómo funciona con constexpr :

template<typename T, size_t SIZE> constexpr size_t getSize (const T (&a)[SIZE]) { return SIZE; } int a1[100]; MyArray<decltype(*a1), getSize(a1)> obj;

En resumen, cualquier función (por ejemplo, getSize(a1) ) puede usarse como argumento de plantilla solo si el compilador lo reconoce como constexpr .

constexpr también se usa para verificar la lógica negativa. Asegura que un objeto dado está en tiempo de compilación. Aquí está el link referencia, por ejemplo

int i = 5; const int j = i; // ok, but `j` is not at compile time constexprt int k = i; // error


Sin la palabra clave, el compilador no puede diagnosticar errores. El compilador no podría decirle que la función es inválida sintácticamente como constexpr . Aunque dijiste que esto proporciona una "falsa sensación de seguridad", creo que es mejor recoger estos errores lo antes posible.