歡迎光臨
每天分享高質量文章

從Xcode10不再支持libstdc++說起

作者:歐陽大哥2013

鏈接:https://www.jianshu.com/p/44915099abaf

眾所周知從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++的各種版本:


在C++11標準出來以前,市面上的編譯器廠商基本上支持的都是C++98的版本。大部分的書籍或者知識裡面的語法和規則都是基於C++98的。C++11主要添加了: 型別自動推導、執行緒API支持、智慧指標記憶體管理、lamda運算式、STL擴展等能力(如果你想更加詳細瞭解這些新規範,請參考:C++11新特性介紹)https://www.cnblogs.com/feng-sc/p/5710724.html。各大編譯器廠商為了自身的需要會對規範進行一些定製化處理(這些語法的標準以及廠商的定製化稱為方言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<intstd::allocator<int> >::_M_insert_aux(__gnu_cxx::__normal_iterator<int*, std::vector<intstd::allocator<int> > >, int const&) in libcpplib.a(cpplib.o)
  "std::basic_string<charstd::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<charconst&)", 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<charstd::char_traits<char>, std::allocator<char> >::~basic_string()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
ldsymbol(snot 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這個動態庫中的。

2、在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這個動態庫中。

3、在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	est.mm:52:16error: 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 Phases的Link 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;       ///
  extern ostream cout;      ///
  extern ostream cerr;      ///
  extern ostream clog;      ///

#ifdef _GLIBCXX_USE_WCHAR_T
  extern wistream wcin;     ///
  extern wostream wcout;    ///
  extern wostream wcerr;    ///
  extern wostream wclog;    ///
#endif

_GLIBCXX_END_NAMESPACE

上述的兩個宏則定義在下麵,展開這兩個宏定義:

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 

void main()
{
     std::__1::cout <"hello1" endl;
     std::cout <"hello2" 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<intstd::allocator<int> >::_M_insert_aux(__gnu_cxx::__normal_iterator<int*, std::vector<intstd::allocator<int> > >, int const&) in libcpplib.a(cpplib.o)
  "std::basic_string<charstd::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<charconst&)", 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<charstd::char_traits<char>, std::allocator<char> >::~basic_string()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
ldsymbol(snot 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環境中即可,具體的方法和流程大家可以參考如下兩篇文章:

https://blog.csdn.net/box_kun/article/details/80756832

https://blog.csdn.net/u010960265/article/details/82754136

但其實這樣是有風險的,因為Xcode10中對於C++標準庫的頭檔案都是基於C++11的,因此當你通過上述方法引入了老版本的C++標準庫時,雖然在編譯鏈接時不會報錯正常編譯通過,但是在運行時就可能會出現崩潰的問題,尤其是當你的靜態庫中將某個老的C++標準庫中類的物件作為接口或者函式引數暴露出來給外界使用時就有可能因為新老版本的資料結構和內部實現的差異而造成運行時的崩潰!總之為了徹底的解決這些問題,還是要求將你的靜態庫中的代碼在Xcode10中重新編譯是最好的解決方案。

參考串列

  • https://en.wikipedia.org/wiki/C++

  • https://blog.csdn.net/ftell/article/details/79312007?utm_source=copy

  • https://blog.csdn.net/fengbingchun/article/details/79252110

  • https://blog.csdn.net/Jxianxu/article/details/80641476


編號320,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂