Code Snippets——offsetof分析
这是一个新的系列内容,我把它命名为“Code Snippets”。顾名思义,就是给大家分享一些平时工作中遇到的很有趣的代码片段。例如,在Unreal的C++代码中发现的一些非常好的代码写法、藏有潜在Bug的代码书写问题或者引擎的代码架构分析等。
之前的《图形渲染及优化》系列文章还会继续更新。多开一个新的技术系列也能够方便我想到哪写到哪,更加高效地利用碎片时间,提高文章更新的频次。
作为开篇,这次给大家介绍一个Unreal的C++代码中应用非常广泛的宏定义STRUCT_OFFSET。
定义
STRUCT_OFFSET宏定义的具体作用就是取出类成员变量相对于类对象开始地址的内存偏移。通过它我们就可以获得类对象的内存布局。使用方法如下:
我们跳转到其定义:
这里对注释稍微解释一下。C++11标准禁止使用reinterpret_cast来操作常量表达式。
Clang很好地执行了标准的定义,但是msvc没有(这方面微软一向是特例独行)。在Windows的头文件中 offsetof 的定义仍然使用了reinterpret_cast。这会导致在Windows上使用Clang编译时发生编译错误。
我们进入offsetof 的定义:
这个宏定义看起来很简单是不是?真的有那么简单么?
继续往下看之前,大家可以自己先试着去搞懂每一句的语法意义。测试一下自己的C++功力如何。
分析
通过条件编译语句我们可以看到一共有三个分支:
1.非msvc编译环境;
2.msvc的C编译环境;
3.msvc的C++编译环境;
非msvc编译环境,使用了__builtin_offsetof。这是个Clang/GCC编译器内建的定义,我们不讨论。
msvc的C编译环境的定义看起来比较复杂:
#define offsetof(s,m) ((size_t)&(((s*)0)->m))
我们拆开来分析一下,一共执行了四步操作:
1.((s*)0) 将0转型为s类型指针;
2.((s*)0)->m 访问类型的数据成员m;
3.&(((s*)0)->m) 取数据成员m的地址;
4.(size_t)&(((s*)0)->m) 将地址转型为size_t;
这里的核心操作在于将0转换成(s*)。类以内存空间首地址0作为起始地址,则访问的成员地址自然就为偏移地址。这四步操作都使用的是C-style的强制转换操作符。
msvc的C++环境下的定义最为复杂。跟C环境中的定义的区别是,从第三步开始发生了变化:
&reinterpret_cast((((s*)0)->m))
第三步并没有直接取数据成员m的地址,而是先将数据成员m使用reinterpret_cast转换为“char const volatile&”类型。
这里为什么要使用reinterpret_cast,而不是直接使用C-style的强制转换呢?为什么要转成“char const volatile&”这个类型呢?
首先解释第一个问题。关于reinterpret_cast的具体意义这里就不介绍了,有C++基础的人我相信都应该知道。执行以下的代码,看一下输出结果:
输出:
从代码的执行结果可以看出reinterpret_cast实际上就是将指针改变了类型,地址的值并没有发生变化,这正是我们想要的结果。而这里的C-style的强转的结果却是做了指针偏移,指针指向了B类型的c对象的子对象上。
C++有类的继承,而C没有,所以C代码可以直接使用强转。在C++中C-style的强转并不是我们直观地理解的那样,比C中要复杂一些。
第二个问题拆开看,首先看一下为什么要转换成char类型呢?
在C++中有操作符重载,调用取地址操作符,如果操作的是类类型对象,并且其重载了取地址操作符。那么语义会发生变化,不能保证取出正确的地址。所以在执行取地址之前,我们一定要先将类型强制转换成基本类型,例如:char、int等。
这里之所以转换成char,是因为内存对齐机制的存在。int等其他类型会有默认的地址对齐值,会影响我们取回的地址值。因为char无论在何种环境下,地址对齐值都是1,都不受对齐影响,所以这里使用了char类型。
那为什么这里char类型必须是const volatile&的呢?
要搞懂这个问题必须先了解reinterpret_cast的特性。reinterpret_cast的作用仅仅是将类型进行转换,如果m成员的类型是有const/volatile/&修饰,那么执行reinterpret_cast调用就会发生编译错误。所里这里的类型必须是“const volatile&”,来适应类型无法预知的m成员。
看起来简简单单的一句代码就涉及了这么多内容。这就是为什么C++这门语言我用了这么多年,却仍然不敢说“精通”的原因。这里的每一个知识点掌握的不扎实,在工作中都有可能掉进自己挖的坑里。
作业
做个测试,思考一下下面的代码:
template
inline _Tp* __addressof(_Tp& __r) _GLIBCXX_NOEXCEPT
{
return reinterpret_cast<_Tp*>(&const_cast(reinterpret_cast(__r)));
}
这段代码等同于C++11的std::addressof的作用。是为了取一个类型为_Tp&的对象__r的地址。那么问题来了:
1.为什么不直接使用“&Object”这种方式来取地址呢?
2.请分析这个函数的实现。
我发布在这里的文章都是转至我的微信订阅号,如果你想及时获得最新发布的文章,可以关注我的微信订阅号。