前幾天我們項目剛剛解決了一個pure virtual function call引起的stopship的bug,乘熱打鐵,學習總結一下。
一、理論上case
當一個純虛函數被調用到時,vc++的debug模式下會彈出這麼一個對話框:
然後就是crash了。
在網上找了一下,發現已經有人對此作了詳細的介紹:"Pure Virtual Function Called": An Explanation. 這是一篇相當全面的文章,從純虛函數抽象基類講起,介紹了對象模型中vptr及vtable的概念以及他們的構造析構過程。有了這些基礎,作者然後列出了5中可能出現"pure virtual function call"的情況,其實可以總結為兩種:
在基類的構造函數或析構函數中直接或間接的調用純虛函數
舉個在基類構造函數中間接調用純虛函數的例子:
通過野指針調用到虛函數
還是上面那個例子,但是不在基類構造函數中調用callVirtual:
其實對於第一種情況,如果你在基類構造函數或析構函數中直接調用純虛函數,編譯器應該能捕捉到這個錯誤;間接的調用雖然編譯器無法檢測到,但是由於Scott同學在<Effective C++>中的大力宣傳:Item 9: Never call virtual functions during construction or destruction,這種情況發生的概率應該比較小,況且即使發生了,排起錯來相對比較簡單。
而對於第二種情況,雖然野指針的行為是未定義的,但就我所了解的,我們一般會得到一個"access violation",而不是"pure virutal function call" :
二、現實中的case
我們在現實中遇到的情況會比上面提到的復雜一些:一個子類對象在析構的過程中遇到異常而未完全銷毀,從而遺留下一個"次品"對象,程序繼續使用此次品對象而調用到純虛函數:
在b)處析構Derived對象的時候,在其基類析構函數中a)處拋出了異常,而此時,因為Derived的析構函數已經調用完畢,該對象中的vptr已經指向基類的vtable,從而形成了一個按照正常流程無法構造出來的"次品"對象,當你使用該對象在c)處來調用virtualFunc時,自然導致 "pure virtual function call"的錯誤。
需要注意的是,這裡,你遇到 "pure virtual function call""的時候,可能離真正的出錯點,也就是析構函數中拋出異常的點已經很遠了。所以這種情況相對來講比較難調試。
三、析構函數與異常
好吧,我知道你忍了好久了,你早想喊出來:"你本來就不該在析構函數中拋出異常!",就像Scott同學說的:Item 11: Prevent exceptions from leaving destructors;就像C++ FAQ中說的:Never throw an exception from a destructor.
雖然,也有人站出來說,there is nothing wrong with throwing destructors,但我還是支持你的觀點,我們的確不應該在析構函數中拋出異常,不然,我們不得不面對以下兩個嚴重的問題:
二次異常導致程序退出;
遺留下來的未完全銷毀的對象與未完成的工作導致的後續問題
pure virtual function call就是這種情況。
但是理想與現實總是有差距的,有些事情,你總得面對:
10多年,幾百萬行代碼,無數人維護過的code base,誰都不敢保證是否某個析構函數會直接或間接的拋出異常。
或許我們應該對第1種情況,也就是我們自己的代碼負責,對原有代碼做一次全面的檢查,並保證之後的代碼不會在析構函數中拋出異常。可是即使如此,如果我們在析構函數中調用了第三方的庫函數,而該函數會拋出異常呢?
即使我們調用到的函數( 包括自己的和第三方的)不會顯式的拋出異常,當我們用SEH處理異常時,如果代碼中出現除0操作,access violation等,都還是會被當做異常捕獲的。
那麼如果在每個non-trivial的析構函數中都加上異常處理呢?這樣代碼未免也太ugly了。況且在保證不主動拋出異常的前提下,這樣的代碼只是以防萬一,意義不是很大。
所以,要在析構函數中完全避免異常還是蠻糾結的。Herb Sutter曾就C++語言提出過一個提議:讓析構函數無法拋出異常,從語言級別上去解決,但被Bjarne Stroustrup, Andy Koenig等人否決了。因為這會導致原有程序的行為不一致,況且在極少數的情況下,我們還是希望能拋出異常來的。
這裡我的結論就是:不主動,不拒絕。不主動是指原則上杜絕在析構函數中拋出異常;不拒絕是指不強制在析構函數中去swallow莫須有的異常,而是把可能的問題暴露出來才便於解決。比如上面提到的那個現實中的例子,通過拋出異常,然後調用pure virtual function call把問題暴露出來,我們就可以著手研究為什麼會拋出異常,如何處理不讓其拋出異常~~~