全局构造函数在静态链接时为什么不生效? | C & C++
本文讨论了在 C++ 中通过全局构造函数实现隐式注册时遇到的静态链接问题与解决方案。
隐式注册
全局构造函数可以用来完成隐式注册功能(全局构造函即全局对象的构造函数)。
这个需求来源于对异构设备函数的管理。我需要在一个全局 map 中分别注册 CpuDevice、GpuDevice 两种内存管理类。我的目的是在不需要 GPU 的时候不链接相关的库,这样我不仅需要在 cmake 文件中做条件判断,还需要在显式调用注册函数的地方通过编译选项做条件判断。因此在扩展新的设备类型时,需要在至少两个地方增加新的编译选项以及相关判断逻辑。这样做扩展时不够优雅。
通过阅读 TNN 的相关代码,我找到了一种隐式注册的方法:为每个设备定义一个注册类,然后在这个类的构造函数中加入注册设备相关的逻辑,最后在相应设备的 cpp 文件中定义一个注册类的全局对象。这样就可以在程序初始化的时候执行这个构造函数,完成注册的功能。而且如果不编译该 cpp 文件,相关设备就不会被注册,真正做到了无侵入扩展(自己造的词)。
代码如下:
1 | class Register { |
问题
当我真正去实现隐式注册的时候,发现我虽然编译了相关设备的代码,该设备却没有被注册,也就是说这个注册类的构造函数没有被执行。在我几经测试以后,得出了这样的结论:
- 如果 main 函数和注册类的定义不在同一个 cpp 文件中,且以动态库的方式链接,全局构造函数会执行
- 如果 main 函数和注册类的定义不在同一个 cpp 文件中,且以静态库的方式链接,main 函数所在编译单元没有引用到任何注册类所在的编译单元中的函数或者类,全局构造函数不会执行
- 如果 main 函数和注册类的定义不在同一个 cpp 文件中,且以静态库的方式链接,且 main 函数所在编译单元引用到了注册类所在的编译单元中的函数或者类,全局构造函数会执行
注:一个 cpp 文件即为一个编译单元。
实验验证
实验一
1 | // static_register.cpp |
1 | // main.cpp |
1 | # compile.sh |
执行结果为:
1 | link with static lib: |
结论:以上实验说明如果 main 函数和注册类的定义不在同一个编译单元中时,且 main 函数所在编译单元没有引用到任何注册类所在的编译单元中的函数或者类,以静态库链接全局构造函数不会执行,以动态库的方式链接则全局构造函数会执行。
实验二
如果将 main.cpp 改成如下形式:
1 | // main.cpp |
则脚本执行结果为:
1 | link with static lib: |
结论:如果 main 函数和注册类的定义不在同一个 cpp 文件中,且以静态库的方式链接,且 main 函数所在编译单元引用到了注册类所在的编译单元中的函数或者类,全局构造函数会被执行。
原因
当链接器链接静态库时,它只复制静态库中被程序引用的编译单元。所以当 main 函数所在编译单元没有引用到注册类所在的编译单元中的函数或者类时,注册类所在的编译单元根本就不会被复制到最终的可执行文件中,用于静态注册的对象就不会被构造。而使用动态库链接时,所有的编译单元都会被添加到动态库中,所以在运行的时候都可以被链接到。
解决方案
解决思路也很简单,我们可以通过添加编译选项令所有编译单元都强制静态链接。
1 | g++ -o main main.o -Wl,--whole-archive libsr.a -Wl,--no-whole-archive |
–whole-archive 之后定义的静态库所有编译单元都会被链接进来。–no-whole-archive 选项则是关闭这个强制开关。
参考
github.com/Tencent/TNN/blob/master/source/tnn/device/cpu/cpu_device.cc
Why Does My Program Not Link or Not Execute Global Constructor
Using –whole-archive linker option with CMake and libraries with other library dependencies
全局构造函数在静态链接时为什么不生效? | C & C++