informacion - ¿Cuándo debería usar la capacidad constexpr en C++ 11?
for c++ 17 (13)
Introducción
constexpr
no se introdujo como una forma de decirle a la implementación que algo se puede evaluar en un contexto que requiere una expresión constante ; Las implementaciones conformes han podido probar esto antes de C ++ 11.
Algo que una implementación no puede probar es la intención de un determinado fragmento de código:
- ¿Qué es lo que el desarrollador quiere expresar con esta entidad?
- ¿Deberíamos permitir ciegamente que el código se use en una expresión constante , simplemente porque funciona?
¿Qué sería el mundo sin constexpr
?
Digamos que está desarrollando una biblioteca y se da cuenta de que desea poder calcular la suma de cada entero en el intervalo (0,N]
.
int f (int n) {
return n > 0 ? n + f (n-1) : n;
}
La falta de intencion
Un compilador puede probar fácilmente que la función anterior es invocable en una expresión constante si el argumento pasado es conocido durante la traducción; pero no ha declarado esto como una intención, simplemente sucedió.
Ahora viene alguien más, lee tu función, hace el mismo análisis que el compilador; " ¡Oh, esta función es utilizable en una expresión constante!" , y escribe la siguiente pieza de codigo.
T arr[f(10)]; // freakin'' magic
La optimizacion
Usted, como desarrollador de bibliotecas "asombroso" , decide que f
debe almacenar el resultado cuando se lo invoca; ¿Quién querría calcular el mismo conjunto de valores una y otra vez?
int func (int n) {
static std::map<int, int> _cached;
if (_cached.find (n) == _cached.end ())
_cached[n] = n > 0 ? n + func (n-1) : n;
return _cached[n];
}
El resultado
Al introducir su optimización tonta, simplemente rompió cada uso de su función que resultó estar en un contexto donde se requería una expresión constante .
Nunca prometió que la función era utilizable en una expresión constante , y sin constexpr
no habría forma de proporcionar tal promesa.
Entonces, ¿por qué necesitamos constexpr
?
El uso principal de constexpr es declarar la intención .
Si una entidad no está marcada como constexpr
, nunca fue diseñada para usarse en una expresión constante ; e incluso si lo es, confiamos en el compilador para diagnosticar dicho contexto (porque no tiene en cuenta nuestra intención).
Me parece que tener una "función que siempre devuelve 5" rompe o diluye el significado de "llamar a una función". Debe haber una razón, o una necesidad de esta capacidad, o no estaría en C ++ 11. ¿Por qué está ahí?
// preprocessor.
#define MEANING_OF_LIFE 42
// constants:
const int MeaningOfLife = 42;
// constexpr-function:
constexpr int MeaningOfLife () { return 42; }
Me parece que si escribo una función que devuelve un valor literal y llego a una revisión de código, alguien me dirá que debería declarar un valor constante en lugar de escribir el retorno 5.
Acabo de comenzar a cambiar un proyecto a c ++ 11 y se encontró con una situación perfectamente buena para constexpr que limpia métodos alternativos para realizar la misma operación. El punto clave aquí es que solo puede colocar la función en la declaración de tamaño de matriz cuando se declara constexpr. Hay una serie de situaciones en las que puedo ver que esto es muy útil para avanzar con el área del código en el que estoy involucrado.
constexpr size_t GetMaxIPV4StringLength()
{
return ( sizeof( "255.255.255.255" ) );
}
void SomeIPFunction()
{
char szIPAddress[ GetMaxIPV4StringLength() ];
SomeIPGetFunction( szIPAddress );
}
Del discurso de Stroustrup en "Going Native 2012":
template<int M, int K, int S> struct Unit { // a unit in the MKS system
enum { m=M, kg=K, s=S };
};
template<typename Unit> // a magnitude with a unit
struct Value {
double val; // the magnitude
explicit Value(double d) : val(d) {} // construct a Value from a double
};
using Speed = Value<Unit<1,0,-1>>; // meters/second type
using Acceleration = Value<Unit<1,0,-2>>; // meters/second/second type
using Second = Unit<0,0,1>; // unit: sec
using Second2 = Unit<0,0,2>; // unit: second*second
constexpr Value<Second> operator"" s(long double d)
// a f-p literal suffixed by ‘s’
{
return Value<Second> (d);
}
constexpr Value<Second2> operator"" s2(long double d)
// a f-p literal suffixed by ‘s2’
{
return Value<Second2> (d);
}
Speed sp1 = 100m/9.8s; // very fast for a human
Speed sp2 = 100m/9.8s2; // error (m/s2 is acceleration)
Speed sp3 = 100/9.8s; // error (speed is m/s and 100 has no unit)
Acceleration acc = sp1/0.5s; // too fast for a human
Es útil para algo como
// constants:
const int MeaningOfLife = 42;
// constexpr-function:
constexpr int MeaningOfLife () { return 42; }
int some_arr[MeaningOfLife()];
Vincule esto con una clase de rasgos o similar y se vuelve bastante útil.
Otro uso (aún no mencionado) es constexpr
constructores. Esto permite crear constantes de tiempo de compilación que no tienen que inicializarse durante el tiempo de ejecución.
const std::complex<double> meaning_of_imagination(0, 42);
Combine eso con los literales definidos por el usuario y tendrá soporte completo para las clases definidas por el usuario literal.
3.14D + 42_i;
Por lo que he leído, la necesidad de constexpr proviene de un problema en la metaprogramación. Las clases de rasgos pueden tener constantes representadas como funciones, piense: numeric_limits :: max (). Con constexpr, esos tipos de funciones se pueden usar en metaprogramación, o como límites de matriz, etc., etc.
Otro ejemplo de la parte superior de mi cabeza sería que para las interfaces de clase, es posible que desee que los tipos derivados definan sus propias constantes para alguna operación.
Editar:
Después de buscar en SO, parece que otros han dado some examples de lo que podría ser posible con constexprs.
Puede habilitar algunas nuevas optimizaciones. tradicionalmente, const
es una sugerencia para el sistema de tipos, y no se puede utilizar para la optimización (por ejemplo, una función miembro const
puede const_cast
y modificar el objeto de todas formas, legalmente, por lo que no se puede confiar en la const
para la optimización).
constexpr
significa que la expresión realmente es constante, siempre que las entradas a la función sean const. Considerar:
class MyInterface {
public:
int GetNumber() const = 0;
};
Si esto se expone en algún otro módulo, el compilador no puede confiar en que GetNumber()
no devolverá valores diferentes cada vez que se llame, incluso de manera consecutiva sin llamadas no constantes entre, porque const
podría haber sido descartado en el implementación. (Obviamente, cualquier programador que hizo esto debería recibir un disparo, pero el lenguaje lo permite, por lo tanto, el compilador debe cumplir con las reglas).
Añadiendo constexpr
:
class MyInterface {
public:
constexpr int GetNumber() const = 0;
};
El compilador ahora puede aplicar una optimización donde el valor de retorno de GetNumber()
se almacena en caché y eliminar las llamadas adicionales a GetNumber()
, porque constexpr
es una garantía más sólida de que el valor de retorno no cambiará.
Solía haber un patrón con metaprogramación:
template<unsigned T>
struct Fact {
enum Enum {
VALUE = Fact<T-1>*T;
};
};
template<>
struct Fact<1u> {
enum Enum {
VALUE = 1;
};
};
// Fact<10>::VALUE is known be a compile-time constant
Creo que se introdujo constexpr
para permitirle escribir tales construcciones sin la necesidad de plantillas y construcciones extrañas con especialización, SFINAE y demás, pero exactamente como escribiría una función de tiempo de ejecución, pero con la garantía de que el resultado se determinará en tiempo de compilación.
Sin embargo, tenga en cuenta que:
int fact(unsigned n) {
if (n==1) return 1;
return fact(n-1)*n;
}
int main() {
return fact(10);
}
¡Compila esto con g++ -O3
y verás que el fact(10)
está efectivamente evaluado en tiempo de compilación!
Un compilador compatible con VLA (por lo tanto, un compilador de C en modo C99 o compilador de C ++ con extensiones de C99) puede incluso permitirte hacerlo:
int main() {
int tab[fact(10)];
int tab2[std::max(20,30)];
}
Pero en este momento no es C ++ estándar - constexpr
parece una forma de combatir esto (incluso sin VLA, en el caso anterior). Y todavía existe el problema de la necesidad de tener expresiones constantes "formales" como argumentos de plantilla.
Su ejemplo básico sirve para el mismo argumento que el de las constantes mismas. Por que usar
static const int x = 5;
int arr[x];
terminado
int arr[5];
Porque es mucho más mantenible. Usar constexpr es mucho, mucho más rápido de escribir y leer que las técnicas de metaprogramación existentes.
Supongamos que hace algo un poco más complicado.
constexpr int MeaningOfLife ( int a, int b ) { return a * b; }
const int meaningOfLife = MeaningOfLife( 6, 7 );
Ahora tiene algo que puede ser evaluado hasta una constante mientras mantiene una buena legibilidad y permite un procesamiento un poco más complejo que simplemente establecer una constante en un número.
Básicamente, proporciona una buena ayuda para el mantenimiento, ya que se vuelve más obvio lo que está haciendo. Tome max( a, b )
por ejemplo:
template< typename Type > constexpr Type max( Type a, Type b ) { return a < b ? b : a; }
Es una opción bastante simple, pero significa que si se llama a max
con valores constantes, se calcula explícitamente en el momento de la compilación y no en el tiempo de ejecución.
Otro buen ejemplo sería una función DegreesToRadians
. Todo el mundo encuentra grados más fáciles de leer que los radianes. Si bien es posible que sepas que 180 grados están en radianes, está mucho más claro:
const float oneeighty = DegreesToRadians( 180.0f );
Aquí hay mucha información útil:
Todas las otras respuestas son geniales, solo quiero dar un ejemplo genial de una cosa que puedes hacer con constexpr que es increíble. See-Phit ( https://github.com/rep-movsd/see-phit/blob/master/seephit.h ) es un analizador HTML de compilación y un motor de plantillas. Esto significa que puede insertar HTML y extraer un árbol que puede ser manipulado. Tener el análisis realizado en tiempo de compilación puede darle un poco de rendimiento adicional.
Desde el ejemplo de la página github:
#include <iostream>
#include "seephit.h"
using namespace std;
int main()
{
constexpr auto parser =
R"*(
<span >
<p color="red" height=''10'' >{{name}} is a {{profession}} in {{city}}</p >
</span>
)*"_html;
spt::tree spt_tree(parser);
spt::template_dict dct;
dct["name"] = "Mary";
dct["profession"] = "doctor";
dct["city"] = "London";
spt_tree.root.render(cerr, dct);
cerr << endl;
dct["city"] = "New York";
dct["name"] = "John";
dct["profession"] = "janitor";
spt_tree.root.render(cerr, dct);
cerr << endl;
}
Tome std::numeric_limits<T>::max()
: por cualquier razón, este es un método. constexpr
sería beneficioso aquí.
Otro ejemplo: desea declarar un C-array (o un std::array
) que es tan grande como otro array. La forma de hacerlo en este momento es así:
int x[10];
int y[sizeof x / sizeof x[0]];
Pero, ¿no sería mejor poder escribir:
int y[size_of(x)];
Gracias a constexpr
, puedes:
template <typename T, size_t N>
constexpr size_t size_of(T (&)[N]) {
return N;
}
constexpr
funciones constexpr
son realmente agradables y una gran adición a c ++. Sin embargo, tiene razón en que la mayoría de los problemas que resuelve se pueden solucionar de forma poco inteligente con macros.
Sin embargo, uno de los usos de constexpr
no tiene constantes mecanografiadas equivalentes a C ++ 03.
// This is bad for obvious reasons.
#define ONE 1;
// This works most of the time but isn''t fully typed.
enum { TWO = 2 };
// This doesn''t compile
enum { pi = 3.1415f };
// This is a file local lvalue masquerading as a global
// rvalue. It works most of the time. But May subtly break
// with static initialization order issues, eg pi = 0 for some files.
static const float pi = 3.1415f;
// This is a true constant rvalue
constexpr float pi = 3.1415f;
// Haven''t you always wanted to do this?
// constexpr std::string awesome = "oh yeah!!!";
// UPDATE: sadly std::string lacks a constexpr ctor
struct A
{
static const int four = 4;
static const int five = 5;
constexpr int six = 6;
};
int main()
{
&A::four; // linker error
&A::six; // compiler error
// EXTREMELY subtle linker error
int i = rand()? A::four: A::five;
// It not safe use static const class variables with the ternary operator!
}
//Adding this to any cpp file would fix the linker error.
//int A::four;
//int A::six;