C++之父談關于C++的五個需要被重新認識的觀點(中)

jopen 9年前發布 | 26K 次閱讀 C/C++開發 C/C++

概述:學習和使用過C++的人幾乎都曾經聽說過下面的五個關于C++的描述,并且對這些話篤信不已,那么現在的情況是怎么樣的呢?本文的作者——C++之父Bjarne Stroustrup將會對這些觀點作逐一回擊。本篇為中篇,探討其中的第三個觀點。
</blockquote> </div>

  • C++之父談關于C++的五個需要被重新認識的觀點(上)
  • C++之父談關于C++的五個需要被重新認識的觀點(中)
  • </ul> </div>

    學習和使用過C++的人幾乎都曾經聽說過下面的五個關于C++的描述,并且對這些話篤信不已,那么現在的情況是怎么樣的呢?本文的作者——C++之父Bjarne Stroustrup將會對這些觀點作逐一回擊。

    以下的這五個觀點盛行于C++多年:

    1. “要了解C++,你必須先學習C語言。”
    2. “C++是一門面向對象的語言。”
    3. “對于可靠的軟件,垃圾回收機制必不可少。”
    4. “為了提高效率,你必須編寫底層代碼。”
    5. “C++只對大型復雜的項目有用。”

    如果你還對這些觀點深信不已,那么這篇文章可以給你一些重新認識。這些觀點在特定的時間對于某些人、某些工作來說是正確的。但是對于今天的C++,隨著ISO C++11標準的編譯器和工具的廣泛使用,這些觀點都需要被重新認識。

    上一篇,這一篇里我們將圍繞“對于可靠的軟件,垃圾回收機制必不可少。”的觀點進行探討。

    觀點三:“對于可靠的軟件,垃圾回收機制必不可少。”

    對于回收未使用的內存這份工作,垃圾回收做得不錯但卻不夠完美。它并非靈丹妙藥。內存可以被間接引用并且許多資源并非單純的內存。來看這個例子:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    這里Filter的構造函數會開啟兩個用于數據存儲的文件(file)。完成這項工作以后,Filter從輸入文件執行輸入任務并將產生的輸出結果 保存到輸出文件里。 這些任務包括硬連接到Filter,作為匿名(lambda)函數,提供一個可能具有覆蓋虛函數派生類的函數。在談及資源管理時這些細節并不重要。我們可 以這樣創建Filter:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    從資源管理的角度來看,這里的問題是如何關閉文件以及對與輸入輸出流相關聯的對象資源進行回收重用。

    在許多種依托于垃圾回收的語言和系統里,常見解決方案是放棄使用delete(它很容易在編程過程中被人遺忘,從而導致內存泄漏)和析構函數(被垃 圾回收后的語言中盡量少用析構函數和不用finalizer,因為它們在邏輯上令人捉摸不透并經常破壞性能)。垃圾回收器可以回收所有的內存資源,但是我 們還需要使用手動操作(通過編寫代碼的方式)來關閉文件并釋放任何與數據流相關的非內存資源(比如鎖)。因此雖然內存被自動完全回收了,但是由于其它資源 是手動管理的,內存的錯誤和泄漏仍有可能發生。

    被C++推薦和使用的方法是依靠析構函數來處理資源回收的問題。值得一提的是,這些被構造函數獲取的資源是通過RAII(“資源獲取即初始化”)這 一簡單而通用的技術來處理的。在user()中,用于flt的析構函數隱式調用了用于輸入輸出流(IS及OS)的析構函數。這些析構函數依次關閉文件并釋 放與數據流相關的資源。而delete對*p會做同樣的操作。

    擁有豐富的現代C++開發經驗的程序員會注意到user()非常笨拙且容易產生錯誤,而采用下面的編寫方式會更好:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    現在當user()退出后*p需要被隱式釋放。程序員不能忘記這項操作。與內置的“裸”指針不同的是,智能指針unique_ptr是一個用于確保資源釋放掉后就不再需要運行時間和內存空間等系統開銷的標準庫類。

    然而,我們仍然能夠看到new。這個解決方案有點冗長(Filter類型重復了),并且由于結構被普通指針(使用的new)和智能指針(在這里是 unique_ptr)分拆開而使某些重要的優化丟失。我們可以使用一個C++14的幫助函數make_unique來進行改善,它能夠構造一個指定類型 的對象并返回一個指向它的unique_ptr指針:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    除非出現需要第二個具有指針語義的Filter的情況(不太可能),否則這段代碼將會更好:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    最后的一個版本比原來的更加簡短、清晰和快速。

    Filter的析構函數做了什么呢?它釋放了屬于Filter的資源。也就是說,它關閉了文件(通過調用它們的析構函數)。事實上,這項工作是通過 隱式的方式完成的,所以除了Filter需要的一些東西,我們可以去掉Filter析構函數的顯式聲明并讓編譯器來處理這一切。因此,我只需要這樣編寫:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    這樣比大多數擁有垃圾回收機制的語言(如Java或者C#)的編寫都要簡單,而且也不會因為程序員的健忘而導致內存泄漏。它比其它的替代方案也要快 速的多(無需模擬自由/動態內存的使用且不需要運行垃圾回收器)。值得一提的是,相對于手動操作的方法RAII還降低了資源的滯留時間。

    這是理想的資源管理方法。它處理的不僅是內存,還包括一般(非內存)資源,比如文件句柄、線程句柄以及鎖等。但這樣就夠了么?對于那些需要從一個函數傳遞到另外一個函數的對象又該怎么辦呢?對于那些沒有明顯的單一所有者的對象又該怎么辦呢?

    轉移所有權:move

    讓我們首先來考慮將對象(所包含的信息)從一個作用域轉移到另一個的問題。這個問題的關鍵在于在不使用copy或易錯指針等需要影響系統性能的情況下如何從作用域之外獲得大量關于所需對象的信息。傳統的方法是使用一個指針:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    現在負責刪除對象的是誰?在這個簡單的例子中,很明顯是make_X()的調用者,但在通常情況下這個答案是不明確的。假如make_X()為了將 系統開銷降低最小而保留了對象緩存呢?假如user()將指針傳遞給了一些other_user()呢?這種方法產生混亂的可能性很大并且也容易產生內存 泄漏。

    我可以使用shared_ptr或者unique_ptr來明確所創建對象的所有權。例如:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    但是為什么非要使用一個指針(智能指針或者一般指針)呢?我通常都不希望使用指針,因為指針的使用與常規的對象引用不合拍。例如,一個Matrix加法函數創建了一個包含2個參數的新對象(求和),但如果返回一個指針則會導致代碼變得非常奇怪: 

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    那個*的位置應該是需要的求和結果,而不是一個指向這個結果的指針。在很多時候,我真正想獲取的是一個對象,而不是指向對象的指針。而多數情況下,獲取對象都會很簡單,特別是對于那些小型對象,只需要簡單的copy就可以了,根本不需要考慮使用指針:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    另一方面,一個包含大量數據信息的對象通常會處理大部分那樣的數據。比如istream,string,vector,list和thread。它們只是使用了幾句關于數據的簡單命令就可以確保潛在的大量數據的合理訪問。讓我們再來看看Matrix加法,我們希望的是

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    我們可以很容易用這種實現(創建臨時對象函數):

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    在默認的情況下,程序會把res(臨時對象)的元素copy到r,但隨后res會被銷毀,持有這些元素所占用的內存也會被釋放,我們考慮到了一種無 需copy(C++的設計目標就是盡量少分配內存)的方法:直接“竊取”這些元素。從第一天學習C++的初學者到老手,每一個人都想過要這么做,但這種方 法很難實現且技術還沒有得到廣泛理解。C++11的出現使這種構想成為了現實。它支持“竊取對象信息(steal the representation)”的理念——通過move句柄的形式轉移對象所有權(即轉移對象所包含信息)。來看看下面這個簡單的2維雙重Matrix 函數:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    copy操作可通過引用(&)參數來識別的,同樣的,move操作可通過右值引用(&&) 參數來識別。move操作可以用來“竊取”對象的信息并遺留下一個“空對象”。對于Matrix來說,這就意味著是這樣的:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    它的機制是這樣的:當編譯器看到了return res,它就明白可以把res銷毀了。也就是說,res在返回之后就不會再使用了。因此,編譯器會立刻應用一個move構造函數而不是copy構造函數來轉移返回的值。通過以下的形式:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    在operator+()中的res會成為空對象,然后交由析構函數來善后,而res中的元素現在已經歸r所有。將對象包含的信息從函數 operator+()提取出來放進調用的變量中,我們已經達成了獲取元素(可能是上百萬字節的內存)的結果,并且我們只使用了最小的成本(也就是差不多 四行用于分配的代碼)。

    老道的C++用戶會指出,在某些情況下,好的編譯器能夠完全清除掉return上所copy的信息(在本例中會保存關于move的四行代碼和調用的 析構函數)。然而,這是對實現的依賴,我不希望基礎編程技術的性能還要由每個獨立編譯器的聰明程度來決定。此外,能夠清除掉copy信息的編譯器也能夠很 輕松的把move給抹掉。我們這里的就有一個用于減小把大量信息從一個作用域copy到另外一個的復雜性和所產生花費的簡單、可靠、通用的方法。

    通常情況下,我們甚至不需要定義所有的這些copy和move操作。如果一個類中缺乏所需的成員,我們可以依靠編譯器所生成的默認操作,比如:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    這個版本的Matrix運行起來與上個版本很相似,除了稍微提升了對錯誤的處理和有一個更多一些的陳述(vector通常只有3行代碼)

    對于那些不是句柄的對象呢?假如它們很小,就象一個int或者一個雙double類型complex那樣,則無須擔心。否則,需要使用nique_ptr或shared_ptr這樣的智能指針來處理它們并進行返回操作。注意,不要加入“裸”指針new和delete。

    不幸的是,就象我舉例的Matrix類一樣,某些類并不是ISO C++標準庫的一部分,但是它的其中一部分還是可用的(開源和面向商業的)。例如,在網上搜索“Origin Matrix Sutton”,你可以看見在我的書The C++ Programming Language (Fourth Edition)的第29章在討論如何設計這樣的一個矩陣。

    共享所有權:shared_ptr

    在關于垃圾回收的討論中,經常會看到并不是每一個對象都對應唯一的所有者。這意味著我們必須確保當對象的最后一個引用消失后,該對象是否已經被銷毀 /釋放。在這個模型里,我們必須使用一個機制來確保當最后一個所有者被銷毀后這個對象也會隨之被銷毀。也就是說,我們需要一個共享所有權的形式。例如,我 們有一個同步隊列sync_queue,用于任務之間的通信。提供者(producer)和使用者(consumer)都被賦予了一個指向 sync_queue的指針:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    我假定task1、task2、iqueue和oqueue已經在其它地方被定義了,在這里我使用了detatch()來讓線程的生存周期比創建線 程的作用域更長。你可能會想到多任務管道和sync_queues。然而,在這里我感興趣的只有一個問題:“是誰刪除了startup()中所創建的 sync_queue?”以書面文字來說,這問題這么提會更好:“最后使用sync_queue的是誰?”這是經典的垃圾回收調用案例。垃圾回收的原型就 是計算指針:持續對使用對象計數,當計數歸零則刪除該對象。(當有一個指針指向自己時計數值加1;當刪除一個指向自己的指針時,計數值減1,如果計數值減 為0,說明已經不存在指向該對象的指針了,則可以安全銷毀)。現在許多語言的垃圾回收機制都是以此為藍本發展的而在C++11里shared_ptr就是 使用的這種機制。上面的例子可變成:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    用于task1和task2的析構函數可以銷毀它們的shared_ptrs(在大多數優秀的設計當中都會非常隱蔽的干這項工作),兩者中較晚完成的會同時對sync_queue進行銷毀。

    這個方法簡單且合理高效。它意味著一個運行復雜的系統并一定需要垃圾回收器。重要的是,它不僅可以回收與sync_queue相關的內存資源,還能 夠回收sync_queue中用于管理不同任務的多線程同步性的同步對象(互斥對象、鎖等)。這種方法不僅適用于內存管理,還適合一般的資源管理。“隱 藏”的同步對象準確處理前面例子中文件句柄和數據流緩沖器所處理的工作。

    我們可以嘗試通過在某些封裝任務的作用域中引入一個唯一所有者來替代使用shared_ptr,當這樣做起來并不一定簡單,因此C++11提供了unique_ptr(用于唯一所有權)和shared_ptr(用于共享所有權)。

    類型安全

    前面,我只談論了垃圾回收與資源管理的關系。在類型安全方面,垃圾回收也影響重大。只要我們有一個明確的delete操作,它就有可能被誤用。例如:

    C++之父談關于C++的五個需要被重新認識的觀點(中)

    不要這樣做,在一般的用戶代碼上使用“裸指針”delete是危險且多余的。讓delete遠離字符串、輸出流、線程、unique_ptr和shared_ptr這樣的資源管理類。在這些地方,delete需要與new謹慎配用來以確保無害。

    摘要:資源管理理念

    對于資源管理,我認為垃圾回收應該作為最后的選擇,而不是作為“解決方案”或者理念:

    • 使用遞歸和隱式的占用抽象來處理自己的資源,對于這種作用域變量的對象來說是更好的選擇。
    • 當你需要指針/引用語義時,使用如unique_ptr或者shared_ptr這樣的智能指針來表示所有權。
    • 如果所有都失敗了(比如,因為你的代碼是一段包含缺乏內存管理和錯誤處理的語言特性支持的混亂指針的程序),請嘗試“手動”處理非內存資源并嵌入一個保守的垃圾回收器來處理幾乎不可能避免的內存泄漏。

    這樣的策略很完美么?不,但是至少它是簡單適用的。基于傳統垃圾回收的策略并不完美,它并不能直接解決非內存資源的問題。

    前一篇我們探討了“要了解C++,你必須先學習C語言。”和“C++是一門面向對象的語言。”的觀點,在下一篇我們將探討最后兩個觀點“為了提高效率,你必須編寫底層代碼。”和“C++只對大型復雜的項目有用。”

    本文翻譯自Five Popular Myths about C++, Part 2,作者為:C++之父Bjarne Stroustrup

    本文譯者為慧都控件網——回憶和感動,轉載請注明:本文轉載自慧都控件網

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!