c++ - 如何在 C++ 中编写可维护、快速、编译时的位掩码?

标签 c++ c++11 bit-manipulation

我有一些或多或少像这样的代码:

#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*, ...)

我应该如何编写这段代码,以便两个编译器都能做正确的事情?如果做不到这一点,我应该如何编写它才能保持清晰、快速和可维护?

最佳答案

最佳版本是 :

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;
}

返回 ,我们可以做这个奇怪的把戏:

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;
}

或者,如果我们遇到 ,我们可以递归求解:

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 中高效地扩展参数包,这是在 中添加折叠表达式的原因之一。 .

最好由内而外理解:

    r |= (1ull << indexes) // side effect, used

这只是更新 r1<<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 .

接下来我们转换discardvoid -- 如果你创建了一个变量并且从不读取它,大多数编译器都会警告你。如果您将其转换为 void,所有编译器都不会提示。 ,这是一种说“是的,我知道,我没有使用它”的方式,因此它会抑制警告。

关于c++ - 如何在 C++ 中编写可维护、快速、编译时的位掩码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54786278/

相关文章:

c++ - 如何将 sort() 的比较参数与模板和自定义数据结构一起使用

C++11线程id,有更简单的命名约定吗?

c++ - 对象的第二个实例无法正常工作

c++ - 体系结构 x86_64 的 undefined symbol - 显示头文件中的函数

c++ - 在 C++11 中创建 N 元素 constexpr 数组

c++ - 测试类型 V 是否属于不带可变参数的元组 <...> 的类型

python - 修改静态变量是线程安全的吗?

javascript - 了解大端与字符串之间的相互转换

c++ - 如何从头开始编写 std::floor 函数

linq - 与 NHibernate 和 Oracle 的按位与