已签名/未签名的别名规则是否按预期工作?

时间:2019-01-08 05:30:34

标签: c++ language-lawyer strict-aliasing reinterpret-cast type-punning

以下是C ++ 17形式的规则([basic.lval] / 8),但在其他标准中(在C ++ 98中为“ lvalue”而不是“ glvalue”)看起来很相似:

  

8如果程序尝试通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:

     

(8.4)—一种类型,它是与对象的动态类型相对应的有符号或无符号类型

规则听起来像“除非有X,否则您将拥有UB”,但这并不意味着如果您有X,您将不会像预期的那样获得UB!实际上,根据标准的版本,执行X是有条件的UB还是无条件的UB。

让我们看下面的代码:

int i = -1;
unsigned j = reinterpret_cast<unsigned&>(i);

此代码的行为是什么?

C ++ 98和C ++ 11

[expr.reinterpret.cast] / 10(在C ++ 11中为/ 11)(强调是我的):

  

如果类型为“指针”的表达式可以将类型为T1的左值表达式强制转换为类型“对T2的引用”   使用reinterpret_cast可以将“ T1到T1”显式转换为“ T2指针”。这是一个   参考强制转换reinterpret_cast(x)与转换具有相同的效果   带有内置&和*运算符的* reinterpret_cast(&x)。 结果是一个引用的左值   到与源左值相同的对象,但类型不同

因此reinterpret_cast<unsigned&>(i)左值引用int对象i,但类型为usigned。初始化需要初始化表达式的值,这正式意味着将左值到右值转换应用于左值。

[conv.lval] / 1:

  

非功能性,非数组类型T的左值可以转换为右值。如果T不完整   类型,则表示需要进行此转换的程序格式错误。 如果左值引用的对象不是   类型为T的对象,而不是从T派生的类型的对象,或者如果对象未初始化,则为   

我们的unsigned类型的左值未引用unsigned类型的对象,这意味着行为是不确定的。

C ++ 14和C ++ 17

在这些标准中,情况稍微复杂一些,但是规则有所放松。 [expr.reinterpret.cast] / 11讲了同样的话:

  

结果指向与源glvalue相同的对象,但是具有指定的类型。

关于UB的冒犯性用语已从[conv.lval] / 1中删除:

  

非功能性,非数组类型T的glvalue可以转换为prvalue 。如果T是不完整的类型,则必须进行这种转换的程序格式错误。 如果T是非类类型,则prvalue的类型是T的cv不合格版本。否则,prvalue的类型为T。

但是L到R转换读取哪个值? [conv.lval] /(2.6)(在C ++ 17中为(/(3.4)))回答了这个问题:

  

…glvalue指示的对象中包含的值是prvalue结果

unsigned左值reinterpret_cast<unsigned&>(i)表示i int对象,其值为-1,并且从L到R转换得到的prvalue具有{{1 }}类型。 [expr] / 4说:

  

如果在对表达式进行求值时,未在数学上定义结果,或者该结果不在其类型的可表示值范围内,则行为不确定。

unsigned绝对不在prvalue表达式的-1类型的可表示值范围内,因此行为是不确定的。

如果unsigned对象包含[0,INT_MAX]范围内的值,则将定义行为。

当通过i glvalue访问unsigned对象时,同样的理由也适用。除非对象的值在[0,INT_MAX]范围内,否则在C ++ 98和C ++ 11中为UB,在C ++ 14和C ++ 17中为UB。

因此,与普遍认为该别名规则允许将对象重新解释为包含对应的带符号/无符号类型的值的说法相反,它不允许这样做。对于[0,INT_MAX]范围内的值,有符号和无符号类型的对象具有相同的表示形式(“ 有符号整数类型的非负值范围是对应的无符号整数类型的子范围,表示形式在两种类型中,每种类型的值都相同”。“在C ++ 17中,[basic.fundamental] / 3表示)。很难将这种访问称为“重新解释”,更不用说这是C ++ 14之前的无条件UB。

那么规则([basic.lval] /(8.4))的目的是什么?

1 个答案:

答案 0 :(得分:1)

这是defect report 2214的主题,内容为:

  

部分:6.9.1 [basic.fundamental]状态:C ++ 17发布者:Richard Smith日期:2015-12-15

     

[在2017年2月/ 3月的会议上通过。]

     

根据6.9.1 [基本,基本]第3段,

     
    

有符号整数类型的非负值范围是相应的无符号整数类型的子范围,并且每个相应的有符号/无符号类型的值表示应相同。 (这是C ++ 11和C ++ 14版本的措辞,尽管段落编号可能不同-n.m。)

  
     

C11对应的措辞是

     
    

有符号整数类型的非负值范围是相应的无符号整数类型的子范围,并且每种类型中相同值的表示形式相同。

  
     

C措辞可以说更清晰,但是它失去了C ++措辞的含义,即带符号类型的符号位是相应的无符号类型的值表示的一部分。

     

拟议决议(2017年1月):

     

将6.9.1 [基本,基本]第3段更改如下:

     
    

...标准和扩展的无符号整数类型统称为无符号整数类型。有符号整数类型的非负值范围是对应的无符号整数类型的子范围,两种类型中的每种相同值的表示形式相同,并且每个相应的有符号/无符号类型应相同。标准带符号整数类型...

  

这显然一直是我们的意图。 C ++ 17刚刚修改了措辞。

C和C ++标准从不打算允许将负值重新解释为无符号,反之亦然。野外有几种带符号的整数表示形式(例如,一个人的补码,两个人的补码,符号和大小),并且该标准不强制要求它们中的任何一个,因此它无法规定这种重新解释的效果。它们可以被实现定义,但是考虑到陷阱表示的可能性,这样做没有真正的好处。 “实现定义的结果或陷阱”与“未定义的”一样好。