从捕获 constexpr 函数 return 值的变量中删除 constexpr 会删除编译时评估

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

考虑以下 constexpr 函数 static_strcmp,它使用 C++17 的 constexpr char_traits::compare 函数:

#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 表明这在编译时得到评估,并优化为:

main:
    xor     eax, eax
    ret

bool result 中删除 constexpr:

如果我们从 constexpr bool result 中删除 constexpr,现在调用将不再优化。

#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 显示我们现在调用 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

添加短路length检查:

如果我们首先比较 char_traits::lengthstatic_strcmp 中的两个参数,然后 调用 char_traits::compare 没有 constexpr on bool result,调用再次优化。

#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 表明我们回到了正在优化的调用:

main:
    xor     eax, eax
    ret

请注意,标准 中没有明确要求 要求在编译时调用 constexpr 函数,请参阅最新草案中的 9.1.5.7:

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]).

(强调我的)

现在,当调用出现在常量表达式中时,编译器无法在编译时避免 运行 该函数,因此它尽职尽责。如果没有(如在您的第二个代码段中),则只是缺少优化的情况。身边不缺人。

我们有三个工作案例:

1) 需要计算值来初始化 constexpr 值或严格要求编译时已知值的地方(非类型模板参数、C 样式数组的大小、测试在 static_assert(), ...)

2) constexpr 函数使用编译时未知的值(例如:从标准输入接收的值。

3) constexpr 函数接收编译时已知的值,但结果放在编译时不需要的地方。

如果我们忽略 as-if 规则,我们有:

  • 在情况 (1) 中,编译器 必须 计算值编译时,因为计算值是编译时所必需的

  • 在情况 (2) 中,编译器 必须 计算值 运行-time 因为不可能在编译时计算它

  • 在情况 (3) 中,我们处于灰色区域,编译器可以在编译时计算值,但计算值并不是编译时严格要求的;在这种情况下,编译器可以选择是计算编译时还是 运行-time.

用初始代码

constexpr bool result = static_strcmp(a, b);

您属于情况 (1):编译器 必须 计算编译时间,因为 result 变量声明为 constexpr.

删除 constexpr

bool result = static_strcmp(a, b); // no more constexpr

您的代码在灰色区域(案例 (3))中进行翻译,其中可以进行编译时计算但并非严格要求,因为输入值是已知的编译时间(ab) 但结果 转到不需要编译时值的地方(普通变量)。所以编译器可以选择,在你的情况下,选择 运行-time computation with a version of the function, compile-time computation with another version.

你的程序有未定义的行为,因为你总是比较 strlen(a) 个字符。字符串 b 没有那么多字符。

如果您将字符串修改为等长(这样您的程序就会定义明确),您的程序将如您所愿optimised

所以这不是遗漏优化。编译器会优化您的程序,但因为它包含未定义的行为,所以不会对其进行优化。


注意,是否是未定义行为,不是很清楚。考虑到编译器使用 memcmp,它认为两个输入字符串必须至少 strlen(a) 长。所以根据编译器的行为,是undefined behavior。

以下是当前标准草案关于比较的内容:

Returns: 0 if for each i in [0, n), X::eq(p[i],q[i]) is true; else, a negative value if, for some j in [0, n), X::lt(p[j],q[j]) is true and for each i in [0, j) X::eq(p[i],q[i]) is true; else a positive value.

现在不指定compare是否允许读取p[j+1..n)q[j+1..n)(其中j是第一个差异的索引)。