[翻译]类型双关不好玩:C中使用指针重新解释是坏的
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中使用指针重新解释是坏的的更多相关文章
- 11.翻译系列:在EF 6中配置一对零或者一对一的关系【EF 6 Code-First系列】
原文链接:https://www.entityframeworktutorial.net/code-first/configure-one-to-one-relationship-in-code-fi ...
- C#中调用C++的dll的参数为指针类型的导出函数(包括二级指针的情况)
严格来说这篇文章算不上C++范围的,不过还是挂了点边,还是在自己的blog中记录一下吧. C++中使用指针是家常便饭了,也非常的好用,这也是我之所以喜欢C++的原因之一.但是在C#中就强调托管的概念了 ...
- 将String类型的二维数组中的元素用FileOutputStream的write方法生成一个文件
将String类型的二维数组中的元素用FileOutputStream的write方法生成一个文件import java.io.File;import java.io.FileOutputStre ...
- [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置
[翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置 原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET COR ...
- 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.翻译系列:EF 6 Code-First中数据库初始化策略(EF 6 Code-First系列)
原文链接:http://www.entityframeworktutorial.net/code-first/database-initialization-strategy-in-code-firs ...
- MySql数据库类型bit等与JAVA中的对应类型【布尔类型怎么存】
用char(1):可以表示字符或者数字,但是不能直接计算同列的值.存储消耗1个字节 用tinyint:只能表示数字,可以直接计算,存储消耗2个字节 用bit: 只能表示0或1,不能计算,存储消耗小于等 ...
- 维度属性的KeyColumns如果是Integer类型,那么维度表中该列的值不能有为null的
如果维度属性的 KeyColumns的DataType设置为了Integer类型,那么要注意该维度属性列在数据库中不能有为null的值. 例如下图中我们有维度DIM_Vehcile,其中有个维度属性叫 ...
- Swift中的指针类型
Swift编程语言为了能与Objective-C与C语言兼容,而引入了指针类型.尽管官方不建议频繁使用指针类型,但很多时候,使用指针能完成更多.更灵活的任务.比如,我们要实现一个交换两个整数值的函数的 ...
随机推荐
- python os.path
os.path 提供了一些处理文件路径的函数. os.path.abspath(path) 返回绝对路径, 在大多数平台上, os.path.abspath(path) == os.path.norm ...
- JavaScript高级程序设计学习笔记--DOM
DOM(文档对象模型)是针对HTML和XML文档的一个API(应用程序接口). Document类型 文档的子节点 虽然DOM标准规定Document节点的子节点可以是DocumentType,Ele ...
- 解决log4j:WARN No appenders could be found for logger
这个问题是因为我们的log4j.properties文件配置不够完整,所以我们给它配置齐了就不会再出现这个问题. log4j.properties不完整配置如下: log4j.rootLogger=D ...
- windows2008r2的时间同步小结
一.在windows2008r2域控的环境下进行时间同步的配置(当已经拥有可以使用的ntp服务器,并知晓ip,客户端到其网络正常): 客户端的配置过程如下: 1.搜索窗口输入 gpedit.msc 打 ...
- 与你相遇好幸运,使用gulp流程化Typescript后端开发
tsc --init { "compilerOptions": { "module": "commonjs", ...
- python计算文件的md5值
前言 最近要开发一个基于python的合并文件夹/目录的程序,本来的想法是基于修改时间的比较,即判断文件有没有改变,比较两个文件的修改时间即可.这个想法在windows的pc端下测试没有问题. 但是当 ...
- MVC 好记星不如烂笔头之 ---> 页面压缩GIP
public class BaseController : Controller { /// <summary> /// Called before the action method i ...
- Linux第01天
Linux第01天 1.虚拟机安装linux(centos 32bit) 1.1 虚拟机安装前置工作的准备,如内存.硬盘.CPU分配.镜像下载等 1.2 安装方式(图形界面或者命令行 推荐图形界面即直 ...
- js文件上传
DOM: <form id="clueForm" class="insert-dialog" action="/xxx/xxx"met ...
- 浏览器-05 HTML和CSS解析1
一个浏览器内核几个主要部分,HTML/CSS解析器,网络处理,JavaScript引擎,2D/3D图形引擎,多媒体支持等; HTML 解析和 DOM 网页基本结构 一个网页(Page),每个Page都 ...