原文地址

Type punning isn't funny: Using pointers to recast in C is bad.

C语言中一个重新解释(reinterpret)数据类型的技巧有可能造成严重的bug。Apple知道,这也是为什么NSRectToCGRect的实现并没有按照文档的声明执行。我在这里展示一种安全的在你的代码中重新解释数据的技术。

Apple的NSRectToCGRect文档称函数如此定义:

CGRect NSRectToCGRect(NSRect nsrect) {
return (*(CGRect *)&(nsrect));
}

如果你看过许多C代码,那么你可能已经见过此类实现。你不能直接将一个结构声明为另一个来重新解释——即使他们有相同的域——所以一种普遍的做法是声明一个指针然后类型转换该指针。

虽然函数的功能是确定的,但这种实现并非如此。实际上,函数的实现应该像这样:

NS_INLINE CGRect NSRectToCGRect(NSRect nsrect) {
union _ {NSRect ns; CGRect cg;};
return ((union _ *)&nsrect)->cg;
}

区别在哪?为什么要创建一个union?为什么不直接通过指针进行类型转换?

类型双关(type punning)

尽管通过指针进行类型转换是一种常用的方法,但它实际上是一种错误实践,有潜在的风险。造成错误的原因就是类型双关。

类型双关

是一种指针重叠(pointer aliasing),两个指针指向内存同一位置但将其解释为不同的数据类型。编译器会将两个指针视为不相干的指针。对于任何可以通过两个指针访问的数据,类型双关会产生依赖问题。

大多数时候,类型双关不会导致问题,它是C标准中的未定义行为,但通常会按照我们的预期运行。

然而,当你想要通过优化选项提高程序性能时,就会出现问题。例如,当你打开XCode中的"Enforce Strict Aliasing"选项(或者GCC中的-fstrict_aliasing),就会出现不可预测的结果。在强重叠/严格别名(strict aliasing)下,编译器可能会按照错误顺序处理事务,甚至将指令完全漏过。

具体来说,错误只会出现在当你当你想要在同一个函数域内间接引用两个指针(或者想访问它们的共享数据)时,仅仅创建指针是安全的。

双关错误的例子

在NSRectToCGRect函数存在之前,有其它的一些代码:

NSRect ellipseBounds;
ellipseBounds.origin.x = ;
ellipseBounds.origin.y = ;
ellipseBounds.size.width = WIDGET_SIZE - 1.0;
ellipseBounds.size.height = WIDGET_SIZE - 1.0;
ellipseBounds = NSInsetRect(ellipseBounds, , ); CGContextAddEllipseInRect(context, *(CGRect *)&ellipseBounds);
CGContextFillPath(context);

这段代码创建了一个NSRect类型的数据,并在使用它之前将其转换为了一个CGRect类型。

在这个例子中,如果打开-fstrict_aliasing选项,GCC会将NSInsetRect操作放到CGContextAddEllipseInRect之后执行,因为当使用另一种数据类型间接引用ellipseBounds时,类型双关破坏了两个操作之间的依赖关系。

使用Union解决问题

传统的解决方法是使用union,像之前的代码中展示的,union需要包含源类型和目标类型,只需要在从目标类型中读数据之前将其声明为源类型即可。

根据C标准,任何包括类型双关的行为都是具体实现(implementation specific)的。所以在“标准”层面,使用union并不解决问题。根据标准,如果你在union的一个域中定义了数据,你就必须从同一个域中读回数据。

幸运的是,GCC明确地允许了不同的做法。根据GCC的文档:

从不同的union成员中读取数据,而不是从最近写入的成员中读取,(被称为类型双关),这种行为是普遍的。即使使用-fstrict-aliasing选项,类型双关也是允许的,只要内存是通过union类型访问的。

完美。

一个安全地重新解释数据的宏

很简单:

#define UNION_CAST(x, destType) \
(((union {__typeof__(x) a; destType b;})x).b)

所以你可以使用如下代码将一个float变量转换为int:

int myInt = UNION_CAST(myFloat, int);

你会发现我并不排斥使用内联函数,没有给union命名,也没有在类型转换前声明指针。Apple的NSRectToCGRect函数做了这些,但实际上都是不必要的。编译器会抛弃这些额外工作,无需在意。

结论

创建一个指针,然后通过指针类型转换是重新解释数据的最普遍方法,尽管流行,但你不该使用它。总是通过union来重新解释数据,这会在优化时防止大量的错误。

[翻译]类型双关不好玩:C中使用指针重新解释是坏的的更多相关文章

  1. 11.翻译系列:在EF 6中配置一对零或者一对一的关系【EF 6 Code-First系列】

    原文链接:https://www.entityframeworktutorial.net/code-first/configure-one-to-one-relationship-in-code-fi ...

  2. C#中调用C++的dll的参数为指针类型的导出函数(包括二级指针的情况)

    严格来说这篇文章算不上C++范围的,不过还是挂了点边,还是在自己的blog中记录一下吧. C++中使用指针是家常便饭了,也非常的好用,这也是我之所以喜欢C++的原因之一.但是在C#中就强调托管的概念了 ...

  3. 将String类型的二维数组中的元素用FileOutputStream的write方法生成一个文件

      将String类型的二维数组中的元素用FileOutputStream的write方法生成一个文件import java.io.File;import java.io.FileOutputStre ...

  4. [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置

    [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置 原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET COR ...

  5. 18.翻译系列:EF 6 Code-First 中的Seed Data(种子数据或原始测试数据)【EF 6 Code-First系列】

    原文链接:https://www.entityframeworktutorial.net/code-first/seed-database-in-code-first.aspx EF 6 Code-F ...

  6. 6.翻译系列:EF 6 Code-First中数据库初始化策略(EF 6 Code-First系列)

    原文链接:http://www.entityframeworktutorial.net/code-first/database-initialization-strategy-in-code-firs ...

  7. MySql数据库类型bit等与JAVA中的对应类型【布尔类型怎么存】

    用char(1):可以表示字符或者数字,但是不能直接计算同列的值.存储消耗1个字节 用tinyint:只能表示数字,可以直接计算,存储消耗2个字节 用bit: 只能表示0或1,不能计算,存储消耗小于等 ...

  8. 维度属性的KeyColumns如果是Integer类型,那么维度表中该列的值不能有为null的

    如果维度属性的 KeyColumns的DataType设置为了Integer类型,那么要注意该维度属性列在数据库中不能有为null的值. 例如下图中我们有维度DIM_Vehcile,其中有个维度属性叫 ...

  9. Swift中的指针类型

    Swift编程语言为了能与Objective-C与C语言兼容,而引入了指针类型.尽管官方不建议频繁使用指针类型,但很多时候,使用指针能完成更多.更灵活的任务.比如,我们要实现一个交换两个整数值的函数的 ...

随机推荐

  1. Pyqt 获取打包二进制文件中的资源

    记得有一次打开一个单独exe程序,点击btn中的一个帮助说明按钮,在同级目录下就多出一个help.chm 文件并自动打开. 那这个exe肯定是把help.chm 打包到exe中,当我触发“帮助”按钮的 ...

  2. ajax完整格式

    $.ajax({             url: encodeURI( "@Url.Action("NavList", "Home")"  ...

  3. 2015ACM/ICPC亚洲区长春站

    5532 Almost Sorted Array 题目大意:给你一个序列,如果它拿掉其中一个数后,可以是该序列成为非递减或非递增序列,则输出YES. 有两种思路,第一种代码比较简单,是LIS,复杂度n ...

  4. Codeforces 307 div2 E.GukiZ and GukiZiana 分块

    time limit per test 10 seconds memory limit per test 256 megabytes input standard input output stand ...

  5. Delphi Webbrowser 修改 textarea 值 百度

    有个按钮 调用  <a href="#" onclick="$.ajax({url: '/redmine/journals/edit/29606.js', type ...

  6. 分布式缓存技术memcached学习(五)—— memcached java客户端的使用

    Memcached的客户端简介 我们已经知道,memcached是一套分布式的缓存系统,memcached的服务端只是缓存数据的地方,并不能实现分布式,而memcached的客户端才是实现分布式的地方 ...

  7. 最详细易懂的CRC-16校验原理(附源程序)

    from:http://www.openhw.org/chudonganjin/blog/12-08/230184_515e6.html 最详细易懂的CRC-16校验原理(附源程序) 1.循环校验码( ...

  8. 简单CSS3动画制作

    本贴已重新编辑至http://www.cnblogs.com/fastmover/p/4977358.html 最近需要用到了一些CSS3动画,基本用Animate.css(https://githu ...

  9. Page-encoding specified in XML prolog (UTF-8) is different from that specified in page directive (utf-8)

    org.apache.jasper.JasperException:xxx.jsp(1,1) Page-encoding specified in XML prolog (UTF-8) is diff ...

  10. 一个获取指定目录下一定格式的文件名称和文件修改时间并保存为文件的python脚本

    摘自:http://blog.csdn.net/forandever/article/details/5711319 一个获取指定目录下一定格式的文件名称和文件修改时间并保存为文件的python脚本 ...