c++ - matematicas - ¿Por qué las expresiones constantes tienen una exclusión para el comportamiento indefinido?
constantes y variables matematicas (3)
Estaba investigando lo que está permitido en una expresión constante central * , que se trata en la sección 5.19
Expresiones constantes, párrafo 2 del borrador del estándar C ++ que dice:
Una expresión condicional es una expresión constante central a menos que implique una de las siguientes como subexpresiones potencialmente evaluadas (3.2), pero subexpresiones de operaciones lógicas AND (5.14), lógicas OR (5.15) y condicionales (5.16) que no se evalúan no se consideran [Nota: Un operador sobrecargado invoca una función.-nota final]:
y enumera las exclusiones en las viñetas que siguen e incluye ( énfasis mío ):
- una operación que tendría un comportamiento indefinido [Nota: incluyendo, por ejemplo, desbordamiento de entero con signo (Cláusula 5), cierta aritmética de puntero (5.7), división por cero (5.6), o ciertas operaciones de cambio (5.8) - nota final];
¿ Huh ? ¿Por qué las expresiones constantes necesitan esta cláusula para cubrir el comportamiento indefinido ? ¿Hay algo especial acerca de las expresiones constantes que requiere un comportamiento indefinido para tener una excepción especial en las exclusiones?
¿Tener esta cláusula nos da ventajas o herramientas que no tendríamos sin ella?
Como referencia, esta parece la última revisión de la propuesta de expresiones constantes generalizadas .
Cuando hablamos de un comportamiento indefinido , es importante recordar que el Estándar deja el comportamiento indefinido para estos casos. No prohíbe a las implementaciones hacer garantías más sólidas. Por ejemplo, algunas implementaciones pueden garantizar que el desbordamiento de entero con signo se ajuste, mientras que otras pueden garantizar la saturación.
Exigir a los compiladores que procesen expresiones constantes que impliquen un comportamiento indefinido limitaría las garantías que una implementación podría hacer, restringiéndolas a producir algún valor sin efectos secundarios (lo que el estándar llama valor indeterminado ). Eso excluye muchas de las garantías extendidas que se encuentran en el mundo real.
Por ejemplo, alguna implementación o estándar complementario (es decir, POSIX) puede definir el comportamiento de la división integral por cero para generar una señal. Ese es un efecto secundario que se perdería si la expresión se calculara en tiempo de compilación en su lugar.
Por lo tanto, estas expresiones se rechazan en tiempo de compilación para evitar la pérdida de efectos secundarios en el entorno de ejecución.
Hay otro punto para excluir el comportamiento indefinido de las expresiones constantes: las expresiones constantes deberían, por definición, ser evaluadas por el compilador en tiempo de compilación. Permitir que una expresión constante invoque un comportamiento indefinido le permitiría al compilador mostrar un comportamiento indefinido. Y un compilador que formatea su disco duro porque compila un código maligno no es algo que desee tener.
La redacción es en realidad el tema del informe de defectos # 1313 que dice:
Los requisitos para expresiones constantes actualmente no excluyen, pero deberían, las expresiones que tienen un comportamiento indefinido, como la aritmética del puntero cuando los punteros no apuntan a elementos de la misma matriz.
La resolución es la redacción actual que tenemos ahora, por lo que esta fue claramente la intención, entonces, ¿qué herramientas nos da esto?
Veamos qué pasa cuando tratamos de crear una variable constexpr con una expresión que contenga un comportamiento indefinido , usaremos clang
para todos los ejemplos siguientes. Este código ( verlo en vivo ):
constexpr int x = std::numeric_limits<int>::max() + 1 ;
produce el siguiente error:
error: constexpr variable ''x'' must be initialized by a constant expression
constexpr int x = std::numeric_limits<int>::max() + 1 ;
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: value 2147483648 is outside the range of representable values of type ''int''
constexpr int x = std::numeric_limits<int>::max() + 1 ;
^
Este código ( verlo en vivo ):
constexpr int x = 1 << 33 ; // Assuming 32-bit int
produce este error:
error: constexpr variable ''x'' must be initialized by a constant expression
constexpr int x = 1 << 33 ; // Assuming 32-bit int
^ ~~~~~~~
note: shift count 33 >= width of type ''int'' (32 bits)
constexpr int x = 1 << 33 ; // Assuming 32-bit int
^
y este código que tiene un comportamiento indefinido en una función constexpr:
constexpr const char *str = "Hello World" ;
constexpr char access( int index )
{
return str[index] ;
}
int main()
{
constexpr char ch = access( 20 ) ;
}
produce este error:
error: constexpr variable ''ch'' must be initialized by a constant expression
constexpr char ch = access( 20 ) ;
^ ~~~~~~~~~~~~
note: cannot refer to element 20 of array of 12 elements in a constant expression
return str[index] ;
^
Bueno, eso es útil, el compilador puede detectar un comportamiento indefinido en constexpr , o al menos lo que cree que no está definido . Tenga en cuenta que gcc
comporta de la misma manera, excepto en el caso de un comportamiento indefinido con desplazamiento a la derecha e izquierda, por lo general, gcc
produce una advertencia en estos casos, pero aún ve la expresión como constante.
Podemos usar esta funcionalidad a través de SFINAE para detectar si una expresión adicional causaría desbordamiento, el siguiente ejemplo artificial fue inspirado por la respuesta inteligente de dyp aquí :
#include <iostream>
#include <limits>
template <typename T1, typename T2>
struct addIsDefined
{
template <T1 t1, T2 t2>
static constexpr bool isDefined()
{
return isDefinedHelper<t1,t2>(0) ;
}
template <T1 t1, T2 t2, decltype( t1 + t2 ) result = t1+t2>
static constexpr bool isDefinedHelper(int)
{
return true ;
}
template <T1 t1, T2 t2>
static constexpr bool isDefinedHelper(...)
{
return false ;
}
};
int main()
{
std::cout << std::boolalpha <<
addIsDefined<int,int>::isDefined<10,10>() << std::endl ;
std::cout << std::boolalpha <<
addIsDefined<int,int>::isDefined<std::numeric_limits<int>::max(),1>() << std::endl ;
std::cout << std::boolalpha <<
addIsDefined<unsigned int,unsigned int>::isDefined<std::numeric_limits<unsigned int>::max(),std::numeric_limits<unsigned int>::max()>() << std::endl ;
}
que da como resultado ( verlo en vivo ):
true
false
true
No es evidente que el estándar requiera este comportamiento, pero aparentemente este comentario de Howard Hinnant indica que sí lo es:
[...] y también es constexpr, lo que significa que UB está atrapado en tiempo de compilación
Actualizar
De alguna manera me olvidé del Issue 695 Errores de cálculo del tiempo de compilación en las funciones constexpr, que gira en torno a la redacción de la sección 5
párrafo 4, que solía decir (el énfasis es mío en el futuro ):
Si durante la evaluación de una expresión, el resultado no está matemáticamente definido o no está en el rango de valores representables para su tipo, el comportamiento es indefinido, a menos que tal expresión aparezca cuando se requiere una expresión de constante integral (5.19 [expr.const] ), en cuyo caso el programa está mal formado .
y continúa diciendo:
pretende ser una circunlocución estándar aceptable para "evaluados en tiempo de compilación", un concepto que no está directamente definido por el estándar. No está claro que esta formulación cubra adecuadamente las funciones constexpr.
y una nota posterior dice:
[...] Hay una tensión entre querer diagnosticar errores en tiempo de compilación versus no diagnosticar errores que realmente no ocurrirán en tiempo de ejecución. [...] El consenso del CWG fue que una expresión como 1/0 debería simplemente ser considerado no constante; cualquier diagnóstico sería el resultado del uso de la expresión en un contexto que requiere una expresión constante.
lo que si estoy leyendo correctamente confirma que la intención era poder diagnosticar el comportamiento indefinido en tiempo de compilación en el contexto que requiere una expresión constante.
Definitivamente no podemos decir que este fue el intento, pero sí lo sugiere fuertemente. La diferencia en cómo clang
y gcc
tratan los cambios indefinidos deja espacio para la duda.
Archivé un informe de error de gcc: el comportamiento indefinido de cambio a la derecha e izquierda no es un error en un constexto . Aunque parece que esto es conforme, rompe SFINAE y podemos ver a partir de mi respuesta a ¿Es una extensión de compilación conforme tratar las funciones de biblioteca estándar no constexpr como constexpr? esa divergencia en la implementación observable para los usuarios de SFINAE parece indeseable para el comité.