澄清按位运算符,非负逻辑运算符和类型转换如何交互

时间:2016-04-18 05:17:29

标签: c bit-manipulation language-lawyer

问题

作为一名初出茅庐的C语言律师,我遇到了一种情况,我不确定我是否理解C规范在逻辑上是否正确保证。

根据我的理解,“按位运算符”(&|&)将在非负值上直观地预期任何C的整数类型char / short / int / long / etc,无论signed还是{{1} }) - 无论底层对象表示如何。

这是否正确理解了什么是C中没有严格定义明确的行为?

关键点

在许多方面,这个问题归结为一致的实现是否允许将两个非陷阱,非负值作为位操作符的操作数,并产生陷阱表示结果(来自操作本身,而不是来自将结果分配/解释为/作为不合适的类型。)

实施例

请考虑以下代码:

unsigned

请注意以上评论#include <limits.h> #define MOST_SIGNIFICANT_BIT = (unsigned char )((UCHAR_MAX >> 1) + 1) /* ... in some function: */ unsigned char byte; /* Using broad meaning of "byte", not necessarily "octet" */ int val; /* val is assigned an arbitrary _non-negative_ value at runtime */ byte = val | MOST_SIGNIFICANT_BIT; 在运行时收到非负值(迂腐:所述值可以用val的类型表示)< / p>

我的期望是val具有最高有效位设置,而低位是的底部byte位的纯二进制表示(无填充位或陷阱表示) CHAR_BIT - 1的数值

即使val的类型更改为任何其他整数类型,我希望这仍然是正确的,但我希望这个保证在val的值变为负数后立即消失(没有人保证所有实现的结果),或者val的类型被更改为任何非整数类型(违反C对按位运算符的定义的约束)。

自应答

我发布了对当前理解的解释,因为我对此非常有信心,但我正在寻找对我的任何误解的更正,并且会接受任何更好/更正的答案,而不是我的。

2 个答案:

答案 0 :(得分:2)

  

在许多方面,这个问题归结为一致的实现是否允许将两个非陷阱,非负值作为位操作符的操作数,并产生陷阱表示结果

这由C11第6.2.6.2节(C99类似)涵盖。有一个脚注澄清了更多技术文本的意图:

  

无论如何,对有效值的算术运算除了作为异常条件(如溢出)的一部分之外,不能生成陷阱表示,并且对于无符号类型不会发生这种情况。

按位运算符是算术运算as discussed here

在这个脚注中,“陷阱表示”排除了特殊情况“负零”。负零可能会或可能不会导致UB,但它有自己的文本(在6.2.6.2中)也与陷阱表示文本分开。

因此,您的问题实际上可以针对有符号和无符号值进行回答;唯一危险的情况是“负零”可能性。 (非负输入不能发生)。

答案 1 :(得分:1)

为什么(我认为)这是(可能)正确

按位运算符&|^被定义为对转换后的操作数的实际二进制表示进行操作:操作数被称为经历&#34;通常的算术转换&#34;。

据我了解,从逻辑上讲,当你使用两个具有非负值的整数类型表达式作为其中一个运算符的操作数时,无论填充位还是陷阱表示,它们的值位都将排列&#34;排队& #34;:因此,结果的值位将具有与您所期望的相匹配的数值,如果您只是假设一个&#34;纯二进制表示&#34 ;

踢球者只要你从有效(非陷阱),非负值开始作为操作数,操作数应该总是提升为可以表示它们的两个值的整数类型,因此可以在逻辑上表示这三个操作中的任何一个的结果值。您也永远不会遇到可能的问题,例如: &#34;签署零&#34;,因为将自己限制为非负值可避免​​此类问题。只要结果用作可以保存结果值的类型(或作为无符号整数类型),您就不会引入其他类似/相关的问题。

这些操作可以从非负非陷阱操作数生成陷阱表示吗?

最后一次C99 / C11草案的脚注44/53和45/54似乎表明这种不确定性的答案取决于foo | barfoo & bar和{{1}被认为是&#34;算术运算&#34;。如果是,那么在给定非陷阱值的情况下,不允许它们产生陷阱表示结果。

C99和C11标准草案的索引将按位运算符列为&#34;算术运算符&#34;的子集,建议是。虽然C89没有以这种方式组织索引,但我的C编程语言(第2版)有一个名为&#34;算术运算符&#34;其中包括foo ^ bar+-*/,将按位运算符留给单独的部分。换句话说,关于这一点,没有明确的答案。

在实践中,我不知道任何会发生这种情况的系统(考虑到两个操作数的非负值的约束),以及它的价值。

可以考虑以下内容:预期类型%(并且基本上被C99和C11祝福)能够访问类型的基础对象表示的所有位 - 似乎意图是按位运算符可以与unsigned char一起正常工作 - 在大多数现代系统中,它将整数提升为unsigned char,其余为int:因此{{1}似乎不太可能允许使用} {}},unsigned intfoo | bar生成陷阱表示 - 至少foo & barfoo ^ bar都可以保存在foo中},如果结果已分配到bar

从前两点概括说这是一个非问题是非常诱人的,虽然我不会称之为严格的证明。

应用于示例

这就是为什么我认为这是正确的,并且会按预期工作:

  1. unsigned char主题unsigned char至&#34;通常的算术转换&#34;:根据定义,UCHAR_MAX >> 1将适合UCHAR_MAX或{{ 1}},因为在大多数系统上UCHAR_MAX可以代表int的所有值,而在少数系统上,unsigned int必须能够代表所有值unsigned char`,这只是一个&#34;整数提升&#34;在这种情况下。

  2. 因为位移是根据值而不是按位表示来定义的,所以int是UCHAR_MAX除以2的商。(让我们调用此结果unsigned char

  3. unsigned int使通常的算术转换成为两个参数:如果UCHAR_MAX >> 1适合UCHAR_MAX_DIV_2,则结果为UCHAR_MAX_DIV_2 + 1,否则,它是UCHAR_MAX。无论哪种方式,转换都会在整数提升时停止。

  4. int的结果是一个正值,当转换为int时,它将具有unsigned int最高位设置,并清除所有其他位(因为转换将保留数值,并且UCHAR_MAX_DIV_2 + 1被严格定义为具有纯二进制表示而没有任何填充位或陷阱表示 - 但即使没有这样的显式要求,结果值将设置最重要的位。

  5. unsigned char演员unsigned char在这种情况下实际上是多余的 - 演员阵容或没有演员阵容,它会受到&#34;通常的算术转换的影响&# 34;按位“或”时。 (但在其他情况下可能会有用)。

  6. 上面的五个步骤将在几乎所有编译器上进行常量折叠 - 但是如果没有合适的编译器不应该以与代码的语义不同的方式进行常量折叠。 ,所以以上所有都适用。

  7. unsigned char是有趣的地方:与(unsigned char)MOST_SIGNIFICANT_BIT不同,val | MOST_SIGNIFICANT_BIT和其他二元运算符 定义于操纵二进制表示的术语。 <<>>都经常进行算术转换:比特或陷阱表示的布局等细节可能意味着不同的二进制表示,但应保留:给定两个相同整数类型的变量,保持非负,非陷阱值,值位应该&#34;排队&#34;正确的,所以我实际上期望|产生正确的值(让我们调用这个结果val)。我没有明确保证这一步骤不会产生陷阱表示,但我不相信它会实现C的实现。

  8. MOST_SIGNIFICANT_BIT强制进行转换:将整数类型(只要该值为非陷阱值)转换为较小的无符号整数类型已明确定义:在这种情况下,< em> value 以模val | MOST_SIGNIFICANT_BIT为模。由于VAL_WITH_MSB_SET被声明为,因此最终结果是byte = VAL_WITH_MSB_SET的余下UCHAR_MAX + 1除以val

  9. 解释它无法工作的地方

    如果byte是负值或非整数类型,我们就不幸运了,因为不再有逻辑确定性得到二进制“或”的位将具有相同的&#34;含义&#34;:

    1. 如果VAL_WITH_MSB_SET是有符号整数类型但具有负值,那么即使UCHAR_MAX + 1被提升为兼容类型,即使值位&#34;排队&# 34;,结果没有任何保证含义(因为C不能保证负数如何编码),结果(对于任何编码)也没有相同的含义,特别是在最后一步分配到val

    2. 如果val具有非整数类型,则它已违反C标准,该标准约束MOST_SIGNIFICANT_BITunsigned charval运算符操作&#34;整数类型&#34;。但是如果你的编译器允许它(或者你使用了工会等做了一些技巧),那么你无法保证每个位的含义,因此你设置的位没有意义。