有什么方法可以在 constexpr/consteval 上下文中访问已知大小的任意数据作为 char 数组?
Is there any way of accessing arbitrary data of known size as a char array in a constexpr/consteval context?
我正在尝试实现一些可以接收任意数据位(在编译时已知)并将它们的 CRC 计算为 consteval
的东西,因此我可以将它用于例如使用整数键索引此类数据而无需任何运行时开销。当输入是 char 字符串文字时,我可以使用它,但是当输入是 wchar_t
字符串文字时,我很难让它工作。
我遇到了一个相当神秘的错误...
error: accessing value of '"T[=14=]0e[=14=]0s[=14=]0t[=14=]0[=14=]0"' through a 'const char' glvalue in a constant expression
...这似乎是由在 constexpr 上下文中使用 reinterpret_cast 引起的(这显然是不允许的)
我的问题是,是否有 任何 方式将任意数据解释为普通的旧字节数组?我不在乎它有多难看或缺乏可移植性(只要这一切都发生在编译时)。现在,只需解决以 wchar_t
数组作为输入的情况就足够了。
供参考,失败代码如下:
// Details of CRCInternal omitted for brevity
template <size_t len> consteval uint32_t CRC32(const char (&str)[len])
{
return CRCInternal::crc32<len - 1>(str) ^ 0xFFFFFFFFu;
}
template <size_t len> consteval uint32_t CRC32FromWide(const wchar_t (&filename)[len])
{
return CRC32(reinterpret_cast<const char(&)[len * sizeof(wchar_t)]>(filename));
}
void main()
{
CRC32FromWide(L"Test"); // <==== Error
}
C++ 对象模型通常是虚构的,是编写代码的程序员与生成二进制可执行文件的编译器之间的协议。对于可执行文件,对象不存在;它只是存储在内存中的位。因此,您可以利用 C++ 有许多后门这一事实,这些后门可用于有效地假装对象模型不是真实的。其中许多被声明为表现出未定义的行为,但没有编译器会检查这些违反对象模型的行为并阻止您。你违反了约定,但编译器没有注意到,所以你逃脱了它。
常量表达式求值不是这种情况。已编译的可执行文件在 CPU 上运行;常量表达式求值在 编译器内运行。对象模型不必映射到“位”或“内存”或类似的东西;它可以是具有完整生命周期跟踪和分析的真实对象模型。
因此,C++ 标准要求,在持续评估期间,如果您执行 任何显示 UB 的操作,编译器必须检测到这一点并声明您的程序格式错误。此外,constexpr 代码完全禁止使用最大的后门:reinterpret_cast
.
在编译时,对象不是存储中的字节。所以你不能像对待他们一样对待他们。
这一点尤为重要,因为编译器的执行环境和最终二进制文件的执行环境不必相同。如果您正在为某些嵌入式系统进行开发,您所针对的 CPU 的字节序可能与您的 编译器 执行的 CPU 的字节序不匹配在。因此,如果您能够以字节形式访问任何编译时数据,那么您在编译时会得到与在运行时不同的答案。
太糟糕了。
C++20 的 std::bit_cast
存在并且可以提供帮助,但即使那样也不能解决所有问题。如果一个类型是 TriviallyCopyable 并且 不存储指针(除其他外),则该类型仅适用于 constexpr
bit_cast
-ing。这是因为编译时指针不仅仅是地址;它们是一些复杂的数据类型,必须记住它指向的对象(否则,当您 static_cast
它们指向某个不相关的类型并尝试通过错误的类型访问该对象时,将无法检测到)。 =22=]
但是,如果您将类型限制为 constexpr
bit_cast
可用的类型,那么您可以 bit_cast
将它们bit_cast
设置为它们大小的数组。
请注意 constexpr
bit_cast
并不是最容易实现的事情,因为它必须使源对象数据像在目标 CPU 和环境上执行一样工作,而不是编译器在其中执行的那个。所以如果目标是大端机器而源是小端机器,constexpr
bit_cast
必须做字节序转换,而且它必须用关于源对象和目标对象的每个组件类型的具体知识。
我正在尝试实现一些可以接收任意数据位(在编译时已知)并将它们的 CRC 计算为 consteval
的东西,因此我可以将它用于例如使用整数键索引此类数据而无需任何运行时开销。当输入是 char 字符串文字时,我可以使用它,但是当输入是 wchar_t
字符串文字时,我很难让它工作。
我遇到了一个相当神秘的错误...
error: accessing value of '"T[=14=]0e[=14=]0s[=14=]0t[=14=]0[=14=]0"' through a 'const char' glvalue in a constant expression
...这似乎是由在 constexpr 上下文中使用 reinterpret_cast 引起的(这显然是不允许的)
我的问题是,是否有 任何 方式将任意数据解释为普通的旧字节数组?我不在乎它有多难看或缺乏可移植性(只要这一切都发生在编译时)。现在,只需解决以 wchar_t
数组作为输入的情况就足够了。
供参考,失败代码如下:
// Details of CRCInternal omitted for brevity
template <size_t len> consteval uint32_t CRC32(const char (&str)[len])
{
return CRCInternal::crc32<len - 1>(str) ^ 0xFFFFFFFFu;
}
template <size_t len> consteval uint32_t CRC32FromWide(const wchar_t (&filename)[len])
{
return CRC32(reinterpret_cast<const char(&)[len * sizeof(wchar_t)]>(filename));
}
void main()
{
CRC32FromWide(L"Test"); // <==== Error
}
C++ 对象模型通常是虚构的,是编写代码的程序员与生成二进制可执行文件的编译器之间的协议。对于可执行文件,对象不存在;它只是存储在内存中的位。因此,您可以利用 C++ 有许多后门这一事实,这些后门可用于有效地假装对象模型不是真实的。其中许多被声明为表现出未定义的行为,但没有编译器会检查这些违反对象模型的行为并阻止您。你违反了约定,但编译器没有注意到,所以你逃脱了它。
常量表达式求值不是这种情况。已编译的可执行文件在 CPU 上运行;常量表达式求值在 编译器内运行。对象模型不必映射到“位”或“内存”或类似的东西;它可以是具有完整生命周期跟踪和分析的真实对象模型。
因此,C++ 标准要求,在持续评估期间,如果您执行 任何显示 UB 的操作,编译器必须检测到这一点并声明您的程序格式错误。此外,constexpr 代码完全禁止使用最大的后门:reinterpret_cast
.
在编译时,对象不是存储中的字节。所以你不能像对待他们一样对待他们。
这一点尤为重要,因为编译器的执行环境和最终二进制文件的执行环境不必相同。如果您正在为某些嵌入式系统进行开发,您所针对的 CPU 的字节序可能与您的 编译器 执行的 CPU 的字节序不匹配在。因此,如果您能够以字节形式访问任何编译时数据,那么您在编译时会得到与在运行时不同的答案。
太糟糕了。
C++20 的 std::bit_cast
存在并且可以提供帮助,但即使那样也不能解决所有问题。如果一个类型是 TriviallyCopyable 并且 不存储指针(除其他外),则该类型仅适用于 constexpr
bit_cast
-ing。这是因为编译时指针不仅仅是地址;它们是一些复杂的数据类型,必须记住它指向的对象(否则,当您 static_cast
它们指向某个不相关的类型并尝试通过错误的类型访问该对象时,将无法检测到)。 =22=]
但是,如果您将类型限制为 constexpr
bit_cast
可用的类型,那么您可以 bit_cast
将它们bit_cast
设置为它们大小的数组。
请注意 constexpr
bit_cast
并不是最容易实现的事情,因为它必须使源对象数据像在目标 CPU 和环境上执行一样工作,而不是编译器在其中执行的那个。所以如果目标是大端机器而源是小端机器,constexpr
bit_cast
必须做字节序转换,而且它必须用关于源对象和目标对象的每个组件类型的具体知识。