从捕获 constexpr 函数返回值的变量中删除 constexpr 会删除编译时计算

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

本文关键字:删除 constexpr 计算 编译 函数 返回值 变量      更新时间:2023-10-16

考虑以下constexpr函数static_strcmp,它使用 C++17 的constexprchar_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检查:

如果我们先比较static_strcmp中两个参数的char_traits::length,然后再调用char_traits::compare而不constexprbool 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
  • 为什么从初始调用中删除constexprstatic_strcmp会导致持续评估失败?
  • 显然,即使没有constexpr,对char_traits::length的调用也在编译时被计算,那么为什么不在static_strcmp的第一个版本中没有constexpr相同的行为呢?

我们有三个工作案例:

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

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

3)constexpr函数接收编译时已知的值,但结果位于不需要编译时的位置。

如果我们忽略 as-if 规则,我们就会得到:

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

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

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

使用初始代码

constexpr bool result = static_strcmp(a, b);

情况 (1):编译器必须计算编译时,因为result变量被声明为constexpr

删除constexpr

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

您的代码在灰色区域(情况 (3))中转换,其中编译时计算是可能的,但不是严格要求的,因为输入值是已知的编译时(ab),但结果 转到不需要编译时的值(普通变量)的地方。因此,编译器可以选择,并且在您的情况下,选择具有函数版本的运行时计算,以及具有另一个版本的编译时计算。

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

对 constexpr 函数的调用产生与调用相同的结果在所有方面等效的非 constexpr 函数,除了 (7.1) 对 constexpr 函数的调用可以出现在常量表达式中,并且 (7.2) 复制省略不在常量表达式中执行 ([class.copy.elision])。

(强调我的)

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

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

如果将字符串修改为长度相等(以便程序定义明确),则程序将按预期进行优化。

所以这不是错过优化。编译器将优化程序,但由于它包含未定义的行为,因此不会对其进行优化。


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

以下是当前标准草案对比较的看法:

返回:如果对于 [0, n] 中的每个 i,X::eq(p[i],q[i])true;否则,如果对于 [0, n] 中的某些 j,X::lt(p[j],q[j])true,对于 [0, j) 中的每个 i,X:

:eq(p[i],q[i])true,则为负值; 否则为正值。

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