Comparar con el literal de cadena no resuelto en tiempo de compilación

8

Recientemente encontré algo similar a las siguientes líneas:

#include <string>

// test if the extension is either .bar or .foo
bool test_extension(const std::string& ext) {
    return ext == ".bar" || ".foo";
    // it obviously should be
    // return ext == ".bar" || ext == ".foo";
}

La función obviamente no hace lo que sugiere el comentario. Pero ese no es el punto aquí. Tenga en cuenta que esto no es un duplicado de ¿Puede usar 2 o más condiciones O en una declaración if? ¡ya que soy completamente consciente de cómo escribirías la función correctamente!


Empecé a preguntarme cómo un compilador podría tratar este fragmento. Mi primera intuición habría sido que esto se compilaría return true;básicamente. Al enchufar el ejemplo en godbolt , se demostró que ni GCC 9.2 ni clang 9 realizan esta optimización con optimización -O2.

Sin embargo, cambiando el código a 1

#include <string>

using namespace std::string_literals;

bool test_extension(const std::string& ext) {
    return ext == ".bar"s || ".foo";
}

parece hacer el truco ya que el ensamblaje ahora es en esencia:

mov     eax, 1
ret

Entonces, mi pregunta principal es: ¿Hay algo que me haya perdido y que no permita que un compilador realice la misma optimización en el primer fragmento?


1 Con ".foo"sesto ni siquiera se compilaría, ya que el compilador no quiere convertir un std::stringa bool;-)


Editar

El siguiente fragmento de código también se optimiza "correctamente" para return true;:

#include <string>

bool test_extension(const std::string& ext) {
    return ".foo" || ext == ".bar";
}
AlexV
fuente
3
Hm, ¿ string::compare(const char*)tiene algunos efectos secundarios que el compilador no eliminará (que operator==(string, string)no tiene)? Parece poco probable, pero el compilador ya determinó que el resultado siempre es verdadero (también lo tiene mov eax, 1 ret) incluso para el primer fragmento.
Max Langhof
2
¿Quizás porque operator==(string const&, string const&)es noexceptmientras operator==(string const&, char const*)que no es? No tengo tiempo para profundizar más ahora.
Programador del
@MaxLanghof Al cambiar el orden a foo || ext == ".bar", la llamada se optimiza (ver edición). ¿Eso contradice tu teoría?
AlexV
2
@AlexV No estoy seguro de lo que se supone que significa eso. Cortocircuito para la expresión a || bsignifica "evaluar la expresión bsolo si la expresión aes false". Es ortogonal al tiempo de ejecución o al tiempo de compilación. true || foo()puede optimizarse true, incluso si foo()tiene efectos secundarios, porque (sin importar si está optimizado o no) el lado derecho nunca se evalúa. Pero foo() || trueno se puede optimizar a truemenos que el compilador pueda probar que las llamadas foo()no tienen efectos secundarios observables.
Max Langhof
1
Cuando tomo el enlace de Compiler Explorer provisto y verifico la opción "Compilar en binario y desensamblar la salida", de repente se compila xor eax,eaxaunque sin esa opción llama a la función de comparación de cadenas. No tengo idea de qué hacer con eso.
Daniel H

Respuestas:

3

Esto te aturdirá aún más: ¿qué sucede si creamos un tipo de char personalizado MyCharTy lo usamos para hacer nuestro propio personalizado std::basic_string?

#include <string>

struct MyCharT {
    char c;
    bool operator==(const MyCharT& rhs) const {
        return c == rhs.c;
    }
    bool operator<(const MyCharT& rhs) const {
        return c < rhs.c;
    }
};
typedef std::basic_string<MyCharT> my_string;

bool test_extension_custom(const my_string& ext) {
    const MyCharT c[] = {'.','b','a','r', '\0'};
    return ext == c || ".foo";
}

// Here's a similar implementation using regular
// std::string, for comparison
bool test_extension(const std::string& ext) {
    const char c[] = ".bar";
    return ext == c || ".foo";
}

Ciertamente, un tipo personalizado no se puede optimizar más fácilmente que un simple char, ¿verdad?

Aquí está el ensamblaje resultante:

test_extension_custom(std::__cxx11::basic_string<MyCharT, std::char_traits<MyCharT>, std::allocator<MyCharT> > const&):
        mov     eax, 1
        ret
test_extension(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
        sub     rsp, 24
        lea     rsi, [rsp+11]
        mov     DWORD PTR [rsp+11], 1918984750
        mov     BYTE PTR [rsp+15], 0
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::compare(char const*) const
        mov     eax, 1
        add     rsp, 24
        ret

¡Véalo en vivo!


Mindblown!

Entonces, ¿cuál es la diferencia entre mi tipo de cadena "personalizada" y std::string ?

Optimización de cuerdas pequeñas

Al menos en GCC, Small String Optimization se compila en el binario para libstdc ++. Esto significa que, durante la compilación de su función, el compilador no tiene acceso a esta implementación, por lo tanto, no puede saber si hay algún efecto secundario. Debido a esto, no puede optimizar la llamada a compare(char const*)distancia. Nuestra clase "personalizada" no tiene este problema porque el SSO se implementa solo de forma simple std::string.

Por cierto, si compila con -std=c++2a, el compilador lo optimiza . Lamentablemente, no soy lo suficientemente inteligente en C ++ 20 aún para saber qué cambios hicieron esto posible.

Cássio Renan
fuente