C++17 关键新特性介绍及代码讲解 (5) — attributes for namespaces and enumerators
一句话概括:
在定义 namespace、enum时,允许指定 attributes。 这里的 attributes 指: 具体所使用的C++编译器所提供的语言扩展属性。
gcc 提供的 attributes : 参见 [gcc Attribute Syntax]
clang 提供的 attributes : 参见 [Clang 15.0.0git documentation ATTRIBUTES IN CLANG]
为 enumerators 设置 attributes
在定义 enum时,我们可以单独为每个 enumerator 指定 attributes, 比如 gnu::deprecated:
enum class ARM_CPU {
cpu_R5 [[gnu::unavailable("end_of_life")]], // 'deprecated' enumerator.
cpu_R8,
};
上面代码中, gnu::deprecatedattribute 的作用:早期的项目代码,定义了一个代表 CPU 类型的 enum class: ARM_CPU,
供项目中选择CPU类型的代码引用。后来随着项目的发展,cpu_R5这种 CPU 类型面临逐步停产,为了对项目中使用到该CPU类型的代码进行清理、修改,
我们可以为 cpu_R5这个 enumerator 增加一条 attribute: gnu::deprecated,并且将原因标注为"end_of_life";
然后,在整个项目代码 re-compile 时,引用到cpu_R5的代码,就会触发编译告警,从而锁定需要清理、修改的代码,以便在将来的版本里彻底去除
cpu_R5这个enumerator。 比如,下面代码中的choose_cpu函数在 re-compile 时,就会触发编译告警:
ARM_CPU choose_cpu() {
return ARM_CPU::cpu_R5;
}
gcc编译告警:
warning: ‘ARM_CPU::cpu_R5’ is deprecated: end_of_life
完整 demo 代码参见 gitee.com 上的公开代码仓库 : [attributes_for_namespaces_and_enumerators]
为 namespace 设置 attributes
为 namespace 设置的 attributes,可以对在namespace中定义的类型、函数、变量生效。
目前可以用于 namespace 的 attibutes 种类还很少。
我们举一个 abi_tag的例子,完整 demo 代码参见 gitee.com 上的公开代码仓库 : [attributes_for_namespaces_and_enumerators]
先简单提一下,ABI 即 Application Binary Interface 的缩写,代表二进制代码(比如各种libraray),在目标OS/硬件平台上,被
OS/application/library 调用和访问的接口。ABI 可以看做同编译器/OS 强相关的一种 “访问协议”。
例如,在Linux 上,各开发者的 application 链接、运行时,就是通过 ABI 访问各个 library;所以,各个 library 提供稳定的 ABI 很重要,
这样才能保证 library 更新后,各个 aaplication 的开发者不用 re-build 所有的 application 源代码:只要 library 提供的 ABI
没有改变、且没有影响 application 的源代码级别的 API 更新,所有 application 还是可以继续调用更新后的 library 运行,即各个 application
可以对 library 的更新不感知。
更详细的 ABI 介绍,参见StackOverflow上的这篇问答: [What is an application binary interface (ABI)?]
我们回到 abi_tag 的使用举例:
以 Linux Ubuntu 20.04 作为我们的开发和运行环境。我们有一个数据结构 Box,最初的定义是 2D 的 Box,即只具备 length/width 两个
成员变量,代码如下:
namespace BasicShape {
struct Box {
Box(int l, int w) : length(l), width(w){};
void debug_print() {
std::cout << "debug print of OLD_VER 2D box. length: " << length << " , width: " << width << std::endl;
}
int length;
int width;
};
}
同时,有一个第三方的、负责生成 Box 对象的 library,叫做 producer,提供了接口函数 get_box(),该接口函数返回 {长,宽} 为 {100,200} 的 Box
对象,代码如下:
namespace ProducerLib {
BasicShape::Box get_box() {
// old version 2D box.
int l{100}, w{200};
std::cout << "producer lib create OLD VERSION of Shape: 2D box. length: " << l
<<" , width: " << w << std::endl;
return BasicShape::Box(l, w);
}
}
在 Linux 里,编译链接 producer library 的代码后,得到 libproducer.so,并部署到了当前的开发和运行环境;
我们通过 nm 命令,可以查到接口函数 get_box()在符号表(symbol table)中的名称:
$ nm --demangle libproducer.so | grep -i get_box
00000000000011d9 T ProducerLib::get_box()
有一个开发者 Jack, 他开发的应用程序叫做 consumer, 使用到了 producer library 提供的 get_box() 接口, 代码如下:
int main(){
auto box = ProducerLib::get_box();
box.debug_print();
}
我们在当前开发环境编译、链接 consumer 的代码,我们得到可执行程序 consumer.2dbox, 运行后打印输出如下:
$ ./consumer.2dbox
producer lib create OLD VERSION of Shape: 2D box. length: 100 , width: 200
debug print of OLD_VER 2D box. length: 100 , width: 200
可以看到,producer library 生成 2D Box 的实例,consumer 也是按照 2D Box 进行解析。
时间过去了半年,我们对 Box 进行了“升级”,从 2D Box 升级为 3D Box,增加了 height 成员变量,代码如下:
// upgrade to 3D shape.
namespace BasicShape {
struct Box {
Box(int l, int w, int h) : length(l), width(w), height(h) {
// std::cout << "CTOR of NEW VERSION of Shape: 3D box. length: " << length
// <<" , width: " << width << ", height: " << height << std::endl;
};
void debug_print() {
std::cout << "debug print of NEW_VER 3D box. length: " << length << " , width: "
<< width << ", height: " << height << std::endl;
}
int length;
int width;
// new member.
int height;
};
}
假设在我们的开发、运行环境中,已经部署的 producer library 并没有 re-build,依然是基于 2D Box 进行 build 的版本, 但 Jack 将自己开发的
consumer 代码,基于 3D Box 的新数据结构进行了re-build,得到新的可执行程序 consumer.3dbox;
因为 ABI 的稳定性和一致性,consumer.3dbox 依然可以加载、调用基于 2D Box 进行构建的 producer library; consumer.3dbox 运行后的打印输出:
$ ./consumer.3dbox
producer lib create OLD VERSION of Shape: 2D box. length: 100 , width: 200
debug print of NEW_VER 3D box. length: 100 , width: 200, height: 200
可以看到,producer library 生成的 2D Box {长,宽} 是 {100,200}, 并没有 height 信息; 但是 consumer.3dbox 是按照 3D Box 来解析
Box 对象的,上述打印输出中的
height: 200
实际上是一个并没有赋初值的字段。
上面的 Box/producer/consumer 是非常简单的例子,可以预见,在真正的业务代码中,这种类似情况很容易造成程序运行时莫名其妙的 crash/bug。
这时候,我们需要一种机制显式的暴露出这个问题,让使用 3D Box 的 consumer.3dbox 程序,不会在不感知的情况下,仍然从 producer library 得到
2D Box 实例;这个机制就是对新定义的 Box 类型使用 inline namespace和 abi_tagattribute, 代码如下:
// upgrade to 3D shape.
namespace BasicShape {
#ifdef __clang__
inline namespace A __attribute__((abi_tag("use_3D_Box"))) {
#elif defined(__GNUC__)
inline namespace [[gnu::abi_tag("use_D_Box")]] A {
#endif
struct Box {
Box(int l, int w, int h) : length(l), width(w), height(h) {};
void debug_print() {
std::cout << "debug print of NEW_VER 3D box. length: " << length << " , width: "
<< width << ", height: " << height << std::endl;
}
int length;
int width;
// new member.
int height;
};
}
}
在 Linux 中,基于上述修改过的 Box 类型定义,重新编译 Jack 开发的 consumer 代码,这时如果开发环境中的 producer library,依然是基于
2D Box 进行 build 的版本,则在链接 consumer 时会报错:
$ make
...
[100%] Linking CXX executable consumer
/usr/bin/ld: CMakeFiles/consumer.dir/shape_consumer.cpp.o: in function `main':
shape_consumer.cpp:(.text+0x1c): undefined reference to `ProducerLib::get_box[abi:use_D_Box]()'
collect2: error: ld returned 1 exit status
...
从上面我们看到,此时编译器在链接 get_box()这个接口函数的时候,从 producer library 中,需要查找的 symbol 名称,已是包含 abi_tag 的
名称了,即为
ProducerLib::get_box[abi:use_D_Box]()
我们再看一下当前开发环境中的 libproducer.so 中,get_box()对应的 symbol 名称:
$ nm --demangle libproducer.so | grep get_box
00000000000011d9 T ProducerLib::get_box()
即在 producer library 中, get_box()的 symbol 名称依然是不带 abi_tag 的版本。
所以,通过inline namespace和 abi_tagattribute 特性,我们就能区分开来使用 2D Box 和使用 3D Box 的 get_box()
接口函数版本,避免混淆。
上面的情况是在 consumer 链接的时候报错,如果在 consumer 运行的时候,仍然使用 abi_tag 的前提下,有没有检查报错机制呢? 答案是有的。
为了模拟这个问题,我们先将 producer 的代码,基于 3D Box 的新版本类型,进行re-build,这时候我们就得到了基于 3D Box 类型的“升级版”
producer library, 并部署到当前开发运行环境,然后再 re-build consumer 的代码,编译、链接成功,得到可执行程序 consumer.3dbox.abi_tag;
我们运行该程序,得到打印输出:
$ ./consumer.3dbox.abi_tag
producer lib create NEW VERSION of Shape: 3D box. length: 100 , width: 200, height: 300
debug print of NEW_VER 3D box. length: 100 , width: 200, height: 300
可以看到,这时的 debug print 可以输出 height 的正确值了。
然后,我们在当前运行环境中,将 libproducer.so, 替换为基于 2D Box 构建的老版本,即模拟了当前运行环境中,producer library 是基于 2D Box
构建的版本, consumer.3dbox.abi_tag 是基于 3D Box 构建的版本;我们运行 consumer.3dbox.abi_tag :
$ ./consumer.3dbox.abi_tag
./consumer.3dbox.abi_tag: symbol lookup error: ./consumer.3dbox.abi_tag: undefined symbol: _ZN11ProducerLib7get_boxB9use_D_BoxEv
我们用 c++filt 对没有找到的symbol _ZN11ProducerLib7get_boxB9use_D_BoxEv进行解析:
$ c++filt _ZN11ProducerLib7get_boxB9use_D_BoxEv
ProducerLib::get_box[abi:use_D_Box]()
也即此时基于 3D Box 构建的 consumer.3dbox.abi_tag,在运行时,需要从 libproducer.so 中动态加载 get_foo(),对应的 symbol 名是一个
打上 abi_tag 的版本: ProducerLib::get_box[abi:use_D_Box]() 。
这样就避免了从基于 2D Box 构建的 libproducer.so 中去加载错误的 get_foo() 接口函数实现。
最后再补充一点:
为 inline namespace 设置的 abi_tag ,实际上是一种 implicit tag,即 namespace 本身的名字不会被打上tag,但是当该 namespace 内部定义的数据类型
被其他函数、变量使用到时,编译器会在对应输出的二进制文件中,自动将这些函数、变量的 symbol 名称打上 abi_tag;
扩展阅读:[GCC 5.1 libstdc++ 的 Dual ABI]
e 设置的 abi_tag ,实际上是一种 implicit tag,即 namespace 本身的名字不会被打上tag,但是当该 namespace 内部定义的数据类型
被其他函数、变量使用到时,编译器会在对应输出的二进制文件中,自动将这些函数、变量的 symbol 名称打上 abi_tag;
本文介绍了C++17中针对枚举(enumerators)和命名空间(namespaces)的attribute特性,展示了如何使用attribute进行标记以辅助代码管理和维护。通过示例解释了为枚举添加`end_of_life`属性以标识即将废弃的CPU类型,以及使用`abi_tag`来确保库与应用程序之间的二进制兼容性。文章提供了完整代码示例,帮助读者理解这些新特性的实际应用。

3108

被折叠的 条评论
为什么被折叠?



