GCC编译宏_GLIBCXX_USE_CXX11_ABI背景分析和实现原理

文章目录

  • 背景
  • 一、使用_GLIBCXX_USE_CXX11_ABI的背景
    • 1. std::string的Copy-On-Write行为
    • 2. list::size()的时间复杂度
    • 3. 使用_GLIBCXX_USE_CXX11_ABI的原因
  • 二、_GLIBCXX_USE_CXX11_ABI的实现原理
    • 1. name mangling
    • 2. inline namespace


背景

最近项目中涉及到使用两个版本的GCC编译得到的第三方库, 由于部分比较久远的第三方库是使用GCC4.8编译的,而新版的第三方库是使用GCC7.3编译的。项目本身是使用GCC7.3编译的, 如果在在项目中如果要混用这两个版本的第三方库,则必须在项目编译的时候,就加上编译宏-D_GLIBCXX_USE_CXX11_ABI=0

本文将介绍为什么需要_GLIBCXX_USE_CXX11_ABI以及其实现原理。


一、使用_GLIBCXX_USE_CXX11_ABI的背景

在GCC说明文档中,需要_GLIBCXX_USE_CXX11_ABI是因为GCC的ABI变化,为什么要引入这个变化呢,主要是为了支持新的C++标准中要禁止std::string的Copy-On-Write行为和支持std::list中size()的时间复杂度为O(1)。

In the GCC 5.1 release libstdc++ introduced a new library ABI that includes new implementations of std::string and std::list. These changes were necessary to conform to the 2011 C++ standard which forbids Copy-On-Write strings and requires lists to keep track of their size.

1. std::string的Copy-On-Write行为

下面的代码:

#include 
#include 

int main() {
    std::string s1("Hello, World.\n");
    std::string s2 = s1;
    printf("%p\n", s1.data());
    printf("%p\n", s2.data());
    
}

在GCC5.1前的编译器上输出s1和s2地址是一样的,测试代码。
从GCC5.1开始,输出的地址则不是一样的,测试代码。
GCC5.1之前的Copy–On-Write行为有什么不好呢,在这里不做详细分析,只看一个简单的例子:

#include 
#include 



int main() {
   std::string s("str");
   const char* p = s.data();
   {
        std::string s2(s);
        (void) s[0];
    }
    
    std::string a("helloworld");
    std::cout << p << std::endl;  // p is dangling
    
}

上面的代码在GCC5.1之前的编译器上输出是什么呢,因为p是悬空指针,所以这里会产生了undefined behavior,比如这里的测试代码,会输出helloworld

为什么会这样呢,是因为GCC5.1之前std::string实现Copy–On-Write行为是采用了和智能指针类似的引用计数方法。p最开始指向string s,s赋值给s2,此时s2s指向同一块内存,但是因为s[0]需要写s对应的内存,由于Copy–On-Write机制,这时,s2指向s的内存,当s2析构后,s2指向的内存也同时析构,p指向的内存也同时析构。

在GCC5.1和5.1之后的编译器,只要使用默认的-D_GLIBCXX_USE_CXX11_ABI=1,那么,上面的代码就是可以正常运行的。

2. list::size()的时间复杂度

这篇C++ std::list中size()的时间复杂度中分析了list在GCC5.1之前和之后的时间复杂度,也和_GLIBCXX_USE_CXX11_ABI 关系密切。

3. 使用_GLIBCXX_USE_CXX11_ABI的原因

从上面1和2的分析,GCC5.1之后,要实现上面的功能,必须修改std::string和std::list的源代码,对于std::list, 它必须添加一个成员记录当前list的元素个数才能保证std::list时间复杂度为O(1)的要求。
我们看下GCC5.1前后std::list, std::string的大小(测试代码)

std::sizeof(std::list) std::sizeof(std::string)
GCC4.8 16 8
GCC7.5 24 32

实际上,GCC5.1上面通过添加_GLIBCXX_USE_CXX11_ABI宏,使同一份代码中同时支持两份ABI。
那么,为什么一定要通过宏的方式,使新版的GCC同时支持新旧ABI呢?这是因为,如果通过改变soname的方式,当同一个程序dlopen两个不同版本的shared library时,必然会发生命名冲突。所以,通过使用编译宏_GLIBCXX_USE_CXX11_ABI,其值要么是0,要么是1,一个单独编译的项目中任意时候都只有一份实现。

二、_GLIBCXX_USE_CXX11_ABI的实现原理

如果当前项目需要链接第三方库的API,比如第三方库编译时使用_GLIBCXX_USE_CXX11_ABI=0,当本项目编译时用的是_GLIBCXX_USE_CXX11_ABI=1,这个时候两个库ABI不兼容,应该报错; 当本项目编译时也用的是_GLIBCXX_USE_CXX11_ABI=0,这个时候两个库ABI是一样的,应该编译通过。

第三方库(_GLIBCXX_USE_CXX11_ABI=0) 第三方库(_GLIBCXX_USE_CXX11_ABI=1)
本项目(_GLIBCXX_USE_CXX11_ABI=0) 编译通过 编译不通过
本项目(_GLIBCXX_USE_CXX11_ABI=1) 编译不通过 编译通过

所以,这里有两个问题:

  • 当链接第三方库时GCC是如何区别不同的ABI版本?
  • 当GCC采用不同的编译宏,项目源代码不变的情况下,项目源代码为何可以编译通过?

这里,第一个问题是借助name mangling的方式解决的,第二个问题是通过inline namespace解决的。

1. name mangling

假设我们的代码中有这样的三个函数:void blargle() and void blargle(int) and void argle::blargle(), 那么编译器会通过name mangling机制为每个函数生成唯一的函数名供外部程序使用或链接使用,上面的函数产生的内部函数名如下:

_Z7blarglev
_Z7blarglei
_ZN5argle7blargleEv

在链接第三方库时,根据name mangling机制提供的唯一的函数名,可以做到只有相同版本的ABI才能链接成功。

2. inline namespace

inline namespace是C++11推出的一个新特性,它的介绍如下:

An inline namespace is a namespace that uses the optional keyword inline in its original-namespace-definition.
Members of an inline namespace are treated as if they are members of the enclosing namespace in many situations (listed below). This property is transitive: if a namespace N contains an inline namespace M, which in turn contains an inline namespace O, then the members of O can be used as though they were members of M or N.

也就是说,对于次低层namespace, 如果在其前面加上inline, 那么外部可以直接忽略这个。

所以,结合name mangling和inline namespace,宏_GLIBCXX_USE_CXX11_ABI的原理类似等同于下面代码中的宏xxx。当定义了宏xxx=1时,程序输出"hello, world from cxx11",反之输出"hello, world from std"

#include 

namespace std {
#if xxx 
inline namespace  cxx11 {
   void printHello() {
       std::cout << "hello, world from cxx11" <<std::endl;
   }
}
#else

   void printHello() {
       std::cout << "hello, world from std" <<std::endl;
   }
#endif

}

int main() {
   std::printHello();

   return 0; 
}

参考:

  1. https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html
  2. https://docs.alliancecan.ca/wiki/GCC_C%2B%2B_Dual_ABI
  3. http://litaotju.github.io/c++/2019/02/24/Why-we-need-D_GLIBCXX_USE_CXX11_ABI=0/
  4. https://developers.redhat.com/blog/2015/02/05/gcc5-and-the-c11-abi
  5. https://blog.csdn.net/xiexievv/article/details/117381343

你可能感兴趣的:(C++,c++,算法,c语言,name,mangling)