日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Item 14: 如果函数不会抛出异常就把它们声明为noexcept

發(fā)布時間:2025/6/17 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Item 14: 如果函数不会抛出异常就把它们声明为noexcept 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本文翻譯自modern effective C++,由于水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

博客已經(jīng)遷移到這里啦

在C++98中,異常規(guī)范(exception specifications)是一個不穩(wěn)定因素。你必須總結(jié)出一個函數(shù)可能會拋出的異常類型,所以如果函數(shù)的實現(xiàn)被修改了,異常規(guī)范可能也需要被修正。改變異常規(guī)范則又可能影響到客戶代碼,因為調(diào)用者可能依賴于原先的異常規(guī)范。編譯器通常不會提供幫助來維護“函數(shù)實現(xiàn),異常規(guī)范以及客戶代碼”之間的一致性。最終,大多數(shù)程序員覺得C++98的異常規(guī)范不值得去使用。

C++11中,對于函數(shù)的異常拋出行為來說,出現(xiàn)了一種真正有意義的信息,它能說明函數(shù)是否有可能拋出異常。是或不是,一個函數(shù)可能拋出一個異常或它保證它不會拋出異常。這種“可能或絕不”二分的情況是C++11異常規(guī)范的基礎(chǔ),這種異常規(guī)范從本質(zhì)上替換了C++98的異常規(guī)范。(C++98風(fēng)格的異常規(guī)范仍然是有效的,但是它們是被棄用了的。)在C++11中,無條件的noexcept就說明這個函數(shù)保證不會拋出異常。

在設(shè)計接口的時候,一個函數(shù)是不是應(yīng)該這么聲明(noexcept)是一個需要考慮的問題。函數(shù)的異常拋出行為是客戶最感興趣的部分。調(diào)用者能詢問一個函數(shù)的noexcept狀態(tài),并且這個詢問的結(jié)果能影響異常安全(exception safety)或著調(diào)用代碼的性能。因此,一個函數(shù)是否是noexcept和一個成員函數(shù)是否是cosnt,這兩個信息使同樣重要。當你知道一個函數(shù)不會拋出異常的時候卻不聲明它為noexcept,就屬于一個不好的接口設(shè)計。

但是,這里還有一個額外的動機讓我們把noexcept應(yīng)用到不會產(chǎn)生異常的函數(shù)上:它允許編譯器產(chǎn)生更好的目標代碼。為了理解為什么會這樣,讓我們檢查一下C++98和C++11中,對于一個函數(shù)不會拋出異常的不同解釋。考慮一個函數(shù)f,它保證調(diào)用者永遠不會收到異常。兩種不同的表示方法:

int f(int x) throw(); //C++98風(fēng)格int f(int x) noexcept; //C++11風(fēng)格

如果,運行時期,一個異常逃離了f,這違反了f的異常規(guī)范。在C++98的異常規(guī)范下,f的調(diào)用者的調(diào)用棧被解開了,然后經(jīng)過一些不相關(guān)的動作,程序終止執(zhí)行。在C++11的異常規(guī)范下,運行期行為稍微有些不同:調(diào)用棧只有在程序終止前才有可能被解開。

解開調(diào)用棧的時機,以及解開的可能性的不同,對于代碼的產(chǎn)生有很大的影響。在一個noexcept函數(shù)中,如果一個異常能傳到函數(shù)外面去,優(yōu)化器不需要保持運行期棧為解開的狀態(tài),也不需要確保noexcept函數(shù)中的對象銷毀的順序和構(gòu)造的順序相反(譯注:因為noexcept已經(jīng)假設(shè)了不會拋出異常,所以就算異常被拋出,大不了就是程序終止,而不可能處理異常)。使用“throw()”異常規(guī)范的函數(shù),以及沒有異常規(guī)范的函數(shù),沒有這樣的優(yōu)化靈活性。三種情況能這樣總結(jié):

RetType function(params) noexcept; //優(yōu)化最好RetType function(params) throw(); //沒有優(yōu)化RetType function(params); //沒有優(yōu)化

這種情況就能作為一個充足的理由,讓你在知道函數(shù)不會拋出異常的時候,把它聲明為noexcept。

對于一些函數(shù),情況變得更加強烈(更多的優(yōu)化)。move操作就是一個很好的例子。假設(shè)你有一份C++98代碼,它使用了std::vector。Widget通過一次次push_back來加到std::vector中:

std::vector<Widget> vw;...Widget w;... //使用wvw.push_back(w); //把w加到vw中...

假設(shè)這個代碼工作得很好,然后你也沒有興趣把它改成C++11的版本。但是,基于C++11的move語法能提升原來代碼的性能(當涉及move-enabled類型時)的事實,你想做一些優(yōu)化,因此你要保證Widget有一個move operation,你要么自己寫一個,要么用函數(shù)生成器來實現(xiàn)(看Item 17)。

當一個新的元素被添加到std::vector時,可能std::vector剩下的空間不足了,也就是std::vector的size等于它的capacity(容量)。當發(fā)生這種事時,std::vector申請一個新的,更大的內(nèi)存塊來保存它的元素,然后把原來的內(nèi)存塊中的元素,轉(zhuǎn)移到新塊中去。在C++98中,轉(zhuǎn)移是通過拷貝來完成的,它先把舊內(nèi)存塊中的所有元素拷貝到新內(nèi)存塊中,再銷毀舊內(nèi)存塊中的對象(譯注:再delete舊內(nèi)存)。這種方法確保push_back能提供強異常安全的保證:如果一個異常在拷貝元素的時候被拋出,std::vector的狀態(tài)沒有改變,因為在所有的元素都成功地被拷貝到新內(nèi)存塊前,舊內(nèi)存塊中的元素都不會被銷毀。

在C++11中,會進行一個很自然的優(yōu)化:用move來替換std::vector元素的拷貝。不幸的是,這樣做會違反push_back的強異常安全保證。如果n個元素已經(jīng)從舊內(nèi)存塊中move出去了,在move第n+1個元素時,有一個異常拋出,push_back操作不能執(zhí)行完。但是原來的std::vector已經(jīng)被修改了:n個元素已經(jīng)被move出去了。想要恢復(fù)到原來的狀態(tài)是不太可能的,因為嘗試”把新內(nèi)存塊中的元素move回舊內(nèi)存塊中“的操作也可能產(chǎn)生異常。

這是一個嚴重的問題,因為一些歷史遺留代碼的行為可能依賴于push_back的強異常安全的保證。因此,除非知道它不會拋出異常,否則C++11中的push_back的實現(xiàn)不能默默地用move操作替換拷貝操作。在這種情況(不會拋出異常)下,用move替換拷貝操作是安全的,并且唯一的效果就是能提升代碼的性能。

std::vector::push_back采取”如果可以就move,不能就copy“的策略,并且在標準庫中,不只是這個函數(shù)這么做。在C++98中,其他提供強異常安全的函數(shù)(比如,std::vector::reserve,std::deque::insert等等)也采取這樣的策略。如果知道m(xù)ove操作不會產(chǎn)生異常,所有這些函數(shù)都在C++11中使用move操作來替換原先C++98中的拷貝操作。但是一個函數(shù)怎么才能知道m(xù)ove操作會不會產(chǎn)生異常呢?回答很明顯:它會檢查這個操作是否被聲明為noexcept。

swap函數(shù)特別需要noexcept,swap是實現(xiàn)很多STL算法的關(guān)鍵部分,并且它也常常被拷貝賦值操作調(diào)用。它的廣泛使用使得noexcept提供的優(yōu)化特別有價值。有趣的是,標準庫的swap是否是noexcept常常取決于用戶自定義的swap是否是noexcept。舉個例子,標準庫中,array和std::pair的swap這么聲明:

template<class T, size_t N> void swap(T (&a)[N],T (&a)[N]) noexcept(noexcept(swap(*a, *b)));template<class T1, class T2> sturct pair{...void swap(pair& p) noexcept(noexcept(swap(first, p.first)) && noexcept(swap(second, p.second)));... };

這些函數(shù)是條件noexcept(conditionally noexcept):它們是否是noexcept取決于noexcept中的表達式是否是noexcept。舉個例子,給出兩個Widget的數(shù)組,只有用數(shù)組中的元素來調(diào)用的swap是noexcept時(也就是用Widget來調(diào)用的swap是noexcept時),用數(shù)組調(diào)用的swap才是noexcept。反過來,這也決定了Widget的二維數(shù)組是否是noexcept。相似地,std::pair<Widget, Widget>對象的swap成員函數(shù)是否是noexcept取決于用Widget調(diào)用的swap是否是noexcept。事實上,只有低層次數(shù)據(jù)結(jié)構(gòu)的swap調(diào)用是noexcept,才能使得高層次數(shù)據(jù)結(jié)構(gòu)的swap調(diào)用是noexcept。這鼓勵你盡量提供noexcept swap函數(shù)。

現(xiàn)在我希望你已經(jīng)對noexcept提供的優(yōu)化機會感到興奮了。哎,可是我必須澆滅你的熱情。優(yōu)化很重要,但是正確性更重要。我記得在這個Item的開始說過,noexcept是函數(shù)接口的一部分,所以只有當你愿意長期致力于noexcept的實現(xiàn)時,你才應(yīng)該聲明函數(shù)為noexcept。如果你聲明一個函數(shù)為noexcept,并且之后對于這個決定后悔了,你的選擇是將是絕望的。1:你能把noexcept從函數(shù)聲明中移除(也就是改變函數(shù)接口),則客戶代碼會遭受運行期風(fēng)險。2:你也能改變函數(shù)的實現(xiàn),讓異常能夠逃離函數(shù),但是保持著原來的異常規(guī)范(現(xiàn)在,原來的規(guī)范聲明是錯誤的)。如果你這么做,當一個異常嘗試逃離函數(shù)時,你的程序?qū)K止。3:或者你可以拋棄一開始想要改變實現(xiàn)的想法,回歸到你現(xiàn)存的實現(xiàn)中去。這些選擇沒有一個是好的選擇。

事實上,很多函數(shù)都是異常中立的(exception-neutral)。這些函數(shù)自己不拋出異常,但是他們調(diào)用的函數(shù)可能拋出異常。當發(fā)生這樣的事時,異常中立的函數(shù)允許異常通過調(diào)用鏈傳給處理程序。異常中立的函數(shù)永遠不是noexcept,因為他們可能拋出“我只經(jīng)過一下”(異常產(chǎn)生的地方在別的函數(shù)中,但是需要經(jīng)過我們來傳遞出去)的異常。因此,很大部分函數(shù)都不適合設(shè)計為noexcept。

然而,一些函數(shù)天生就不拋出異常,并且對于一些函數(shù)(特別是move操作和swap函數(shù))成為noexcept能有很大的回報,只要有任何可能,它們都值得實現(xiàn)為noexcept。當你能很明確地說一個函數(shù)永遠不應(yīng)該拋出異常的時候,你應(yīng)該明確地把這個函數(shù)聲明為noexcept。

請記住,我說過一些函數(shù)天生就適合實現(xiàn)為noexcept。但是如果扭曲一個函數(shù)的實現(xiàn)來允許noexcept聲明,這樣是本末倒置的。假設(shè)一個簡單的函數(shù)實現(xiàn)可能會產(chǎn)生異常(比如,它調(diào)用的函數(shù)可能拋出異常),如果你想隱藏這樣的調(diào)用(比如,在函數(shù)內(nèi)部捕捉所有的異常并且把它們替換成相應(yīng)的狀態(tài)值或者特殊的返回值)不僅將使你的函數(shù)實現(xiàn)更加復(fù)雜,它還將使你的函數(shù)調(diào)用變得更加復(fù)雜。舉個例子,調(diào)用者必須要檢查狀態(tài)值或特殊的返回值。同時增加的運行期的費用(比如,額外的分支,以及更大的函數(shù)在指令緩存上會增加更大的壓力。等等)會超過你希望通過noexcept來實現(xiàn)的加速,同時,你還要承擔(dān)源代碼更加難以理解和維護的負擔(dān)。這真是一個糟糕的軟件工程。

對于一些函數(shù)來說,聲明為noexcept不是如此重要,它們在默認情況下就是noexcept了。在C++98中,允許內(nèi)存釋放函數(shù)(比如operator delete和operator delete[])和析構(gòu)函數(shù)拋出異常是很糟糕的設(shè)計風(fēng)格,在C++11中,這種設(shè)計風(fēng)格已經(jīng)在語言規(guī)則的層次上得到了改進。默認情況下,所有的內(nèi)存釋放函數(shù)和所有的析構(gòu)函數(shù)(包括用戶自定義的和編譯器自動生成的)都隱式成為noexcept。因此我們不需要把它們聲明成noexcept的(這么做不會造成任何問題,但是不尋常。)只有一種情況析構(gòu)函數(shù)不是隱式noexcept,就是當類中的一個成員變量(包括繼承來和被包含在成員變量中的成員變量)的析構(gòu)函數(shù)聲明表示了它可能會拋出異常(比如,聲明這個析構(gòu)函數(shù)為“noexcept(false)”)。這樣的聲明是不尋常的,標準庫中就沒有。如果把一個帶有能拋出異常的析構(gòu)函數(shù)的對象用在標準庫中(比如,這個對象在一個容器中或者這個對象被傳給一個算法),那么程序的行為是未定義的。

我們值得去注意一些庫的接口設(shè)計區(qū)分了寬接口(wide contract)和窄接口(narrow contract)。一個帶寬接口的函數(shù)沒有前提條件。這樣的函數(shù)被調(diào)用時不需要注意程序的狀態(tài),它在傳入的參數(shù)方面沒有限制。帶寬接口的函數(shù)永遠不會展現(xiàn)未定義行為。

不帶寬接口條件的函數(shù)就是窄接口函數(shù)。對這些函數(shù)來說,如果傳入的參數(shù)違反了前提條件,結(jié)果將是未定義的。

如果你在寫一個寬接口的函數(shù),并且你知道你不會拋出一個異常,那就遵循本Item的建議,把它聲明為noexcept。對于那些窄接口的函數(shù),情況將變得很棘手。舉個例子,假設(shè)你正在寫一個函數(shù)f,這個函數(shù)接受一個std::string參數(shù),并且它假設(shè)f的實現(xiàn)永遠不會產(chǎn)生一個異常。這個假設(shè)建議我們把f聲明為noexcept。

現(xiàn)在假設(shè)f有一個前提條件:std::string參數(shù)的數(shù)據(jù)長度不會超過32個字節(jié)。如果用一個超過32字節(jié)的std::string來調(diào)用f,f的行為將是未定義的,因為一個不符合前提條件的參數(shù)會導(dǎo)致未定義行為。f沒有義務(wù)去檢查前提條件,因為函數(shù)假設(shè)它們的前提條件是被滿足的(調(diào)用者有責(zé)任確保這些假設(shè)是有效的)。由于前提條件的存在,把f聲明為noexcept看起來是合理的。

void f(const std::string& s) noexcept; //前提條件:s.length() <= 32

但是假設(shè)f的實現(xiàn)選擇檢查前提條件是否被違反了。檢查本不是必須的,但是它也不是被禁止的,并且檢查一下前提條件是有用的(比如,在進行系統(tǒng)測試的時候)。調(diào)試時,捕捉一個拋出的異常總是比嘗試找出未定義行為的原因要簡單很多。但是要怎么報道出前提條件被違反了呢?只有報道了才能讓測試工具或客戶端的錯誤處理機制來捕捉到它。一個直接的方法就是拋出一個“前提條件被違反”的異常,但是如果f被聲明為noexcept,那么這個方法就不可行了,拋出一個異常就會導(dǎo)致程序終止。因此,區(qū)分寬接口和窄接口的庫設(shè)計者通常只為寬接口函數(shù)提供noexcept聲明。

最后還有一點,讓我完善一下我之前的觀點(編譯器常常無法對“找出函數(shù)實現(xiàn)和它們的異常規(guī)范之間的矛盾”提供幫助)。考慮一下下面的代碼,這段代碼是完全合法的:

void setup(); //在別處定義的函數(shù) void cleanup(); void doWork() noexcept {setup(); //做設(shè)置工作... //做實際的工作cleanup(); //做清理工作 }

在這里,盡管doWork調(diào)用了non-noexcept函數(shù)(setup和cleanup),doWork還是被聲明為noexcept。這看起來很矛盾,但是有可能setup和cleanup在說明文檔中說了它們永遠不會拋出異常。就算它們沒有在說明文檔中說明,我們 還是有多理由來解釋他們的聲明式為什么是non-noexcept。舉個例子,它們可能是用C寫的。(也可能是從C標準庫移動到std命名空間但缺少異常規(guī)范的函數(shù),比如,std::strlen沒有聲明為noexcept)或者它們可能是C++98標準庫的一部分,沒有使用C++98的異常規(guī)范,并且到目前為止還沒有被修改成C++11的版本。

因為這里有很多合適的理由來解釋為什么noexcept函數(shù)可以調(diào)用缺乏noexcept保證的函數(shù),所以C++允許這樣的代碼,并且編譯器通常不會對此發(fā)出警告。

            你要記住的事
  • noexcept是函數(shù)接口的一部分,并且調(diào)用者可能會依賴這個接口。
  • 比起non-noexcept函數(shù),noexcept函數(shù)可以更好地被優(yōu)化。
  • noexcept對于move操作,swap,內(nèi)存釋放函數(shù)和析構(gòu)函數(shù)是特別有價值的,
  • 大部分函數(shù)是異常中立的而不是noexcept。

轉(zhuǎn)載于:https://www.cnblogs.com/boydfd/p/5035940.html

總結(jié)

以上是生活随笔為你收集整理的Item 14: 如果函数不会抛出异常就把它们声明为noexcept的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。