c++ - songs - tag series netflix
¿Cuándo y por qué usarías estática con constexpr? (3)
Como un descargo de responsabilidad, he hecho mi investigación sobre esto antes de preguntar. Encontré una pregunta SO similar, pero la respuesta se siente un poco "hombre de paja" y realmente no respondí la pregunta personalmente. También me he referido a mi útil página de referencia de cpp, pero la mayoría de las veces no ofrece una explicación muy sencilla de las cosas.
Básicamente, sigo aumentando en constexpr
, pero en este momento mi entendimiento es que requiere que las expresiones se evalúen en tiempo de compilación. Dado que solo pueden existir en tiempo de compilación, realmente no tendrán una dirección de memoria en tiempo de ejecución. Entonces, cuando veo a gente que usa static constexpr
(como en una clase, por ejemplo) me confunde ... la static
sería superflua ya que eso solo es útil para contextos de tiempo de ejecución.
He visto contradicción en la constexpr
" constexpr
no permite nada más que expresiones de tiempo de compilación" (particularmente aquí en SO). Sin embargo, un artículo de la página de Bjarne Stroustrup explica en varios ejemplos que, de hecho, constexpr
requiere la evaluación de la expresión en el momento de la compilación. Si no, se debe generar un error de compilación.
Mi párrafo anterior parece un poco fuera de tema, pero es una línea de base necesaria para comprender por qué la static
puede o debe usarse con constexpr
. Esa línea de base, desafortunadamente, tiene mucha información contradictoria flotando alrededor.
¿Puede alguien ayudarme a reunir toda esta información en hechos puros con ejemplos y conceptos que tengan sentido? Básicamente, junto con comprender cómo se comporta realmente constexpr
, ¿por qué constexpr
static
con él? ¿Y a través de qué ámbitos / escenarios tiene sentido static constexpr
, si pueden usarse juntos?
Las variables constexpr no son valores de tiempo de compilación
Un valor es inmutable y no ocupa el almacenamiento (no tiene dirección), sin embargo, los objetos declarados como constexpr
pueden ser mutables y ocupan el almacenamiento (según la regla as-if).
Mutabilidad
La mayoría de los objetos declarados como constexpr
son inmutables, pero es posible definir un objeto constexpr
que sea (parcialmente) mutable de la siguiente manera:
struct S {
mutable int m;
};
int main() {
constexpr S s{42};
int arr[s.m]; // error: s.m is not a constant expression
s.m = 21; // ok, assigning to a mutable member of a const object
}
Almacenamiento
El compilador puede, bajo la regla as-if, elegir no asignar ningún almacenamiento para almacenar el valor de un objeto declarado como constexpr
. Del mismo modo, puede hacer tales optimizaciones para variables no constexpr. Sin embargo, considere el caso en el que necesitamos pasar la dirección del objeto a una función que no esté en línea; por ejemplo:
struct data {
int i;
double d;
// some more members
};
int my_algorithm(data const*, int);
int main() {
constexpr data precomputed = /*...*/;
int const i = /*run-time value*/;
my_algorithm(&precomputed, i);
}
El compilador aquí necesita asignar almacenamiento para precomputed
, para pasar su dirección a alguna función no en línea. Es posible que el compilador asigne el almacenamiento para precomputed
y i
contiguamente; uno podría imaginar situaciones en las que esto podría afectar el rendimiento (ver más abajo).
Standardese
Las variables son objetos o referencias [básico] / 6 . Centrémonos en los objetos.
Una declaración como constexpr int a = 42;
Es gramáticamente una simple declaración ; consiste en decl-specifier-seq init-declarator-list ;
De [dcl.dcl] / 9, podemos concluir (pero no rigurosamente) que tal declaración declara un objeto. Específicamente, podemos (rigurosamente) concluir que es una declaración de objeto , pero esto incluye declaraciones de referencias. Vea también la discusión de si podemos o no tener variables de tipo void
.
El constexpr
en la declaración de un objeto implica que el tipo del objeto es const
[dcl.constexpr] / 9 . Un objeto es una región de almacenamiento [intro.object] / 1 . Podemos inferir de [intro.object] / 6 y [intro.memory] / 1 que cada objeto tiene una dirección. Tenga en cuenta que es posible que no podamos tomar directamente esta dirección, por ejemplo, si se hace referencia al objeto a través de un prvalue. (Incluso hay prvalores que no son objetos, como el literal 42
). Dos objetos completos distintos deben tener direcciones diferentes [intro.object] / 6 .
A partir de este punto, podemos concluir que un objeto declarado como constexpr
debe tener una dirección única con respecto a cualquier otro objeto (completo).
Además, podemos concluir que la declaración constexpr int a = 42;
Declara un objeto con una dirección única.
estática y constexpr
El único problema interesante de IMHO es la " static
por función", à la
void foo() {
static constexpr int i = 42;
}
Por lo que sé, pero esto todavía no está del todo claro , el compilador puede calcular el inicializador de una variable constexpr
en tiempo de ejecución. Pero esto parece patológico; supongamos que no lo hace, es decir, precomputa el inicializador en tiempo de compilación.
La inicialización de una variable local static constexpr
se realiza durante la inicialización estática , que debe realizarse antes de cualquier inicialización dinámica [basic.start.init] / 2 . Aunque no está garantizado, probablemente podemos suponer que esto no impone un costo de tiempo de ejecución / tiempo de carga. Además, como no hay problemas de concurrencia para la inicialización constante, creo que podemos asumir con seguridad que esto no requiere una verificación en tiempo de ejecución segura de subprocesos si la variable static
ya se ha inicializado o no. (Mirar las fuentes de clang y gcc debería arrojar algo de luz sobre estos temas).
Para la inicialización de variables locales no estáticas, hay casos en que el compilador no puede inicializar la variable durante la inicialización constante:
void non_inlined_function(int const*);
void recurse(int const i) {
constexpr int c = 42;
// a different address is guaranteed for `c` for each recursion step
non_inlined_function(&c);
if(i > 0) recurse(i-1);
}
int main() {
int i;
std::cin >> i;
recurse(i);
}
Conclusión
Como parece, podemos beneficiarnos de la duración del almacenamiento estático de una variable static constexpr
en algunos casos de esquina. Sin embargo, podemos perder la localidad de esta variable local, como se muestra en la sección "Almacenamiento" de esta respuesta. Hasta que vea un punto de referencia que muestre que esto es un efecto real, asumiré que no es relevante.
Si solo hay estos dos efectos de static
en los objetos constexpr
, usaría static
por defecto: Normalmente no necesitamos la garantía de direcciones únicas para nuestros objetos constexpr
.
Para los objetos constexpr
mutables (tipos de clase con miembros mutable
), obviamente hay una semántica diferente entre los objetos constexpr
estáticos locales y no estáticos. De manera similar, si el valor de la dirección en sí es relevante (por ejemplo, para una búsqueda de mapa de hash).
Sólo ejemplos. Wiki de la comunidad.
static
== por función (duración de almacenamiento estático)
Los objetos declarados como constexpr
tienen direcciones como cualquier otro objeto. Si, por algún motivo, se usa la dirección del objeto, el compilador podría tener que asignarle almacenamiento:
constexpr int expensive_computation(int n); // defined elsewhere
void foo(int const p = 3) {
constexpr static int bar = expensive_computation(42);
std::cout << static_cast<void const*>(&bar) << "/n";
if(p) foo(p-1);
}
La dirección de la variable será la misma para todas las invocaciones; no se requerirá espacio de pila para cada llamada de función. Comparar con:
void foo(int const p = 3) {
constexpr int bar = expensive_computation(42);
std::cout << static_cast<void const*>(&bar) << "/n";
if(p) foo(p-1);
}
Aquí, las direcciones serán diferentes para cada invocación (recursiva) de foo
.
Esto importa, por ejemplo, si el objeto es grande (por ejemplo, una matriz) y necesitamos ambos para usarlo en un contexto donde se requiere una expresión constante (requiere una constante de tiempo de compilación) y necesitamos tomar su dirección.
Tenga en cuenta que dado que las direcciones deben ser diferentes, el objeto podría inicializarse en tiempo de ejecución ; por ejemplo, si la profundidad de la recursión depende de un parámetro de tiempo de ejecución. El inicializador aún puede ser precalculado, pero el resultado debe copiarse en la nueva región de memoria para cada paso de recursión. En ese caso, constexpr
solo garantiza que el intializer se pueda evaluar en tiempo de compilación, y la inicialización se podría realizar en tiempo de compilación para una variable de ese tipo.
static
== por clase
template<int N>
struct foo
{
static constexpr int n = N;
};
Lo mismo que siempre: declara una variable para cada especialización de plantilla (creación de instancias) de foo
, por ejemplo, foo<1>
, foo<42>
, foo<1729>
. Si desea exponer el parámetro de plantilla que no es de tipo, puede utilizar, por ejemplo, un miembro de datos estáticos. Puede ser constexpr
para que otros puedan beneficiarse del valor conocido en tiempo de compilación.
static
== enlace interno
// namespace-scope
static constexpr int x = 42;
Bastante redundante; constexpr
variables constexpr
tienen enlaces internos por defecto. No veo ninguna razón actualmente para usar static
en este caso.
Uso static constexpr
como reemplazo de las enumeraciones sin nombre en lugares donde no conozco una definición de tipo exacto, pero quiero consultar información sobre el tipo (generalmente en tiempo de compilación).
Hay algunos beneficios adicionales para el tiempo de compilación sin nombre enumeración. Depuración más sencilla (los valores aparecen en un depurador como una variable "normal". Además, puede utilizar cualquier tipo que se pueda construir constexpr (no solo números), en lugar de solo números con una enumeración.
Ejemplos:
template<size_t item_count, size_t item_size> struct item_information
{
static constexpr size_t count_ = item_count;
static constexpr size_t size_ = item_size;
};
Ahora, puedes acceder a estas variables en tiempo de compilación:
using t = item_information <5, 10>;
constexpr size_t total = t::count_ * t::size_;
Alternativas:
template<size_t item_count, size_t item_size> struct item_information
{
enum { count_ = item_count };
enum { size_ = item_size };
};
template<size_t item_count, size_t item_size> struct item_information
{
static const size_t count_ = item_count;
static const size_t size_ = item_size;
};
Las alternativas no tienen todos los aspectos positivos de constexpr estático: tiene garantizado el tiempo de compilación, la seguridad de tipos y el uso (potencialmente) menor de la memoria (las variables constexpr no necesitan ocupar memoria, son realmente difíciles) codificado a menos que sea posible).
A menos que comience a tomar la dirección de las variables constexpr (y posiblemente incluso si todavía lo hace), no hay un aumento de tamaño en sus clases como lo vería con una constante estática estándar.