Calcular la longitud de una cadena C en tiempo de compilación. ¿Es esto realmente un constexpr?

94

Estoy tratando de calcular la longitud de una cadena literal en tiempo de compilación. Para hacerlo, estoy usando el siguiente código:

#include <cstdio>

int constexpr length(const char* str)
{
    return *str ? 1 + length(str + 1) : 0;
}

int main()
{
    printf("%d %d", length("abcd"), length("abcdefgh"));
}

Todo funciona como se esperaba, el programa imprime 4 y 8. El código ensamblador generado por clang muestra que los resultados se calculan en tiempo de compilación:

0x100000f5e:  leaq   0x35(%rip), %rdi          ; "%d %d"
0x100000f65:  movl   $0x4, %esi
0x100000f6a:  movl   $0x8, %edx
0x100000f6f:  xorl   %eax, %eax
0x100000f71:  callq  0x100000f7a               ; symbol stub for: printf

Mi pregunta: ¿está garantizado por el estándar que la lengthfunción se evaluará en tiempo de compilación?

Si esto es cierto, la puerta para los cálculos de literales de cadena en tiempo de compilación se me acaba de abrir ... por ejemplo, puedo calcular hashes en tiempo de compilación y muchos más ...

Mircea Ispas
fuente
3
Siempre que el parámetro sea una expresión constante, debe serlo.
chris
1
@chris ¿Existe una garantía de que algo que puede ser una expresión constante debe evaluarse en tiempo de compilación cuando se usa en un contexto que no requiere una expresión constante?
TC
12
Por cierto, incluir <cstdio>y luego llamar ::printfno es portátil. El estándar solo requiere <cstdio>proporcionar std::printf.
Ben Voigt
1
@BenVoigt Ok, gracias por señalar eso :) Inicialmente usé std :: cout, pero el código generado era bastante grande para encontrar los valores reales :)
Mircea Ispas
3
@Felics A menudo uso godbolt cuando respondo preguntas relacionadas con la optimización y el uso printfpuede llevar a una cantidad significativamente menor de código para tratar.
Shafik Yaghmour

Respuestas:

76

No se garantiza que las expresiones constantes se evalúen en tiempo de compilación, solo tenemos una cita no normativa del borrador de la sección estándar de C ++ 5.19 Expresiones constantes que dice esto:

[...]> [Nota: las expresiones constantes se pueden evaluar durante la traducción. — nota final]

Puede asignar el resultado a la constexprvariable para asegurarse de que se evalúe en el momento de la compilación, podemos ver esto en la referencia de C ++ 11 de Bjarne Stroustrup que dice (el énfasis es mío ):

Además de poder evaluar expresiones en tiempo de compilación, queremos poder requerir que las expresiones sean evaluadas en tiempo de compilación; constexpr delante de una definición de variable hace eso (e implica const):

Por ejemplo:

constexpr int len1 = length("abcd") ;

Bjarne Stroustrup ofrece un resumen de cuándo podemos asegurar la evaluación del tiempo de compilación en esta entrada del blog de isocpp y dice:

[...] La respuesta correcta - como dice Herb - es que de acuerdo con el estándar, una función constexpr puede evaluarse en tiempo de compilación o en tiempo de ejecución a menos que se use como una expresión constante, en cuyo caso debe evaluarse en la compilación -hora. Para garantizar la evaluación en tiempo de compilación, debemos usarlo cuando se requiera una expresión constante (por ejemplo, como un enlace de matriz o como una etiqueta de caso) o usarlo para inicializar un constexpr. Espero que ningún compilador que se precie pierda la oportunidad de optimización de hacer lo que dije originalmente: "Una función constexpr se evalúa en tiempo de compilación si todos sus argumentos son expresiones constantes".

Entonces, esto describe dos casos en los que debe evaluarse en el momento de la compilación:

  1. Úselo donde se requiera una expresión constante, esto parecería estar en cualquier parte del borrador de estándar donde se usa la frase shall be ... converted constant expressiono shall be ... constant expression, como un límite de matriz.
  2. Úselo para inicializar a constexprcomo describo anteriormente.
Shafik Yaghmour
fuente
4
Dicho esto, en principio, un compilador tiene derecho a ver un objeto con vínculo interno o sin vínculo con constexpr int x = 5;, observar que no requiere el valor en tiempo de compilación (asumiendo que no se usa como parámetro de plantilla o cualquier otra cosa) y en realidad emite código que calcula el valor inicial en tiempo de ejecución utilizando 5 valores inmediatos de 1 y 4 operaciones de suma. Un ejemplo más realista: el compilador podría alcanzar un límite de recursividad y aplazar el cálculo hasta el tiempo de ejecución. A menos que haga algo que obligue al compilador a utilizar realmente el valor, "garantizado para ser evaluado en tiempo de compilación" es un problema de QOI.
Steve Jessop
@SteveJessop Bjarne parece estar usando un concepto que no tiene un análogo que pueda encontrar en el borrador del estándar que se usa como un medio de expresión constante evaluado en la traducción. Por lo tanto, parecería que el estándar no establece explícitamente lo que está diciendo, por lo que tenderé a estar de acuerdo con usted. Aunque tanto Bjarne como Herb parecen estar de acuerdo en esto, lo que podría indicar que está subespecificado.
Shafik Yaghmour
2
Creo que ambos están considerando solo "compiladores que se respeten a sí mismos", en contraposición al compilador que se ajusta a los estándares, pero deliberadamente obstructivo, que supongo. Es útil como medio de razonamiento sobre lo que realmente garantiza el estándar , y no mucho más ;-)
Steve Jessop
3
@SteveJessop Compiladores deliberadamente obstructivos, como el infame (y desafortunadamente inexistente) Hell ++. Tal cosa sería realmente excelente para probar la conformidad / portabilidad.
Angew ya no se enorgullece de SO
Bajo la regla como si, incluso usar el valor como una aparente constante de tiempo de compilación no es suficiente: el compilador es libre de enviar una copia de su fuente y recompilarla en tiempo de ejecución, o hacer un cálculo de rute para determinar el tipo de un variable, o simplemente vuelva a ejecutar inútilmente su constexprcálculo por pura maldad. Incluso es gratis esperar 1 segundo por personaje en una línea de fuente determinada, o tomar una línea de fuente determinada y usarla para sembrar una posición de ajedrez, luego jugar en ambos lados para determinar quién ganó.
Yakk - Adam Nevraumont
27

Es realmente fácil averiguar si una llamada a una constexprfunción da como resultado una expresión constante central o simplemente se está optimizando:

Úselo en un contexto donde se requiera una expresión constante.

int main()
{
    constexpr int test_const = length("abcd");
    std::array<char,length("abcdefgh")> test_const2;
}
Ben Voigt
fuente
4
... y compila con -pedantic, si usas gcc. De lo contrario, no recibirá advertencias ni errores
BЈовић
@ BЈовић O utilícelo en un contexto donde GCC no tiene extensiones que puedan interferir, como un argumento de plantilla.
Angew ya no se enorgullece de SO
¿No sería más confiable un truco de enumeración ? Como enum { Whatever = length("str") }?
diente afilado
18
Digno de mención esstatic_assert(length("str") == 3, "");
chris
8
constexpr auto test = /*...*/;es probablemente el más general y sencillo.
TC
19

Solo una nota, que los compiladores modernos (como gcc-4.x) hacen strlenpara los literales de cadena en tiempo de compilación porque normalmente se define como una función intrínseca . Sin optimizaciones habilitadas. Aunque el resultado no es una constante de tiempo de compilación.

P.ej:

printf("%zu\n", strlen("abc"));

Resultados en:

movl    $3, %esi    # strlen("abc")
movl    $.LC0, %edi # "%zu\n"
movl    $0, %eax
call    printf
Maxim Egorushkin
fuente
Tenga en cuenta que esto funciona porque strlenes una función incorporada, si la usamos -fno-builtinsvuelve a llamarla en tiempo de ejecución,
véala en
strlenes constexprpara mí, incluso con -fno-nonansi-builtins(parece que -fno-builtinsya no existe en g ++). Digo "constexpr", porque puedo hacer esto template<int> void foo();y foo<strlen("hi")>(); g ++ - 4.8.4
Aaron McDaid
18

Permítanme proponer otra función que calcula la longitud de una cadena en tiempo de compilación sin ser recursiva.

template< size_t N >
constexpr size_t length( char const (&)[N] )
{
  return N-1;
}

Eche un vistazo a este código de muestra en ideone .

usuario2436830
fuente
4
Puede no ser igual a strlen debido al '\ 0' incrustado: strlen ("hi \ 0there")! = Length ("hi \ 0there")
unkulunkulu
Esta es la forma correcta, este es un ejemplo en Effective Modern C ++ (si recuerdo bien). Sin embargo, hay una buena clase de cadena que es completamente constexpr, vea esta respuesta: str_const de Scott Schurr , tal vez esto sea más útil (y menos estilo C).
QuantumKarl
@MikeWeir Ops, eso es extraño. Aquí hay varios enlaces: enlace a la pregunta , enlace al papel , enlace a la fuente en git
QuantumKarl
ahora sí: char temp[256]; sprintf(temp, "%u", 2); if(1 != length(temp)) printf("Your solution doesn't work"); ideone.com/IfKUHV
Pablo Ariel
7

No hay garantía de que una constexprfunción se evalúe en tiempo de compilación, aunque cualquier compilador razonable lo hará con los niveles de optimización adecuados habilitados. Por otro lado, los parámetros de la plantilla deben evaluarse en tiempo de compilación.

Usé el siguiente truco para forzar la evaluación en tiempo de compilación. Desafortunadamente, solo funciona con valores integrales (es decir, no con valores de coma flotante).

template<typename T, T V>
struct static_eval
{
  static constexpr T value = V;
};

Ahora, si escribes

if (static_eval<int, length("hello, world")>::value > 7) { ... }

puede estar seguro de que la ifdeclaración es una constante en tiempo de compilación sin sobrecarga de tiempo de ejecución.

5gon12eder
fuente
8
o simplemente use std :: integral_constant <int, length (...)> :: value
Mircea Ispas
1
El ejemplo es un uso un poco inútil, ya lenque de todos constexprmodos lengthdebe ser evaluado en tiempo de compilación.
chris
@chris No sabía que debía ser, aunque he observado que es con mi compilador.
5gon12eder
Ok, de acuerdo con la mayoría de las otras respuestas tiene que hacerlo, así que modifiqué el ejemplo para que sea menos inútil. De hecho, fue una ifcondición (donde era esencial que el compilador eliminara el código muerto) para la que originalmente usé el truco.
5gon12eder
1

Una breve explicación de la entrada de Wikipedia sobre expresiones constantes generalizadas :

El uso de constexpr en una función impone algunas limitaciones sobre lo que puede hacer esa función. Primero, la función debe tener un tipo de retorno que no sea nulo. En segundo lugar, el cuerpo de la función no puede declarar variables ni definir nuevos tipos. En tercer lugar, el cuerpo puede contener solo declaraciones, declaraciones nulas y una única declaración de retorno. Deben existir valores de argumento tales que, después de la sustitución de argumentos, la expresión en la declaración de retorno produzca una expresión constante.

Tener la constexprpalabra clave antes de la definición de una función indica al compilador que verifique si se cumplen estas limitaciones. Si es así, y la función se llama con una constante, se garantiza que el valor devuelto es constante y, por lo tanto, se puede usar en cualquier lugar donde se requiera una expresión constante.

Kaedinger
fuente
Estas condiciones no garantizan que el valor devuelto sea constante . Por ejemplo, la función podría llamarse con otros valores de argumento.
Ben Voigt
Correcto, @BenVoigt. Lo edité para que se llamara con una expresión constante.
Kaedinger