我使用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
是在你期望它有益的地方工作。因此,您尝试满足其要求。但是,您需要测试是否已经生成了预期在编译时生成的代码,以及用户是否实际使用生成的结果或在运行时触发函数。
我找到了两种方法来检测类或(成员)函数是否正在使用编译时或运行时计算路径。
- 使用
constexpr
函数的属性,如果在编译时计算,则从noexcept 运算符(bool noexcept( expression )
)返回true
。因为生成的结果将是编译时常量。此方法非常易于访问,并且可以在单元测试中使用。
(请注意,将这些函数显式标记为noexcept
会破坏测试。
来源:cppreference.com(2017/3/3)
由于 noexcept 运算符始终为常量表达式返回 true,因此它可用于检查 constexpr 函数的特定调用是否采用常量表达式分支 (...)
-
(不太方便)使用调试器:通过在标记为
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 变量始终是一个实常量表达式。如果编译,则在编译时计算。