從Xcode10不再支援libstdc++說起
眾所周知從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 | |
2003 | ISO/IEC 14882:2003 | |
2011 | ISO/IEC 14882:2011 | , C++0x |
2014 | ISO/IEC 14882:2014 | , C++1y |
2017 | ISO/IEC 14882:2017 | , C++1z |
2020 | to be determined |
在C++11標準出來以前,市面上的編譯器廠商基本上支援的都是C++98的版本。大部分的書籍或者知識裡面的語法和規則都是基於C++98的。C++11主要新增了: 型別自動推導、執行緒API支援、智慧指標記憶體管理、lamda表示式、STL擴充套件等能力(如果你想更加詳細瞭解這些新規範,請參考:)。各大編譯器廠商為了自身的需要會對規範進行一些定製化處理(這些語法的標準以及廠商的定製化稱為方言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中都有那些功能:
在C++中是透過new/delete運算子來實現堆記憶體的分配和銷燬的,因此當在原始碼中使用new/delete關鍵字來分配和銷燬物件時,在不過載運算子的前提下編譯階段就會轉化為對兩個全域性函式的呼叫:
void * operator new(size_t size); void operator delete(void *p);
而這兩個函式的實現程式碼就是存放在libc++abi這個動態庫中的。
在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 Handlersextern _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這個動態庫中。
在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.mmvoid foo(){ auto f = []{ NSLog(@"test"); }; f(); }
預設情況下Xcode對於方言的支援是c++14,因此上面的程式碼可以被編譯透過,如果將C++ Language Dialect的選項改為:C++98[-std=c++98]
後就會發現編譯時報錯:
xxxxxxxtest.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 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之間,比如<iostream>中的標準輸入和輸出流物件的定義片段:
_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之內。比如<iostream>中的標準輸入和輸出流物件的定義片段:
LIBCPP_BEGIN_NAMESPACE_STD#ifndef _LIBCPP_HAS_NO_STDINextern _LIBCPP_FUNC_VIS istream cin;extern _LIBCPP_FUNC_VIS wistream wcin;#endif#ifndef _LIBCPP_HAS_NO_STDOUTextern _LIBCPP_FUNC_VIS ostream cout;extern _LIBCPP_FUNC_VIS wostream wcout;#endifextern _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環境中即可,具體的方法和流程大家可以參考如下兩篇文章:
https://blog.csdn.net/box_kun/article/details/80756832
https://blog.csdn.net/u010960265/article/details/82754136
但其實這樣是有風險的,因為Xcode10中對於C++標準庫的標頭檔案都是基於C++11的,因此當你透過上述方法引入了老版本的C++標準庫時,雖然在編譯連結時不會報錯正常編譯透過,但是在執行時就可能會出現崩潰的問題,尤其是當你的靜態庫中將某個老的C++標準庫中類的物件作為介面或者函式引數暴露出來給外界使用時就有可能因為新老版本的資料結構和內部實現的差異而造成執行時的崩潰!總之為了徹底的解決這些問題,還是要求將你的靜態庫中的程式碼在Xcode10中重新編譯是最好的解決方案。
作者:歐陽大哥2013
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/755/viewspace-2822835/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 從 JSON 說起JSON
- 從Promise的Then說起Promise
- iOS逆向——從RSA說起iOS
- 從程式猿入行說起
- 從兩道面試題說起面試題
- 從CSS盒子模型說起CSS模型
- 動畫篇-從UIView動畫說起動畫UIView
- 璧說:從資料庫連線池說起資料庫
- 從 CALayer 的 Position、AnchorPoint 說起
- 叢集通訊:從心跳說起
- 從用SwiftUI搭建專案說起SwiftUI
- 架構之路(8):從CurrentUser說起架構
- 架構之路(八)從CurrentUser說起架構
- 曹工說Tomcat1:從XML解析說起TomcatXML
- 從 gRPC 的重試策略說起RPC
- 後蘋果時代轉型,從裁員說起蘋果
- 從Kotlin的類開始說起Kotlin
- 夯實Java:從物件導向說起Java物件
- 決策樹詳解,從熵說起熵
- 從放棄到入門-Yaf(從控制器說起)
- 從函數語言程式設計說起函數程式設計
- 從 Google 的一道面試題說起·Go面試題
- 視訊稽核:從優衣庫試衣間說起
- 從一道筆試題題說起筆試
- 從FMDB執行緒安全問題說起執行緒
- 從JavaScript中的類陣列物件說起JavaScript陣列物件
- FinOps實踐,從降本增效說起
- Flutter異常監控 - 壹 | 從Zone說起Flutter
- 故障分析 | 從 data_free 異常說起
- 從 Redux 說起,到手寫,再到狀態管理Redux
- JavaScript 事件迴圈(1) —— 從 setTimeout 說起JavaScript事件
- 從淘汰Oracle資料庫的事情說起Oracle資料庫
- JS的隱式轉換 從 [] ==false 說起JSFalse
- 從開發框架提高開發效率說起框架
- 資料是什麼——從數字說起
- JUnit原始碼分析(四)——從Decorator模式說起原始碼模式
- 從 Form1.Caption = “Hello World”說起 (轉)ORMAPT
- 微軟宣佈即日起不再為Windows 10 1709版本提供技術支援微軟Windows