從Xcode10不再支持libstdc++說起

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

作者丨歐陽大哥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++的各種版本:

從Xcode10不再支持libstdc++說起

在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,比如下面的提示:

Undefinedsymbolsforarchitecturex86_64:

” std::__throw_length_error(charconst*)”, referencedfrom:

std::vector< int, std::allocator< int> > ::_M_insert_aux(__gnu_cxx::__normal_iterator< int*, std::vector< int, std::allocator< int> > >, intconst&) inlibcpplib.a( cpplib.o)

” std::basic_string< char, std::char_traits< char>, std::allocator< char> > ::basic_string(charconst*, std::allocator< char> const&)”, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

” std::allocator< char> ::allocator()”, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

” std::string::c_str()const“, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

” std::allocator< char>::~ allocator()”, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

” std::basic_string< char, std::char_traits< char>, std::allocator< char> >::~ basic_string()”, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

ld: symbol( s) notfoundforarchitecturex86_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* operatornew(size_tsize);

voidoperatordelete(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++標準庫的版本。

從Xcode10不再支持libstdc++說起

我們可以通過下面的代碼來驗證C++語言對於方言的支持選項,因為在C++11中才引入了對lamda表達式的支持,因此你可以在你工程的某個.mm文件的函數做到內寫一段lamda表達式:

//test.mm

voidfoo()

{

autof = []{ 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”); };

^

1errorgenerated.

對於方言的選擇以及語言類型的選擇體現在編譯選項-std= 上,這個選項通過查看Xcode的編譯消息詳情就可以看出:如果文件的後綴是.m,那麼-std=後面的值就是C Language Dialect中的選項;如果文件的後綴是.mm,那麼-std=後面的值就是C++ Language Dialect中的選項。

從Xcode10不再支持libstdc++說起

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)

externistream cin; ///< Linked to standard input

externostream cout; ///< Linked to standard output

externostream cerr; ///< Linked to standard error (unbuffered)

externostream clog; ///< Linked to standard error (buffered)

#ifdef_GLIBCXX_USE_WCHAR_T

externwistream wcin; ///< Linked to standard input

externwostream wcout; ///< Linked to standard output

externwostream wcerr; ///< Linked to standard error (unbuffered)

externwostream wclog; ///< Linked to standard error (buffered)

#endif

_GLIBCXX_END_NAMESPACE

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

# define_GLIBCXX_BEGIN_NAMESPACE(X) namespace X {

# define_GLIBCXX_END_NAMESPACE }

namespacestd{

}

因此可以明確早期的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 } }

namespacestd{

inlinenamespace__1 {

}

}

可以看出在libc++中,所有的類和方法以及變量都不是直接在std這個命名空間中被定義,而是放到其子命名空間std::__1中去了。子命名空間中的 inline關鍵字則是C++11中為命名空間添加的新關鍵字:可以在父命名空間中定義內聯的子命名空間,內聯的子命名空間可以把其包含的名字導入到父命名空間中,從而在父命名空間中可以直接訪問子命名空間中定義的名字,而不用通過域限定符Child::name的形式來訪問。就如下面的例子:

#include

voidmain()

{

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的工程中時會報如下的錯誤:

Undefinedsymbolsforarchitecturex86_64:

” std::__throw_length_error(charconst*)”, referencedfrom:

std::vector< int, std::allocator< int> > ::_M_insert_aux(__gnu_cxx::__normal_iterator< int*, std::vector< int, std::allocator< int> > >, intconst&) inlibcpplib.a( cpplib.o)

” std::basic_string< char, std::char_traits< char>, std::allocator< char> > ::basic_string(charconst*, std::allocator< char> const&)”, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

” std::allocator< char> ::allocator()”, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

” std::string::c_str()const“, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

” std::allocator< char>::~ allocator()”, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

” std::basic_string< char, std::char_traits< char>, std::allocator< char> >::~ basic_string()”, referencedfrom:

–[cpplib testfn]inlibcpplib.a( cpplib.o)

ld: symbol( s) notfoundforarchitecturex86_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++ isdeprecated; move tolibc++ [-Wdeprecated]

ld: librarynotfound for-lstdc++

clang: error: linker command failed withexitcode 1(use -v tosee 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中重新編譯是最好的解決方案。