从Xcode10不再支持libstdc++说起

9,661 阅读15分钟

众所周知从Xcode10起,苹果摒弃了对libstdc++库的支持转而支持libc++库了。这两个库在Xcode9甚至更早的版本就已经同时存在于系统中并且可供开发者选择,当然在Xcode9时代苹果就已经宣布了将要废弃libstdc++的信息了。

C++标准库

一个app应用程序中如果用到C++相关的代码和类库那么就需要链接C++标准库。C++标准库是一套基于C++语言之上的函数和类库,其早期代码都定义在std命名空间中,大部分类都是用template模板实现的,它主要由IO流,string字符串类,和STL组成。标准库中的实现代码除了分布在没有后缀的头文件(比如vector等大部分模板类)外还有一部分代码被存放到了相应的动态库中,也就是存放在libstdc++.dylib或者libc++.dylib中。至于为什么一个标准库由两个动态库来实现则会在后面进行详细介绍。

C++的规范版本

一门语言总是不可能一成不变的,C++也是如此,随着时间的推移它也会有升级变化的改进需求。但是C++这门语言却不像Swift那样不负责任,它的标准和规范的升级相对来说比较严谨。个人觉得原因是其本身已经非常庞大而且完善了,能升级的基本都是微小的调整了。也许你会发现其他很多语言都是C++这门语言的裁剪版。所以可以说学好C++,走遍天下都不怕! 下面这个表格列出的就是C++的各种版本:

Year C++ Standard Informal name
1998 ISO/IEC 14882:1998[20] C++98
2003 ISO/IEC 14882:2003[21] C++03
2011 ISO/IEC 14882:2011[22] C++11, C++0x
2014 ISO/IEC 14882:2014[23] C++14, C++1y
2017 ISO/IEC 14882:2017[8] C++17, C++1z
2020 to be determined C++20

在C++11标准出来以前,市面上的编译器厂商基本上支持的都是C++98的版本。大部分的书籍或者知识里面的语法和规则都是基于C++98的。C++11主要添加了: 类型自动推导、线程API支持、智能指针内存管理、lamda表达式、STL扩展等能力(如果你想更加详细了解这些新规范,请参考:C++11新特性介绍)。各大编译器厂商为了自身的需要会对规范进行一些定制化处理**(这些语法的标准以及厂商的定制化称为方言Dialect)**。目前比较流行的C++编译器有微软的VC++,GNU组织的gcc(g++), 苹果的LLVM(clang++)等。这些厂商或多或少的对C++的规范进行一些裁剪或者扩充以及对C++的各个版本的支持力度也有所不同。就目前来说主流的编译器几乎都对C++11标准已经完全支持了。

libstdc++.dylib和libc++.dylib

正如前面所说的C++有不同的版本,其中的libstdc++.dylib所代表的就是C++98版本的标准库实现动态库,而libc++.dylib所代表的则是C++11版本的标准库实现动态库。也就是说libc++其实一个更加新的C++标准库实现,它完全支持C++11标准,而苹果的Xcode10将不再支持老版本的标准库libstdc++实现,而是升级为只支持新版本的标准库libc++实现了。某个静态库如果以前是依赖于libstdc++库中的代码,那么这个静态库在Xcode10中被链接时将会报符号找不到的链接错误信息:Undefined symbols for architecture XXX,比如下面的提示:

Undefined symbols for architecture x86_64:
  "std::__throw_length_error(char const*)", referenced from:
      std::vector<int, std::allocator<int> >::_M_insert_aux(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&) in libcpplib.a(cpplib.o)
  "std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::allocator<char>::allocator()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::string::c_str() const", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::allocator<char>::~allocator()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
ld: symbol(s) not found for architecture x86_64

可能你会想按理来说libc++库中的代码实现应该只是libstdc++中代码实现的升级版本,应该要存在着兼容的情况,那为什么还会报符号未定义的错误呢?答案我将会在后面详细说明。

libc++abi.dylib

在查看一个程序运行时所加载的所有C++动态库时,你会发现有一个叫libc++abi.dylib的动态库存在。这个库主要是对C++的: new/delete、try/catch/throw、typeid等关键字的实现支持。这些关键字并不是一些简单的关键字,它们还承载着一定的功能。其实在一些语言中为了使用上的简化往往会将一些能力提炼成为一个特殊的关键字,这样在使用这些能力时往往不再需要编写任何的代码,只要借助对应的关键字就可以简化这些功能的实现。除了C++外一个典型的例子就是GO语言中的chan 关键字。对于C++这门语言来说系统会将上述的那些关键字所实现的功能的代码存放到了一个库中,这个库就是libc++abi.dylib库。下面将简单的介绍一下libc++abi.dylib中都有那些功能:

  1. 在C++中是通过new/delete运算符来实现堆内存的分配和销毁的,因此当在源代码中使用new/delete关键字来分配和销毁对象时,在不重载运算符的前提下编译阶段就会转化为对两个全局函数的调用:
  void * operator new(size_t size);
  void operator delete(void *p);

而这两个函数的实现代码就是存放在libc++abi这个动态库中的。

  1. 在C++中是通过try/catch/throw这几个关键字来捕获和抛出异常的。因此当在源代码中使用这些关键字时,在编译阶段就会转化为对如下函数的调用:
extern _LIBCXXABI_FUNC_VIS _LIBCXXABI_NORETURN void
__cxa_throw(void *thrown_exception, std::type_info *tinfo,
           void (*dest)(void *));

// 2.5.3 Exception Handlers
extern _LIBCXXABI_FUNC_VIS void *
__cxa_get_exception_ptr(void *exceptionObject) throw();
extern _LIBCXXABI_FUNC_VIS void *
__cxa_begin_catch(void *exceptionObject) throw();
extern _LIBCXXABI_FUNC_VIS void __cxa_end_catch();   

来实现异常处理的,而这些函数的实现代码也是存放在libc++abi这个动态库中。

  1. 在C++中可以通过typeid这个关键字来获取对象的类描述信息(RTTI)对象的,C++的类描述类是一个type_info类。你可以从这个类中查看一个C++类的名称,数据成员和函数布局的信息,type_info中的信息就类似于OC的isa所指向的Class类型是一样的。type_info这个类的定义实现也是存放在libc++abi这个动态库中的。

可以看出libc++abi这个动态库是一个支持C++语法的核心库。

Xcode对C++的支持和设置

Xcode中建立的工程项目可以选择使用的C++的方言和C++的标准库版本,在工程的Build Settings中的Apple Clang - Language - C++中的分组中的C++ Language Dialect中选择使用的C++方言类型;C++ Standard Library中选择使用的C++标准库的版本。

C++方言的选项

我们可以通过下面的代码来验证C++语言对于方言的支持选项,因为在C++11中才引入了对lamda表达式的支持,因此你可以在你工程的某个.mm文件的函数实现内写一段lamda表达式:

//test.mm
void foo()
{
   auto f = []{ NSLog(@"test"); };
   f();   
}

默认情况下Xcode对于方言的支持是c++14,因此上面的代码可以被编译通过,如果将C++ Language Dialect的选项改为:C++98[-std=c++98]后就会发现编译时报错:

xxxxxxx\test.mm:52:16: error: expected identifier
    auto f = [] { NSLog(@"test"); };
               ^
1 error generated.

对于方言的选择以及语言类型的选择体现在编译选项-std= 上,这个选项通过查看Xcode的编译消息详情就可以看出:如果文件的后缀是.m,那么-std=后面的值就是C Language Dialect中的选项;如果文件的后缀是.mm,那么-std=后面的值就是C++ Language Dialect中的选项。

C++标准库的选项

Xcode中对于C++标准库C++ Stadard Library选项的选择影响的是链接的标准库动态库的版本以及对应的头文件的搜索路径。

  • 如果你选择的标准库是libc++。那么头文件的搜索路径将会是:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1,并且链接的动态库就是libc++.dylib。

  • 如果你选择的标准库是libstdc++,那么头文件的搜索路径将会是:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/c++/4.2.1,并且链接的动态库就是libstdc++.dylib。

对于标准库的选择,体现在编译选项 -libstd=上,查看Xcode的编译消息详情就可以看出:如果某个文件的后缀是.mm,那么-libstd=后面的值就是C++ Standard Library中的选项值。

在低于Xcode10的IDE中还可以在工程的Build PhasesLink Binary With Libraries中同时添加对libc++.tbd和libstdc++.tbd的链接引用,那么这里就会带来一个问题?为什么可以在一个工程中可以同时引入两个定义了相同内容的类库呢?难道不会在编译时报符号冲突或者重名的错误吗?但实际又不会报符号名冲突的错误,原因就是C++11中引入的一个新特性来保证不会处问题的,这个新特性就是内联命名空间(inline namespace)。

内联命名空间(inline namespace)

假如你在两个不同的动态库中定义和导出了一个相同的函数或者类,并且当将这两个动态库都加入依赖后。一旦在程序中调用那个同名函数时,就会出现函数重复定义或者引入不明确的链接错误。可这个问题却不会发生在不同版本的C++标准库:libstdc++和libc++中,你可以在程序中同时依赖这两个库,而不会产生编译链接错误。我们知道libc++中的内容是libstdc++中的超集,为什么在同时引入两个库时不会报函数或者类名冲突呢? 答案就是C++11中提供了对inline namespace的支持。前面说过老版本C++标准库中的所有类的定义都是在std这个命名空间中。当你选择的是libstdc++是你就会在所有头文件中内容都定义在两个宏:_GLIBCXX_BEGIN_NAMESPACE和_GLIBCXX_END_NAMESPACE之间,比如中的标准输入和输出流对象的定义片段:

_GLIBCXX_BEGIN_NAMESPACE(std)

  extern istream cin;		///< Linked to standard input
  extern ostream cout;		///< Linked to standard output
  extern ostream cerr;		///< Linked to standard error (unbuffered)
  extern ostream clog;		///< Linked to standard error (buffered)

#ifdef _GLIBCXX_USE_WCHAR_T
  extern wistream wcin;		///< Linked to standard input
  extern wostream wcout;	///< Linked to standard output
  extern wostream wcerr;	///< Linked to standard error (unbuffered)
  extern wostream wclog;	///< Linked to standard error (buffered)
#endif
  
_GLIBCXX_END_NAMESPACE

上述的两个宏则定义在<bits/c++config.h>下面,展开这两个宏定义:

# define _GLIBCXX_BEGIN_NAMESPACE(X) namespace X { 
# define _GLIBCXX_END_NAMESPACE } 
namespace std {
  }

因此可以明确早期的C++标准库中的所有类和函数以及变量都是定义在std这个命名空间中的。

当你使用libc++标准库时,你会发现所有头文件中的类和方法都定义在_LIBCPP_BEGIN_NAMESPACE_STD和_LIBCPP_END_NAMESPACE_STD之内。比如中的标准输入和输出流对象的定义片段:

LIBCPP_BEGIN_NAMESPACE_STD

#ifndef _LIBCPP_HAS_NO_STDIN
extern _LIBCPP_FUNC_VIS istream cin;
extern _LIBCPP_FUNC_VIS wistream wcin;
#endif
#ifndef _LIBCPP_HAS_NO_STDOUT
extern _LIBCPP_FUNC_VIS ostream cout;
extern _LIBCPP_FUNC_VIS wostream wcout;
#endif
extern _LIBCPP_FUNC_VIS ostream cerr;
extern _LIBCPP_FUNC_VIS wostream wcerr;
extern _LIBCPP_FUNC_VIS ostream clog;
extern _LIBCPP_FUNC_VIS wostream wclog;

上述两个宏的定义在<__config>中可以看到,展开后的定义如下:

//为了更好理解,我把下面的宏和命令空间中的定义进行了简化处理
#define _LIBCPP_BEGIN_NAMESPACE_STD namespace std {inline namespace __1 {
#define _LIBCPP_END_NAMESPACE_STD  } }

namespace std {
  inline namespace __1 {
  }
}

可以看出在libc++中,所有的类和方法以及变量都不是直接在std这个命名空间中被定义,而是放到其子命名空间std::__1中去了。子命名空间中的 inline关键字则是C++11中为命名空间添加的新关键字:**可以在父命名空间中定义内联的子命名空间,内联的子命名空间可以把其包含的名字导入到父命名空间中,从而在父命名空间中可以直接访问子命名空间中定义的名字,而不用通过域限定符Child::name的形式来访问。**就如下面的例子:

#include <iostream>

void main()
{
     std::__1::cout << "hello1" << std::__1::endl;
     std::cout << "hello2" << std::endl;
}

在C++11中的标准输出流对象cout真实的定义是在std::__1这个命名空间中,但是因为std::__1::是内联子命名空间所以可以通过父命名空间std::来访问。 正是因为内联命名空间的使用,所以工程中的代码是可以切换不同版本的C++标准库的,而且还可以同时链接两个不同的C++标准库libstdc++.dylib和libc++.dylib,因为这两个不同版本中的代码所在命名空间是不一样的,因此不会产生符号重复和冲突的错误!其实C++中的命名空间引入inline关键字就是为了解决版本的兼容性和冲突的。 这也就可以解释当我们把一个依赖libstdc++.dylib的静态库,引入到Xcode10的工程中时会报如下的错误:

Undefined symbols for architecture x86_64:
  "std::__throw_length_error(char const*)", referenced from:
      std::vector<int, std::allocator<int> >::_M_insert_aux(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&) in libcpplib.a(cpplib.o)
  "std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::allocator<char>::allocator()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::string::c_str() const", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::allocator<char>::~allocator()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
ld: symbol(s) not found for architecture x86_64

其原因就是因为出错的C++类是在std::这个命名空间中被定义的(因为C++的命名修饰规则的原因,一个方法或者函数被修饰后的名称是包含其所在的命名空间的)。但是新版本的C++标准库中的所有符号都是在std::__1这个命名空间中,因此链接器将无法找到这个符号。比如标准输入流对象cin在libc++中和libstdc++中的定义就不一样:

__ZNSt3__13cinE    //这是cin在libc++.dylib库中的被修饰过后的真实名字

__ZSt3cin   //这是cin在libstdc++.dylib库中的被修饰过后的真实名字

一个问题:刚才不是说到的内联子命名空间是可以直接通过父命名空间来访问的。为什么这里又不可以呢?上述的内联命名空间的访问只是在编译时是没有问题的,但是在链接这个阶段是不会认内联命名空间的,链接阶段只认被修饰过后的符号,也就是在链接阶段是没有内联命名空间这个概念的。

那既然在Xcode10中报链接错误,又怎么解决这种问题呢?方法有两个:

  • 一个是将你所导入的静态库重新编译,将静态库所依赖的标准库升级为libc++.dylib。(推荐方法)
  • 一种就是将老版本中的libstdc++.dylib库拷贝到Xcode10中去。

Xcode10对libstdc++的支持

在Xcode10中已经找不到libstdc++.dylib这个库了,而且当工程中有依赖libstdc++这个库时或者工程设置里面的C++ Stadard Library选项设置为libstdc++时,就会报如下的错误:

clang: warning: libstdc++ is deprecated; move to libc++ [-Wdeprecated]
ld: library not found for -lstdc++
clang: error: linker command failed with exit code 1 (use -v to see invocation)

前面已经分析了Xcode10对两个标准库支持的来龙去脉,而且也简单的介绍了只要将老版本中的libstdc++.dylib拷贝到新版本的IDE环境中即可,具体的方法和流程大家可以参考如下两篇文章:

blog.csdn.net/box_kun/art… blog.csdn.net/u010960265/…

但其实这样是有风险的,因为Xcode10中对于C++标准库的头文件都是基于C++11的,因此当你通过上述方法引入了老版本的C++标准库时,虽然在编译链接时不会报错正常编译通过,但是在运行时就可能会出现崩溃的问题,尤其是当你的静态库中将某个老的C++标准库中类的对象作为接口或者函数参数暴露出来给外界使用时就有可能因为新老版本的数据结构和内部实现的差异而造成运行时的崩溃!总之为了彻底的解决这些问题,还是要求将你的静态库中的代码在Xcode10中重新编译是最好的解决方案。

参考列表

en.wikipedia.org/wiki/C++ blog.csdn.net/ftell/artic… blog.csdn.net/fengbingchu… blog.csdn.net/Jxianxu/art…


欢迎大家访问我的github地址简书地址