C++17 关键新特性介绍及代码讲解 (5) — attributes for namespaces and enumerators

本文介绍了C++17中针对枚举(enumerators)和命名空间(namespaces)的attribute特性,展示了如何使用attribute进行标记以辅助代码管理和维护。通过示例解释了为枚举添加`end_of_life`属性以标识即将废弃的CPU类型,以及使用`abi_tag`来确保库与应用程序之间的二进制兼容性。文章提供了完整代码示例,帮助读者理解这些新特性的实际应用。

C++17 关键新特性介绍及代码讲解 (5) — attributes for namespaces and enumerators

一句话概括:
在定义 namespaceenum时,允许指定 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 namespaceabi_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 namespaceabi_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;


扩展阅读:[GCC 5.1 libstdc++ 的 Dual ABI]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值