问题背景
最近负责编译器的升级,从gcc 4.8.5 版本升级到 gcc 6.3.0 版本,所以把所有依赖的第三方库都重新使用 gcc 6.3.0 版本编译器再编译一遍。但是过程并不顺利,在业务 project 中依赖第三方库,链接时出现链接失败的情况,提示很多函数 undefined。
问题现象
这里先通过一个实验来复现一下问题。
test_cpp 工程如下:
liba
a.h
#include <iostream>
#include <string>
#include <vector>
namespace liba {
void test_a(std::vector<std::string>& vec);
}
a.cpp
#include "a.h"
namespace liba {
void test_a(std::vector<std::string>& vec) {
std::cout << "liba::test_a, vec.size=" << vec.size() << std::endl;
}
}
Makefile
SRC := $(wildcard *.cpp)
OBJ := $(patsubst %.cpp, %.o, $(SRC))
TARGET := liba.a
CXXFLAGS := -g -std=c++11 -D_GLIBCXX_USE_CXX11_ABI=1
$(TARGET): $(OBJ)
ar rsc $@ $<
$(OBJ): $(SRC)
g++ -c $< -o $@ $(CXXFLAGS)
clean:
rm -f *.o *.a
.PHONY: clean
main
main.cpp
#include <iostream>
#include <string>
#include <vector>
#include "liba/a.h"
int main() {
std::vector<std::string> vec;
liba::test_a(vec);
return 0;
}
Makefile
SRC := $(wildcard *.cpp)
OBJ := $(patsubst %.cpp, %.o, $(SRC))
TARGET := main
CXXFLAGS := -g -std=c++11 -D_GLIBCXX_USE_CXX11_ABI=0
LDFLAGS := ../liba/liba.a
INCFLAGS := -I../
$(TARGET): $(OBJ)
g++ -o $@ $< $(LDFLAGS)
$(OBJ): $(SRC)
g++ -o $@ -c $< $(CXXFLAGS) $(INCFLAGS)
clean:
rm -f main *.o
.PHONY: clean
编译 test_cpp
分别编译 liba 和 main:
[root@3b06228be59b liba]# make
g++ -c a.cpp -o a.o -g -std=c++11 -D_GLIBCXX_USE_CXX11_ABI=1
ar rsc liba.a a.o
[root@3b06228be59b liba]# cd ../main
[root@3b06228be59b main]# make
g++ -o main.o -c main.cpp -g -std=c++11 -D_GLIBCXX_USE_CXX11_ABI=0 -I../
g++ -o main main.o ../liba/liba.a
main.o: In function `main':
/test_cpp/main/main.cpp:8: undefined reference to `liba::test_a(std::vector<std::string, std::allocator<std::string> >&)'
collect2: error: ld returned 1 exit status
make: *** [main] Error 1
/test_cpp/main/main.cpp:8: undefined reference to `liba::test_a(std::vector<std::string, std::allocatorstd::string >&)'
提示这个函数没有定义:liba::test_a(std::vector<std::string, std::allocatorstd::string >&)。
使用 nm 命令查看 liba.a 是否对 liba::test_a 有定义。
[root@3b06228be59b main]# nm -C ../liba/liba.a | grep test_a
0000000000000089 t _GLOBAL__sub_I__ZN4liba6test_aERSt6vectorINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESaIS6_EE
0000000000000000 T liba::test_a(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&)
[root@3b06228be59b main]#
我们看到 liba::test_a 已经定义。但是和以上提示 undefined 的函数原型不一样。是:
liba::test_a(std::vector<std::__ cxx11::basic_string<char, std::char_traits, std::allocator >, std::allocator<std::__ cxx11::basic_string<char, std::char_traits, std::allocator > > >&)
原因分析
根据问题的现象,我们知道在在 liba 和 main 看到的 liba::test_a 的函数原型不一样,分别是:
- liba::test_a(std::vector<std::string, std::allocatorstd::string >&)
- liba::test_a(std::vector<std::__ cxx11::basic_string<char, std::char_traits, std::allocator >, std::allocator<std::__cxx11::basic_string<char, std::char_traits, std::allocator > > >&)
所以,我们要先分析为什么会出现不一样的情况。
根本问题就在 std::__cxx11
,所以去看看 gcc 6.3.0 编译器的源码吧。
从源码中发现 std::__cxx11
其实跟 _GLIBCXX_USE_CXX11_ABI 这个宏有关系,在 string 的实现中,当 _GLIBCXX_USE_CXX11_ABI = 1 时,会使用 std::__cxx11
相关的实现。
先看 main 的 Makefile:
SRC := $(wildcard *.cpp)
OBJ := $(patsubst %.cpp, %.o, $(SRC))
TARGET := main
CXXFLAGS := -g -std=c++11 -D_GLIBCXX_USE_CXX11_ABI=0
LDFLAGS := ../liba/liba.a
INCFLAGS := -I../
$(TARGET): $(OBJ)
g++ -o $@ $< $(LDFLAGS)
$(OBJ): $(SRC)
g++ -o $@ -c $< $(CXXFLAGS) $(INCFLAGS)
clean:
rm -f main *.o
.PHONY: clean
再看 liba 的 Makefile:
SRC := $(wildcard *.cpp)
OBJ := $(patsubst %.cpp, %.o, $(SRC))
TARGET := liba.a
CXXFLAGS := -g -std=c++11 -D_GLIBCXX_USE_CXX11_ABI=1
$(TARGET): $(OBJ)
ar rsc $@ $<
$(OBJ): $(SRC)
g++ -c $< -o $@ $(CXXFLAGS)
clean:
rm -f *.o *.a
.PHONY: clean
果然,问题出在 _GLIBCXX_USE_CXX11_ABI 这个宏。这个宏设置为 1 和 0 是使用了 std::string 的两种不同的代码实现。
这个问题在现实工作中的现象是,大家都使用了 blade-build 进行代码编译管理,BLADE_ROOT 配置文件大家都是拷贝来拷贝去的,BLADE_ROOT 中默认指定 -D_GLIBCXX_USE_CXX11_ABI=0,至于什么原因,已经无法追溯了,但是我猜可能就是为了解决在gcc 6.3.0 版本的编译中依赖了使用低版本编译器 gcc 4.8.5 编译出来的第三方依赖库的问题,因为 gcc 4.8.5 版本编译器默认 _GLIBCXX_USE_CXX11_ABI = 0,而 gcc 6.3.0 编译器默认 _GLIBCXX_USE_CXX11_ABI = 1。
再拓展一下 _GLIBCXX_USE_CXX11_ABI 出现的原因:
它主要是为了支持新的C++标准中要禁止 std::string 的 Copy-On-Write 行为和支持 std::list 中size() 的时间复杂度为 O(1)。
std::string 的 Copy-On-Write
先上个代码。
#include <cstdio>
#include <iostream>
#include <string>
int main() {
std::string str1 = "Hello, World.";
std::string str2 = str1;
printf("%p\n", str1.data());
printf("%p\n", str2.data());
return 0;
}
使用 Copy-On-Write 的实现时,str1 和 str2 指向了同一个内存地址(即 "Hello, World." 的地址),只有对 str1 或 str2 进行修改时才会重新 copy 一块新的内存,在上面进行修改。
std::list 中size() 的时间复杂度为 O(1)
复杂度为 O(1) 实际上是增加了一个成员,用于记录 std::list 的 size。