[翻译]类型双关不好玩: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语言兼容,而引入了指针类型.尽管官方不建议频繁使用指针类型,但很多时候,使用指针能完成更多.更灵活的任务.比如,我们要实现一个交换两个整数值的函数的 ...
随机推荐
- 【krpano】高德地图导航插件(源码+介绍+预览)
简介 krpano可以利用js调用第三方网页版地图,因此可以实现导航效果,用来帮助用户导航到我们全景所在的位置. 效果截图如下,在手机端点击左侧按钮,便会对用户进行定位,跳转至高德地图进行导航 ...
- Lua 读表代码,自动生成字段名字
表格格式为 INT STRING INT INT INT INT INT Id Desc Type SceneId OpenId MaxPliesp ClearancePlies 第1关 - 第2关 ...
- MariaDB 多主一从 搭建测试
背景: 目前MySQL依然只支持一个Slave从一个Master复制数据,虽然也可以做到一主多备(M->S),双主复制(M<->M)等架构,但是局限性依然很大.由于项目的要求,需要各 ...
- PHP 链接数据库1(连接数据库&简单的登录注册)
对 解析变量的理解 数据库的名称和表的名称不能重复 从结果中取出的数据 都是以数组的形式取出的 1.PHP查询数据库中的某条信息 //PHP链接数据库 /*1.造链接对象 IP地址 用户名 密码 ...
- mac 安装nginx
首先准备工作,打开mac终端 1.安装brew 输入命令 curl -LsSf http://github.com/mxcl/homebrew/tarball/master | sudo tar x ...
- CDN -- 集合
weui https://cdnjs.cloudflare.com/ajax/libs/weui/0.4.3/style/weui.min.css 百度静态资源库 http://cdn.code.ba ...
- MFC中CListCtrl控件的使用方法
定义一个派生类CViewList 派生于CListCtrl 在要包含该控件的类(CView)中定义一个CViewList类型的变量 CViewList m_wndListView; 在CView响应的 ...
- Power BI for Office 365(三)Power Pivot
在Power Pivot中可以从各种数据源中根据你的需求来创建数据模型,并且可以根据需要随时刷新这些数据.在上一篇中,Anna已经准备好了加载到Power Pivot中的数据.Power Pivot就 ...
- Codeforces Round #370 - #379 (Div. 2)
题意: 思路: Codeforces Round #370(Solved: 4 out of 5) A - Memory and Crow 题意:有一个序列,然后对每一个进行ai = bi - bi ...
- ASP.NET常见面试题及答案(130题)
1.C#中 property 与 attribute(抽像类)的区别,他们各有什么用处,这种机制的好处在哪里?答:property和attribute汉语都称之为属性.不过property是指类向外提 ...