[翻译]类型双关不好玩: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 xml资源解密(破解)软件说明与下载(v1.2)
欢迎加入qq群551278936讨论krpano技术以及获取最新软件. 该软件已经不再维护,现在已经被KRPano资源分析工具取代,详情参见 http://www.cnblogs.com/reac ...
- Linux下hostname与hosts
参考:http://wp.fungo.me/linux/what-the-hell-is-hostname.html hostname 就是机器名,内核中的一个变量,可临时修改也可以永久修改 /etc ...
- NSString相关操作
//创建一个字符串对象 NSString * str_1 = @"Hello"; //字面量方法 ; NSString * str_2 = [NSString stringWith ...
- 两个已排序数组进行合并后的第K大的值--进军硅谷
我看到此题时,首先想到一个一个比较遍历过去,这是最暴力的方法,后面我想到了已经排序,那么对每个数组进行二分,然后比较这两个值.此书第三种解法,挺不错,只对那个长度较小的数组进行二分查找,保证i+j-1 ...
- 时间戳与日期时间互转C语言
/* * ctime.h * * Created on: May 19, 2016 * Author: root */ #ifndef CTIME_H_ #define CT ...
- Git配置姓名和邮箱问题
今天在安装Git for windows完成后,配置姓名和邮箱.按照廖雪峰老师的步骤,在开始菜单里找到"Git"->"Git Bash",单击后并没有跳出 ...
- (转)ShardedJedisPool的使用
package com.test; import java.util.ArrayList; import java.util.List; import redis.clients.jedis.Jedi ...
- mvc+webapi 单元测试
1.前言 现在这个项目已经有阶段性的模块完成了,所以就想着对这些模块进行单元测试,以保证项目的代码的质量.首先虽然标题是mvc+webapi实质上我只是对mvc进行的测试.用的时候vs的unit te ...
- haproxy 实现多域名证书https
[root@ha02 keys]# openssl genrsa - Generating RSA bit long modulus ....+++ ......................... ...
- CountDownLatch
使用的实例: @Override public <V> V get(Future<V> future) { final CountDownLatch l = new Count ...