Search code examples
c++c++17constexprchar-traits

removing constexpr from a variable capturing a constexpr function return value removes compile-time evaluation


Consider the following constexpr function, static_strcmp, which uses C++17's constexpr char_traits::compare function:

#include <string>

constexpr bool static_strcmp(char const *a, char const *b) 
{
    return std::char_traits<char>::compare(a, b,
        std::char_traits<char>::length(a)) == 0;
}

int main() 
{
    constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
    constexpr const char *b = "abc";

    constexpr bool result = static_strcmp(a, b);

    return result;
}

godbolt shows this gets evaluated at compile-time, and optimised down to:

main:
    xor     eax, eax
    ret

Remove constexpr from bool result:

If we remove the constexpr from constexpr bool result, now the call is no longer optimised.

#include <string>

constexpr bool static_strcmp(char const *a, char const *b) 
{
    return std::char_traits<char>::compare(a, b,
        std::char_traits<char>::length(a)) == 0;
}

int main() 
{
    constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
    constexpr const char *b = "abc";

    bool result = static_strcmp(a, b);            // <-- note no constexpr

    return result;
}

godbolt shows we now call into memcmp:

.LC0:
    .string "abc"
.LC1:
    .string "abcdefghijklmnopqrstuvwxyz"
main:
    sub     rsp, 8
    mov     edx, 26
    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:.LC1
    call    memcmp
    test    eax, eax
    sete    al
    add     rsp, 8
    movzx   eax, al
    ret

Add a short circuiting length check:

if we first compare char_traits::length for the two arguments in static_strcmp before calling char_traits::compare, without constexpr on bool result, the call is optimised away again.

#include <string>

constexpr bool static_strcmp(char const *a, char const *b) 
{
    return 
        std::char_traits<char>::length(a) == std::char_traits<char>::length(b) 
        && std::char_traits<char>::compare(a, b, 
             std::char_traits<char>::length(a)) == 0;
}

int main() 
{
    constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
    constexpr const char *b = "abc";

    bool result = static_strcmp(a, b);            // <-- note still no constexpr!

    return result;
}

godbolt shows we're back to the call being optimised away:

main:
    xor     eax, eax
    ret
  • Why does removing constexpr from the initial call to static_strcmp cause the constant evaluation to fail?
  • Clearly even without constexpr, the call to char_traits::length is evaluated at compile time, so why not the same behaviour without constexpr in the first version of static_strcmp?

Solution

  • Note, that nothing in the standard explicitly requires constexpr function to be called at compile time, see 9.1.5.7 in latest draft:

    A call to a constexpr function produces the same result as a call to an equivalent non-constexpr function in all respects except that (7.1) a call to a constexpr function can appear in a constant expression and (7.2) copy elision is not performed in a constant expression ([class.copy.elision]).

    (emphasizes mine)

    Now, when the call appears in constant expression, there is no way compiler can avoid running the function at compile time, so it dutifully obliges. When it does not (as in your second snippet) it is just a case of missing optimization. There is no shortage of those around here.