我有一些或多或少像这样的代码:
#include <bitset>
enum Flags { A = 1, B = 2, C = 3, D = 5,
E = 8, F = 13, G = 21, H,
I, J, K, L, M, N, O };
void apply_known_mask(std::bitset<64> &bits) {
const Flags important_bits[] = { B, D, E, H, K, M, L, O };
std::remove_reference<decltype(bits)>::type mask{};
for (const auto& bit : important_bits) {
mask.set(bit);
}
bits &= mask;
}
Clang >= 3.6做聪明的事并将其编译为单个 and
指令(然后在其他任何地方内联):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
但是every version of GCC I've tried把它编译成一个巨大的困惑,包括应该静态 DCE 的错误处理。在其他代码中,它甚至会将 important_bits
等效为数据放在代码中!
.LC0:
.string "bitset::set"
.LC1:
.string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
sub rsp, 40
xor esi, esi
mov ecx, 2
movabs rax, 21474836482
mov QWORD PTR [rsp], rax
mov r8d, 1
movabs rax, 94489280520
mov QWORD PTR [rsp+8], rax
movabs rax, 115964117017
mov QWORD PTR [rsp+16], rax
movabs rax, 124554051610
mov QWORD PTR [rsp+24], rax
mov rax, rsp
jmp .L2
.L3:
mov edx, DWORD PTR [rax]
mov rcx, rdx
cmp edx, 63
ja .L7
.L2:
mov rdx, r8
add rax, 4
sal rdx, cl
lea rcx, [rsp+32]
or rsi, rdx
cmp rax, rcx
jne .L3
and QWORD PTR [rdi], rsi
add rsp, 40
ret
.L7:
mov ecx, 64
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
xor eax, eax
call std::__throw_out_of_range_fmt(char const*, ...)
我应该如何编写这段代码,以便两个编译器都能做正确的事情?如果做不到这一点,我应该如何编写它才能保持清晰、快速和可维护?
最佳答案
最佳版本是c++17 :
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
然后
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
返回 c++14 ,我们可以做这个奇怪的把戏:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
auto r = 0ull;
using discard_t = int[]; // data never used
// value never used:
discard_t discard = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
(void)discard; // block unused var warnings
return r;
}
或者,如果我们遇到 c++11 ,我们可以递归求解:
constexpr unsigned long long mask(){
return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return mask(indexes...);
}
Godbolt with all 3 -- 你可以切换 CPP_VERSION 定义,得到相同的程序集。
在实践中,我会尽可能使用最现代的。 14 比 11 好,因为我们没有递归,因此没有 O(n^2) 符号长度(这会增加编译时间和编译器内存使用量); 17 比 14 好,因为编译器不必对那个数组进行死代码消除,而且那个数组技巧很丑。
这 14 个是最令人困惑的。在这里,我们创建一个全 0 的匿名数组,同时作为副作用构造我们的结果,然后丢弃该数组。丢弃的数组中有许多 0,等于我们包的大小加上 1(我们添加它以便我们可以处理空包)。
c++14 的详细解释版本正在做。这是一个技巧/窍门,您必须这样做才能在 C++14 中高效地扩展参数包,这是在 c++17 中添加折叠表达式的原因之一。 .
最好由内而外理解:
r |= (1ull << indexes) // side effect, used
这只是更新 r
与 1<<indexes
为固定索引。 indexes
是一个参数包,所以我们必须扩展它。
剩下的工作就是提供一个参数包来扩展indexes
里面。
一步出:
(void(
r |= (1ull << indexes) // side effect, used
),0)
这里我们将表达式转换为 void
,表示我们不关心它的返回值(我们只想要设置 r
的副作用——在 C++ 中,像 a |= b
这样的表达式也会返回它们设置 a
的值)。
然后我们使用逗号运算符 ,
和 0
丢弃void
"value",并返回值 0
.所以这是一个值为 0
的表达式作为计算 0
的副作用它在 r
中设置了一点.
int discard[] = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
此时,我们展开参数包indexes
.所以我们得到:
{
0,
(expression that sets a bit and returns 0),
(expression that sets a bit and returns 0),
[...]
(expression that sets a bit and returns 0),
}
在 {}
. ,
这个用法不是是逗号操作符,而是数组元素分隔符。这是sizeof...(indexes)+1
0
s,它也在 r
中设置位作为副作用。然后我们分配 {}
数组构造指令discard
.
接下来我们转换discard
至void
-- 如果你创建了一个变量并且从不读取它,大多数编译器都会警告你。如果您将其转换为 void
,所有编译器都不会提示。 ,这是一种说“是的,我知道,我没有使用它”的方式,因此它会抑制警告。
关于c++ - 如何在 C++ 中编写可维护、快速、编译时的位掩码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54786278/