如何测试是否正确评估了 constexpr



我使用constexpr来计算编译时的哈希代码。代码正确编译,正确运行。但我不知道,哈希值是编译时还是运行时。如果我在运行时跟踪代码,我不会进入 constexpr 函数。但是,即使对于运行时值也不会跟踪这些值(计算运行时生成的字符串的哈希 - 相同的方法)。我试图研究反汇编,但我完全不明白

出于调试目的,我的哈希代码只有字符串长度,使用以下内容:

constexpr inline size_t StringLengthCExpr(const char * const str) noexcept
{
    return (*str == 0) ? 0 : StringLengthCExpr(str + 1) + 1;
};

我像这样创建了 ID 类

class StringID
{
    public:
       constexpr StringID(const char * key);
    private:
       const unsigned int hashID;
}
constexpr inline StringID::StringID(const char * key)
        : hashID(StringLengthCExpr(key))
{
}

如果我在程序main方法中执行此操作

StringID id("hello world"); 

我得到了这个反汇编的代码(其中的一部分 - 还有更多来自内联方法和其他东西)

;;;     StringID id("hello world"); 
        lea       eax, DWORD PTR [-76+ebp]                     
        lea       edx, DWORD PTR [id.14876.0]                   
        mov       edi, eax                                     
        mov       esi, edx                                     
        mov       ecx, 4                                       
        mov       eax, ecx                                      
        shr       ecx, 2                                        
        rep   movsd                                            
        mov       ecx, eax                                      
        and       ecx, 3                                       
        rep   movsb                                            
// another code

我怎么能从中判断"哈希值"是编译时。我没有看到任何像 11 这样的常量移动到注册。我对 ASM 不太好,所以也许它是正确的,但我不确定要检查什么或如何确定"哈希代码"值是编译时,而不是在运行时从此代码计算的。

(我正在使用Visual Studio 2013 + Intel C++ 15 Compiler - VS Compiler不支持constexpr)

编辑:

如果我更改我的代码并执行此操作

    const int ix = StringLengthCExpr("hello world");
    mov       DWORD PTR [-24+ebp], 11                       ;55.15

我得到了正确的结果

即使有这个

将私有哈希 ID 更改为公共

 StringID id("hello world"); 
  // mov       DWORD PTR [-24+ebp], 11                       ;55.15
 printf("%i", id.hashID);
  // some other ASM code

但是如果我使用私有哈希ID并添加Getter

  inline uint32 GetHashID() const { return this->hashID; };

到ID类,然后我得到了

  StringID id("hello world"); 
  //see original "wrong" ASM code
  printf("%i", id.GetHashID());
  // some other ASM code

最方便的方法是在 static_assert 语句中使用constexpr。如果在编译时未计算代码,则不会编译代码,并且static_assert表达式不会在运行时产生任何开销(并且不会像模板解决方案那样生成不必要的代码)。

例:

static_assert(_StringLength("meow") == 4, "The length should be 4!");

这还会检查您的函数是否正确计算了结果。

如果要确保在编译时计算constexpr函数,请在需要编译时计算的内容中使用其结果:

template <size_t N>
struct ForceCompileTimeEvaluation { static constexpr size_t value = N; };
constexpr inline StringID::StringID(const char * key)
        : hashID(ForceCompileTimeEvaluation<StringLength(key)>::value)
{}

请注意,我已将该函数重命名为 仅 StringLength 。以下划线后跟大写字母开头的名称,或包含两个连续下划线的名称在用户代码中是不合法的。它们保留用于实现(编译器和标准库)。

将来(c++20),您可以使用 consteval 说明符来声明一个函数,该函数必须在编译时计算,因此需要一个常量表达式上下文。

consteval 说明符将函数或函数模板声明为 即时函数,即每次调用该函数都必须 (直接或间接)生成编译时常量表达式。

来自cppreference的示例(参见consteval):

consteval int sqr(int n) {
  return n*n;
}
constexpr int r = sqr(100);  // OK
int x = 100;
int r2 = sqr(x);  // Error: Call does not produce a constant
consteval int sqrsqr(int n) {
  return sqr(sqr(n)); // Not a constant expression at this point, but OK
}
constexpr int dblsqr(int n) {
  return 2*sqr(n); // Error: Enclosing function is not consteval and sqr(n) is not a constant
}

有几种方法可以强制编译时计算。但是这些并不像您在使用constexpr时所期望的那样灵活且易于设置。而且它们不能帮助您查找是否实际使用了编译时常量。

你想要constexpr是在你期望它有益的地方工作。因此,您尝试满足其要求。但是,您需要测试是否已经生成了预期在编译时生成的代码,以及用户是否实际使用生成的结果或在运行时触发函数。


我找到了两种方法来检测类或(成员)函数是否正在使用编译时或运行时计算路径。

  1. 使用constexpr函数的属性,如果在编译时计算,则从noexcept 运算符(bool noexcept( expression ))返回true。因为生成的结果将是编译时常量。此方法非常易于访问,并且可以在单元测试中使用。
    (请注意,将这些函数显式标记为noexcept会破坏测试。

来源:cppreference.com(2017/3/3)
由于 noexcept 运算符始终为常量表达式返回 true,因此它可用于检查 constexpr 函数的特定调用是否采用常量表达式分支 (...)

  1. (不太方便)使用调试器:通过在标记为 constexpr 的函数中放置断点。每当未触发断点时,都会使用编译器计算结果。不是最简单的,但可以进行附带检查。

苏尔:Microsoft文档 (2017/3/3)
注意: 在 Visual Studio 调试器中,可以通过在其中放置断点来判断 constexpr 函数是否正在编译时进行评估。如果命中断点,则在运行时调用该函数。如果不是,则在编译时调用该函数。

我发现这两种方法在试验constexpr时都很有用。虽然我没有对VS2017以外的环境进行任何测试。并且无法在当前的标准草案中找到支持这种行为的明确声明。

以下技巧可以帮助检查 constexpr 函数是否仅在编译时被计算:

使用 gcc,您可以使用程序集列表 + c 源编译源文件;假设 constexpr 及其调用都在源文件中 try.cpp

 gcc -std=c++11 -O2 -Wa,-a,-ad  try.cpp  | c++filt >try.lst  

如果在运行时计算了 constexpr 函数,那么您将在程序集列表中看到编译的函数和调用指令(在 x86 上调用function_name)(请注意,c++filt 命令未修饰链接器名称)

有趣的是,如果在没有优化的情况下编译(没有 -O2 或 -O3 选项),我总是会看到一个调用。

简单地把它放在constexpr变量中。

constexpr StringID id("hello world"); 
constexpr int ix = StringLengthCExpr("hello world");

constexpr 变量始终是一个实常量表达式。如果编译,则在编译时计算。

最新更新