位操作,return 0 如果 x != 0,否则非零

Bit manipulation, return 0 if x != 0, or nonzero otherwise

我正在编写一个函数 is_zero 如果 x != 0 则应该 return 0 否则非零。我不允许使用任何常量。例如,x == 0 是不允许的。 (也不允许使用 == 运算符)

我唯一可以使用的运算符是 =~^*(取消引用)、&|<<>>+.

我现在编写函数的方式是 return 0 如果 x != 0,但它仍然 returns 0 即使 x == 0,这是不应该做的。我尝试了各种组合,但考虑到限制,这个家庭作业问题似乎是不可能的。我在这里发帖作为最后的努力。

任何人都可以告诉我如何让我的代码在 x == 0 时 return 除了 0 之外的其他东西,而在 returning 0x != 0?

int is_zero(int x) {
    return (x ^ x);
}

如果您希望代码在不假设 int 的特定大小和表示形式的情况下工作,我认为不可能解决这个问题。但这适用于 32 位整数和二进制补码表示:

int is_zero(int x) {
    int zero = (x^x);
    int one = ~(~zero + ~zero);
    int five = one + one + one + one + one;
    int thirtyone = (one << five) + ~zero;
    return ~((x >> thirtyone) | ((~x + one) >> thirtyone));
}

它使用多个赋值来构造常量,但如果需要,代码可以折叠成一个表达式。

工作原理

(x >> thirtyone) 如果 x 为负则为 -1,否则为 0。 同样,如果 x 为正,则 (~x + one) >> thirtyone 为 -1,否则为 0。

如果 x 为零,则这两个表达式的按位 or0,否则为 -1。如果 x 为零,则按位 ~ 给出 -1,否则给出 0。

(差不多)字长独立解

它不是完全独立于字长的,但是可以扩展上面的解决方案以适用于 16、32 和 64 位整数(尽管仍然取决于二进制补码表示)。代码小心不要一次移动超过 15 位(否则如果 int 是 16 位,结果是未定义的行为):

int is_zero(int x) {
    int zero = (x^x);
    int one = ~(~zero + ~zero);
    int four = one + one + one + one;
    int k15 = (one << four) + ~zero;
    return ~((x >> k15 >> k15 >> k15 >> k15 >> k15) |
             ((~x + one) >> k15 >> k15 >> k15 >> k15 >> k15));
}

请注意,这建立在@Paul Hankin 关于如何将 0 和 1 作为常量的回答的基础上。 (如果你被允许使用“/”运算符,1 将更容易通过(x/x)获得。因此,一种理论上的方法可能是

  • 按照@PaulHankin 的回答获取常量 0 和 1(很好,顺便说一句)
  • 使用这些 "constants",将 x 向左移动所有可能的位位置,对结果进行或运算(将原始中的任何“1”移动到任何可能的位位置,最后给出 -1(0xffff)。由于我们似乎不知道整数的大小,为了安全起见,请重复 64 次或 128 次(或 1000 次)甚至更多次,并涵盖所有合理的 int 大小。(该行可能会变成有点长...)增加太多班次不会有什么坏处。

x = x | x << one | x << one+one | x << one + one + one ....

如果移位大于实际字宽,这实际上可能以 UB 结束,但通过写入修复(感谢@PaulHankin 的评论)

x = x | x << one | x << one << one | x << one << one << one ....

  • 如果 x 最初是 0,那么现在 x 中将有一个 0。如果它设置了任何位(即不同于 0),则您在最初设置的一组之上有任何位。
  • 做同样的事情向右移动,你会得到 0xffff(或者任何你的字长)或者 0,然后
  • XOR 上面的结果与 -1(从 0 - 1 中作为常量获得)- 这将导致原始 0 为 -1,原始任何其他结果为 0,return,在最后.

我很确定我不想编写那个程序。它可能很容易超出编译器的任何合理行长度。

原来有一个通用的解决方案,它不依赖于字的大小,甚至不依赖于二进制补码算法。这是:

int is_zero(int x)
{
    unsigned    y   = x;
    unsigned    c0  = y^y;
    unsigned    c1  = ~(~c0 + ~c0);
    unsigned    c1h = ~c0 ^ (~c0 >> c1);
    unsigned    y2  = y + ~c0;
    unsigned    t1  = y ^ y2;
    unsigned    t2  = t1 & y2;
    unsigned    s   = t2 & c1h;
    int         r   = s >> c1;

    return r;
}

请注意,所有内容均以无符号形式完成,这是避免溢出问题所必需的,也是强制右移零填充的必要条件。我将参数和 return 值保留为带符号的整数,第一个和最后一个赋值只是改变了 signed/unsigned 行为(最后一个移位只是为了避免溢出——见下面的评论)。

解决方案实际上非常简单。如前所述,恒定生成是微不足道的。零是通过与自身进行异或运算获得的。全 1 是它的按位补码。一个是全 1 的异或,全 1 左移一个。

到目前为止,这一切都是微不足道的。棘手的部分是不管字的大小如何让它工作。为此,它构造一个值,如果 x 非零,则其高位为 0,如果 x 为零,则其高位为 1。然后它用一个常量屏蔽它,该常量是高位位置中的单个 1,它由所有 1 的异或运算和所有 1 的右移 1 构成。移位保证为零填充(而不是符号扩展) 因为该值是无符号的。

请注意,s 在高位位置为零或一。最后的 return 值 rs 右移一位,以避免在分配回有符号整数时溢出。此修复由 Paul Hankin 建议。这使得最终的 return 值要么为零,要么为零,然后是一个,然后是全零。

如果你想避免函数开头和结尾的有符号和无符号之间的隐式转换,你可以使用联合来为值取别名:

int is_zero(int x)
{
    union {int s; unsigned u;} su;
    su.s = x;
    unsigned    y   = su.u;
    unsigned    c0  = y^y;
    unsigned    c1  = ~(~c0 + ~c0);
    unsigned    c1h = ~c0 ^ (~c0 >> c1);
    unsigned    y2  = y + ~c0;
    unsigned    t1  = y ^ y2;
    unsigned    t2  = t1 & y2;
    unsigned    s   = t2 & c1h;
    su.u = s;
    int         r = su.s;

    return r;
}

在这种情况下,s 的最终移位是不必要的,并且 return 值要么是零,要么是一个后跟全零。请注意,C90 标准不允许混合代码和声明,因此如果这是一个问题,您必须将声明与赋值分开,但最终结果是相同的。

@TomKarzes 回答的小优化:

int is_zero2(int x)
{
    unsigned y   = x;             // use (unsigned) x
    unsigned c0  = y ^ y;         // = 0
    unsigned c1  = ~(~c0 + ~c0);  // = 1
    unsigned c1h = ~(~c0 >> c1);  // = mask for sign bit
    unsigned t2  = ~(y | -y);     // mask for bits below (not including) lowest bit set
    unsigned s   = t2 & c1h;      // sign bit: 0 = (x != 0), 1 = (x == 0)
    int      r   = s >> c1;       // prevent overflow
    return r;
}