c++ - and - Detección en tiempo de compilación o en tiempo de ejecución dentro de una función constexpr
constexpr in c++ (2)
Pensé que esta sería una manera de asegurar que MyMin solo pueda usarse con constantes evaluadas en tiempo de compilación, y esto aseguraría que el compilador nunca permitiría su ejecución en tiempo de ejecución
Sí; Hay una manera.
Y funciona con C ++ 11 también.
Buscando en Google, he encontrado una extraña forma de envenenamiento (por Scott Schurr): en resumen, lo siguiente
extern int no_symbol;
constexpr float MyMin (float a, float b)
{
return a != a ? throw (no_symbol)
: (a < b ? a : b) ;
}
int main()
{
constexpr float m0 { MyMin(2.0f, 3.0f) }; // OK
float f1 { 2.0f };
float m1 { MyMin(f1, 3.0f) }; // linker error: undefined "no_symbol"
}
Si entiendo bien, la idea subyacente es que si se ejecuta el tiempo de compilación de throw(no_symbol)
nunca se usa throw(no_symbol)
( a != a
es false), por lo que no hay necesidad de usar no_symbol
que se declara extern
pero sí nunca definido (y throw()
no se puede utilizar el tiempo de compilación).
Si utiliza el MyMin()
ejecución de throw(no_symbol)
se compila throw(no_symbol)
y no_symbol
da un error en la fase de enlace.
En términos más generales, hay una propuesta (siempre de Scott Schurr) pero no estoy al tanto de las implementaciones.
--- EDITAR ---
Como señaló TC (¡gracias!), Esta solución funciona (si funciona y cuándo funciona) solo porque el compilador no optimiza en un punto para comprender que a != a
es siempre falso.
En particular, MyMin()
funciona (sin buenas optimizaciones) porque, en el ejemplo, estamos trabajando con números flotantes y a != a
puede ser cierto si a
es NaN, por lo que es más difícil para un compilador detectar que el throw()
parte es inútil. Si MyMin()
es una función para enteros, el cuerpo puede escribirse (con prueba float(a) != float(a)
para intentar obstruir las optimizaciones del compilador) como
constexpr int MyMin (int a, int b)
{
return float(a) != float(a) ? throw (no_symbol)
: (a < b ? a : b) ;
}
pero no es una solución real para una función donde no hay un caso de error "natural" que se pueda lanzar.
Cuando es un caso de error natural que debería dar error (compilación o ejecución), es diferente: el compilador no puede optimizar y el truco funciona.
Ejemplo: si MyMin()
devuelve el valor mínimo entre b
pero a
y b
deben ser diferentes o MyMin()
debe dar un error de compilación (no es un gran ejemplo ... Lo sé), por lo que
constexpr float MyMin (float a, float b)
{
return a != b ? throw (no_symbol)
: (a < b ? a : b) ;
}
funciona porque el compilador no puede optimizar a != b
y debe compilar (dando un error al vinculador) la parte throw()
.
Estaba emocionado cuando se introdujo constexpr en C ++ 11, pero desafortunadamente hice suposiciones optimistas sobre su utilidad. Asumí que podríamos usar constexpr en cualquier lugar para capturar constantes de tiempo de compilación literales o cualquier resultado de constexpr de una constante de tiempo de compilación literal, incluyendo algo como esto:
constexpr float MyMin(constexpr float a, constexpr float b) { return a<b?a:b; }
Debido a que calificar el tipo de retorno de una función solo como constexpr no limita su uso al tiempo de compilación, y también debe ser invocable en el tiempo de ejecución, pensé que esta sería una manera de asegurar que MyMin solo pueda usarse con constantes evaluadas en tiempo de compilación , y esto aseguraría que el compilador nunca permitiría su ejecución en tiempo de ejecución, permitiéndome escribir una versión alternativa más compatible con el tiempo de ejecución de MyMin, idealmente con el mismo nombre que usa un _mm_min_ss intrínseco, asegurando que el compilador no genere ramificaciones en tiempo de ejecución código. Desafortunadamente, los parámetros de la función no pueden ser constexpr, por lo que parece que esto no se puede hacer, a menos que sea posible algo como esto:
constexpr float MyMin(float a, float b)
{
#if __IS_COMPILE_TIME__
return a<b?a:b;
#else
return _mm_cvtss_f32(_mm_min_ss(_mm_set_ss(a),_mm_set_ss(b)));
#endif
}
Tengo serias dudas de que MSVC ++ tenga algo como esto, pero esperaba que GCC o clang tuvieran al menos algo para lograrlo, por muy poco elegante que sea.
Por supuesto, el ejemplo que presenté fue muy simplista, pero si puedes usar tu imaginación, hay muchos casos en los que podrías sentirte libre de hacer algo como hacer un uso extensivo de sentencias de bifurcación dentro de una función que sabes que solo puede ejecutar en tiempo de compilación. porque si se ejecutara en tiempo de ejecución, el rendimiento se vería comprometido.
Es posible detectar si una expresión de llamada de función dada es una expresión constante y, por lo tanto, seleccionar entre dos implementaciones diferentes. Requiere C ++ 14 para el lambda genérico que se usa a continuación.
(Esta respuesta hizo crecer esta respuesta de @Yakk a una pregunta que hice el año pasado).
No estoy seguro de hasta dónde estoy empujando el Estándar. Esto se prueba en el Clang 3.9, pero hace que g ++ 6.2 dé un "error interno del compilador". Enviaré un informe de error la próxima semana (¡si nadie más lo hace primero!)
Este primer paso es mover la implementación constexpr
a una struct
como un método constexpr static
. Más simplemente, podría dejar el constexpr
actual constexpr
como está y llamarlo desde un método constexpr static
de una nueva struct
.
struct StaticStruct {
static constexpr float MyMin_constexpr (float a, float b) {
return a<b?a:b;
}
};
Además, define esto (aunque parezca inútil!):
template<int>
using Void = void;
La idea básica es que Void<i>
requiere que i
sea una expresión constante. Más precisamente, esta siguiente lambda tendrá sobrecargas adecuadas solo en ciertas circunstancias:
auto l = [](auto ty)-> Void<(decltype(ty):: MyMin_constexpr(1,3) ,0)>{};
/------------------/
testing if this
expression is a
constant expression.
Podemos llamar a l
solo si el argumento ty
es de tipo StaticStruct
y si nuestra expresión de interés ( MyMin_constexpr(1,3)
) es una expresión constante. Si reemplazamos 1
o 3
con argumentos no constantes, entonces la lambda genérica perderá el método a través de SFINAE.
Por lo tanto, las siguientes dos pruebas son equivalentes:
- ¿
StaticStruct::MyMin_constexpr(1,3)
una expresión constante ?- ¿Se me puede llamar por
l(StaticStruct{})
?
Es tentador simplemente eliminar auto ty
decltype(ty)
auto ty
y el tipo de decltype(ty)
de la lambda anterior. Pero eso dará un error difícil (en el caso no constante) en lugar de un fallo de sustitución agradable. Por lo tanto, utilizamos auto ty
para obtener un error de sustitución (que podemos detectar de manera útil) en lugar de un error.
Este código siguiente es una cosa sencilla para devolver std:true_type
si y solo si f
(nuestro lambda genérico) puede llamarse con a
( StaticStruct
):
template<typename F,typename A>
constexpr
auto
is_a_constant_expression(F&& f, A&& a)
-> decltype( ( std::forward<F>(f)(std::forward<A>(a)) , std::true_type{} ) )
{ return {}; }
constexpr
std::false_type is_a_constant_expression(...)
{ return {}; }
A continuación, una demostración de su uso:
int main() {
{
auto should_be_true = is_a_constant_expression(
[](auto ty)-> Void<(decltype(ty):: MyMin_constexpr(1,3) ,0)>{}
, StaticStruct{});
static_assert( should_be_true ,"");
}
{
float f = 3; // non-constexpr
auto should_be_false = is_a_constant_expression(
[](auto ty)-> Void<(decltype(ty):: MyMin_constexpr(1,f) ,0)>{}
, StaticStruct{});
static_assert(!should_be_false ,"");
}
}
Para resolver su problema original directamente, primero podríamos definir una macro para guardar la repetición:
(No he probado esta macro, disculpas por cualquier error tipográfico).
#define IS_A_CONSTANT_EXPRESSION( EXPR ) /
is_a_constant_expression( /
[](auto ty)-> Void<(decltype(ty):: /
EXPR ,0)>{} /
, StaticStruct{})
En esta etapa, tal vez podrías simplemente hacer:
#define MY_MIN(...) /
IS_A_CONSTANT_EXPRESSION( MyMin_constexpr(__VA_ARGS__) ) ? /
StaticStruct :: MyMin_constexpr( __VA_ARGS__ ) : /
MyMin_runtime ( __VA_ARGS__ )
o, si no confías en tu compilador para optimizar std::true_type
y std::false_type
través de ?:
, entonces quizás:
constexpr
float MyMin(std::true_type, float a, float b) { // called if it is a constant expression
return StaticStruct:: MyMin_constexpr(a,b);
}
float MyMin(std::false_type, float , float ) { // called if NOT a constant expression
return MyMin_runtime(a,b);
}
con esta macro en su lugar:
#define MY_MIN(...) /
MyMin( IS_A_CONSTANT_EXPRESSION(MyMin_constexpr(__VA_ARGS__)) /
, __VA_ARGS__)