Clang与GCC编译C++模板变量的符号绑定差异及解决方法
Clang与GCC模板变量特化符号绑定行为差异问题
核心现象
Clang编译无inline修饰的constexpr模板变量特化时生成全局绑定符号,GCC编译相同代码时生成本地绑定符号,该差异会导致从GCC移植到Clang的C++程序在多翻译单元引入相关头文件时触发链接阶段重复符号错误。
测试复现代码
enum struct E : unsigned char { zero = 0, one = 1, two = 2, three = 3, answ = 42, }; template<E> constexpr int doubler = -1; template<> constexpr int doubler<E::zero> = 0; template<> constexpr int doubler<E::one> = 2; template<> constexpr int doubler<E::two> = 4; template<> constexpr int doubler<E::three> = 6; template<> constexpr int doubler<E::answ> = 84;
测试环境与编译命令
测试环境为WSL Ubuntu 20.04.4 LTS,覆盖测试的编译器版本包括:g++、g++-8、g++-9、g++-10、clang++-10、clang++-12,统一使用C++17标准编译,编译命令如下:
for c in g++ g++-10 g++-8 g++-9 clang++-10 clang++-12 ; do $c -c -Wall -Wpedantic -Wextra -std=c++17 meow.cpp -o meow-$c.o ; done
符号表查看结果
编译完成后使用nm、readelf工具查看所有生成目标文件的符号表,执行命令与输出如下:
nm -C *.o meow-clang++-10.o: 0000000000000000 R doubler<(E)0> 0000000000000004 R doubler<(E)1> 0000000000000008 R doubler<(E)2> 000000000000000c R doubler<(E)3> 0000000000000010 R doubler<(E)42> meow-clang++-12.o: 0000000000000000 R doubler<(E)0> 0000000000000004 R doubler<(E)1> 0000000000000008 R doubler<(E)2> 000000000000000c R doubler<(E)3> 0000000000000010 R doubler<(E)42> meow-g++-10.o: 0000000000000000 r doubler<(E)0> 0000000000000004 r doubler<(E)1> 0000000000000008 r doubler<(E)2> 000000000000000c r doubler<(E)3> 0000000000000010 r doubler<(E)42> meow-g++-8.o: 0000000000000000 r doubler<(E)0> 0000000000000004 r doubler<(E)1> 0000000000000008 r doubler<(E)2> 000000000000000c r doubler<(E)3> 0000000000000010 r doubler<(E)42> meow-g++-9.o: 0000000000000000 r doubler<(E)0> 0000000000000004 r doubler<(E)1> 0000000000000008 r doubler<(E)2> 000000000000000c r doubler<(E)3> 0000000000000010 r doubler<(E)42> meow-g++.o: 0000000000000000 r doubler<(E)0> 0000000000000004 r doubler<(E)1> 0000000000000008 r doubler<(E)2> 000000000000000c r doubler<(E)3> 0000000000000010 r doubler<(E)42> readelf -s meow*.o | egrep 'File|doubler' | c++filt File: meow-clang++-10.o 2: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)0> 3: 0000000000000004 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)1> 4: 0000000000000008 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)2> 5: 000000000000000c 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)3> 6: 0000000000000010 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)42> File: meow-clang++-12.o 2: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)0> 3: 0000000000000004 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)1> 4: 0000000000000008 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)2> 5: 000000000000000c 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)3> 6: 0000000000000010 4 OBJECT GLOBAL DEFAULT 3 doubler<(E)42> File: meow-g++-10.o 6: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 doubler<(E)0> 7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 doubler<(E)1> 8: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 doubler<(E)2> 9: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 doubler<(E)3> 10: 0000000000000010 4 OBJECT LOCAL DEFAULT 4 doubler<(E)42> File: meow-g++-8.o 6: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 doubler<(E)0> 7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 doubler<(E)1> 8: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 doubler<(E)2> 9: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 doubler<(E)3> 10: 0000000000000010 4 OBJECT LOCAL DEFAULT 4 doubler<(E)42> File: meow-g++-9.o 6: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 doubler<(E)0> 7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 doubler<(E)1> 8: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 doubler<(E)2> 9: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 doubler<(E)3> 10: 0000000000000010 4 OBJECT LOCAL DEFAULT 4 doubler<(E)42> File: meow-g++.o 6: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 doubler<(E)0> 7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 doubler<(E)1> 8: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 doubler<(E)2> 9: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 doubler<(E)3> 10: 0000000000000010 4 OBJECT LOCAL DEFAULT 4 doubler<(E)42>
结果规律
- 所有测试版本Clang生成的
doubler模板特化符号均为全局绑定:nm输出中符号类型为大写R,readelf输出中标记为OBJECT GLOBAL - 所有测试版本GCC生成的对应符号均为本地绑定:
nm输出中符号类型为小写r,readelf输出中标记为OBJECT LOCAL - 汇编层面对比显示两者生成的汇编逻辑无明显差异
待明确问题
- 相同代码、相同编译标准下,两类编译器生成目标文件的符号绑定存在差异的根本原因是什么?
- 该差异是否来自LLVM汇编器与GNU汇编器的实现区别?
- 如何通过编译选项配置Clang,使其对这类符号生成本地绑定?
实际场景与方案诉求
生产环境中,上述存在ODR违规的模板变量定义在无权修改的第三方头文件中,头文件被项目内多个翻译单元引入:
- GCC编译时,因为符号是本地绑定,不会触发跨翻译单元符号冲突,可正常完成链接
- Clang编译时,因为符号是全局绑定,链接阶段会直接报符号重复定义错误
正确修复方案为给对应模板变量添加
inline关键字,或将其放入匿名命名空间消除ODR违规,但由于头文件维护权限不在己方,需要可落地的临时兼容方案,保证Clang编译的程序可正常完成链接,待后续头文件维护方修复问题后再切换回标准实现。
内容的提问来源于stack exchange,提问作者Bulletmagnet
相关产品推荐
相关产品推荐

