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
impideint x[f()]
mientras que la falta deconst
previeneconst X x; xf();
const X x; xf();
- Ambos aseguran que el código del cliente no codifica la dependencia no deseadaen 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 clienteno 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 conconstexpr
(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:
- Escriba un caso de prueba o asegúrese de que se use en una expresión constante.
- 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.