and c++ c++11 rationale

c++ - and - ¿Por qué está mal formado para tener funciones constexpr multilínea?



constexpr in c++ (5)

EDIT: Ignorar esta respuesta. El papel al que se hace referencia está desactualizado. El estándar permitirá una recursión limitada (ver los comentarios).

Ambas formas son ilegales. La recursión no está permitida en las funciones constexpr, debido a la restricción de que no se puede llamar a una función constexpr hasta que esté definida. El enlace que el OP proporcionó establece esto explícitamente:

constexpr int twice(int x); enum { bufsz = twice(256) }; // error: twice() isn’t (yet) defined constexpr int fac(int x) { return x > 2 ? x * fac(x - 1) : 1; } // error: fac() not defined // before use

Unas líneas más abajo:

El requisito de que una función de expresión constante solo puede llamar a funciones de expresión constante previamente definidas garantiza que no tengamos problemas relacionados con la recursión.

...

Nosotros (todavía) prohibimos la recursión en todas sus formas en expresiones constantes.

Sin estas restricciones, se verá envuelto en el problema de la detención (gracias @Grant por refrescar mi memoria con su comentario sobre mi otra respuesta). En lugar de imponer límites de recursión arbitrarios, los diseñadores consideraron más sencillo decir simplemente "No".

De acuerdo con las Expresiones Constantes Generalizadas — Revisión 5, lo siguiente es ilegal.

constexpr int g(int n) // error: body not just ‘‘return expr’’ { int r = n; while (--n > 1) r *= n; return r; }

Esto se debe a que todas las funciones ''constexpr'' deben tener la forma { return expression; } { return expression; } . No puedo ver ninguna razón para que esto sea así.

En mi opinión, lo único que realmente sería necesario es que no se lea / escriba información de estado externo y que los parámetros que se pasan sean también declaraciones ''constexpr''. Esto significaría que cualquier llamada a la función con los mismos parámetros devolvería el mismo resultado, por lo que es posible "saber" en el momento de la compilación.

Mi principal problema con esto es que parece que te obliga a hacer formas realmente redondas de bucles y con la esperanza de que el compilador lo optimice para que sea igual de rápido para llamadas no constexpr.

Para escribir un constexpr válido para el ejemplo anterior, podría hacer:

constexpr int g(int n) // error: body not just ‘‘return expr’’ { return (n <= 1) ? n : (n * g(n-1)); }

Pero esto es mucho más difícil de entender y hay que esperar que el compilador se encargue de la recursión de la cola cuando llame con parámetros que violan los requisitos de const-expr .


En caso de que haya alguna confusión aquí, es consciente de que constexpr funciones / expresiones constexpr se evalúan en tiempo de compilación . No hay ningún problema de rendimiento en tiempo de ejecución involucrado.

Sabiendo esto, la razón por la que solo permiten declaraciones de retorno únicas en constexpr funciones constexpr es que los implementadores del compilador no necesitan escribir una máquina virtual para calcular el valor constante.

Estoy preocupado por los problemas de QoI con esto sin embargo. Me pregunto si los implementadores del compilador serán lo suficientemente inteligentes como para realizar la memorización.

constexpr fib(int n) { return < 2 ? 1 : fib(n-1) + fib(n-2); }

Sin la memoria, la función anterior tiene una complejidad O (2 n ) , que ciertamente no es algo que me gustaría sentir, incluso en tiempo de compilación.


La razón es que el compilador ya tiene mucho que hacer, sin ser también un intérprete completo, capaz de evaluar código C ++ arbitrario.

Si se quedan con expresiones simples, limitan el número de casos a considerar dramáticamente. En términos generales, simplifica mucho las cosas, ya que no hay punto y coma en particular.

Cada vez que un se encuentra, significa que el compilador tiene que lidiar con los efectos secundarios. Significa que se cambió algún estado local en la declaración anterior, en la que se basará la siguiente declaración. Significa que el código que se está evaluando ya no es solo una serie de operaciones simples, cada una toma como entradas la salida de la operación anterior, sino que también requiere acceso a la memoria, lo cual es mucho más difícil de razonar.

En pocas palabras, esto:

7 * 2 + 4 * 3

Es fácil de calcular. Puedes construir un árbol de sintaxis que se vea así:

+ // / / * * // // 7 2 4 3

y el compilador puede simplemente atravesar este árbol realizando estas operaciones primitivas en cada nodo, y el nodo raíz es implícitamente el valor de retorno de la expresión.

Si tuviéramos que escribir el mismo cálculo utilizando varias líneas, podríamos hacerlo así:

int i0 = 7; int i1 = 2; int i2 = 4; int i3 = 3; int i4 = i0 * i1; int i5 = i2 * i3; int i6 = i4 + i5; return i6;

que es mucho más difícil de interpretar. Necesitamos manejar las lecturas y escrituras de la memoria, y tenemos que manejar las declaraciones de devolución. Nuestro árbol de sintaxis se volvió mucho más complejo. Necesitamos manejar declaraciones de variables. Necesitamos manejar declaraciones que no tienen valor de retorno (por ejemplo, un bucle o una escritura de memoria), pero que simplemente modifican la memoria en algún lugar. Que recuerdo ¿Dónde? ¿Qué pasa si sobrescribe accidentalmente algo de la memoria del compilador? ¿Y si se segfaults?

Incluso sin todos los repugnantes "qué pasa si", el código que el compilador debe interpretar se vuelve mucho más complejo. El árbol de sintaxis ahora podría tener un aspecto similar al siguiente: ( LD y ST son operaciones de carga y almacenamiento, respectivamente)

; // ST / // / i0 3 / ; // ST / // / i1 4 / ; // ST / / / / i2 2 / ; // ST / // / i3 7 / ; // ST / // / i4 * / // / LD LD / | | / i0 i1 / ; // ST / // / i5 * / // / LD LD / | | / i2 i3 / ; // ST / // / i6 + / // / LD LD / | | / i4 i5 / LD | i6

No solo se ve mucho más complejo, sino que ahora también requiere estado. Antes, cada subárbol podía ser interpretado de forma aislada. Ahora, todos ellos dependen del resto del programa. Una de las operaciones de hoja LD no tiene sentido a menos que se coloque en el árbol, de modo que una operación ST se haya ejecutado en la misma ubicación anteriormente .


Probablemente esté mal formado porque es demasiado difícil de implementar. Se tomó una decisión similar en la primera versión de la norma con respecto a los cierres de funciones de los miembros (es decir, poder pasar obj.func como una función que se puede obj.func ). Tal vez una revisión posterior de la norma ofrecerá más latitud.


Según tengo entendido, lo mantuvieron lo más simple posible para no complicar el lenguaje (de hecho, me parece recordar un momento en el que no se permitían las llamadas recursivas, pero ya no es así). La razón es que es mucho más fácil relajar las reglas en estándares futuros que restringirlas.