C++的未來和指標

周昌鴻發表於2013-06-19

上週Meeting C++2013結束後,我對C++思考了很多,有一些內容和指標有關。在C++ 11中只對指標進行了小量的更新(引入了nullptr),不過過去幾年中,C++中指標的語義和用法卻發生了很多變化。

首先,我們從指標的原始意義開始,C++11中簡單如type* pt = nullptr; 這裡的指標是C語言中的核心概念,指標並不是C++發明的,據我所知也不是C發明的。但是C規範中定義了指標,並給出了在C和C++中使用指標的指導。事實上,指標是一個變數,它儲存的值是記憶體中的一個地址。如果你對指標進行解引用操作,就能訪問指標指向的變數。指標實際上是一個基礎變數,它不知道它所指向的值是否有效,也不能感知其指向的值是否無效。在C語言中,一個指標指向0,說明其不指向任何值,因此也不具有效的值。所有其他指標都應該指向記憶體中有意義的地址,但實際上,有些指標沒有正確的初始化,或者乾脆越出了應有的範圍。

在C++11中,將指標正確初始化為0的方法是使用關鍵字nullptr。這讓計算機知道該指標當前為空。另外,還有一種常用的方式是將0定義為NULL或者其他定義或宣告。C++11中使用nullptr統一了這種方式。C++中還引入了引用,它看起來像是變數的別名,其優勢是使用引用的時候必須先初始化,因此,在引用生命週期起始時需要指向一個有效地址。不過,引用也只是指標的解引用,所以,一旦其引用的變數作用範圍結束,其引用也無效了,使用指標時,你可以將指標置為0,但是針對引用卻不能這麼做。

但是在C++11和在C++11標準之前,一些事情發生了變化,指標是語言的核心概念,但是你在現代化的C++程式碼和函式庫中卻很少看到它們。遠在C++11之前,boost建立了一系列非常有用的智慧指標類,針對指標進行了封裝,對其核心機制通過操作符過載。智慧指標本身不是一個指標,而是一個棧上的變數或物件成員。智慧指標使用了RAII來解決指標的一些問題,這並不是指標的職責。當在椎中分配記憶體時,new返回了指向該部分記憶體的地址,所以每分配一塊動態記憶體,就需要使用一個指標,相當於建立物件的一個操作控制程式碼。但是指標僅僅是一個簡單的變數,不知道變數的擁有關係,也不能自動釋放堆上的記憶體空間。智慧指標擔當了這一角色,擁有指標並在變數超出作用域時自動管理其堆上的值。在棧上的值意味著,一旦相應的棧被銷燬,其管理的堆上的值會被自動釋放,即使是在發生異常的情況下。

過去的一些年,C++出現了一些不同風格的使用,從使用類的C及大量使用指標,到類似我想Widget和QT這樣物件導向的框架。在過去5-10年中的形成的一種新樣式被認為是現代C++,一種趨向盡力發掘語言本身擴充套件能力,並試圖找到不同特性針對不同場合的應用。值得注意的是boost在這一趨勢中起到了引領風範的C++框架。C++標準在設計其標準庫時也借鑑了這一點。與此同時,值語義變得流行起來,並且與move語義成為未來C++一個關鍵點。來自Tony van Eerds在Meeting C++的一份備忘幻燈片引起了我對指標的思考。它有兩列,一個代表引用語義,一個代表值語義,以及其朗朗上口的主題詞:

哦,不!使用指標 vs 哦,不要使用指標!

所以,在C++11或者後續的C++14,使用值語義的趨勢蓋過了使用指標。指標在取後臺還是工作著,不過在新的C++14中,new和delete都將不提倡直接使用,new被抽象化為make_shared/make_unique。其內部使用了new,但是返回一個智慧指標。shared_ptr 和 unique_ptr都表現為值語義型別。智慧指標同樣在其作用域結束時使用delete釋放記憶體。這讓我思考,C++中的指標是不是都可以填充不同的“角色”,或者被替換掉。

繼承和虛擬函式

指標一個非常重要的用途是在繼承中使用指標來指向一系列擁有相同介面的型別值。我想用Shape例子來闡明這一點,這裡有一個基類Shape,同時其含有一個虛擬函式叫area的方法。同時,它還有幾個派生類叫Rectange,Cirecle和Triangle。現在,有一個指標容器(比如:std::vector<Shape*>)來容納指向不同形狀的物件指標,每個物件都有自己的計算面積方法。這是C++中最常用指標的方式,尤其是在物件導向時。現在,好訊息是,這裡同樣支援使用智慧指標,當其使用這些智慧指標時,內部會進行訪問指標。Boost中甚至還有一個指標容器,能在清空容器時自動釋放其中的智慧指標元素。

現在考慮虛擬函式呼叫(這雖然不和指標有直接聯絡),虛擬函式呼叫通常會有點點慢,同時也不容易編譯器針對其進行優化。所以,如果其型別在執行時是可知的,就可以使用靜態分發或者編譯器多型性來正確呼叫相應的虛擬函式方法,而不是在執行時使用虛擬函式指標。作為一種模式被叫做CRTP,已經實現了這一方式。最近的研究顯示,這在gcc4.8中可以提高效能。有趣的是,通常情況下使用gcc4.9,優化器可以針對動態分發進行更進一步的優化。還是讓我們繼續回到指標。

不確定指標

有時候指標被用於有一系列可選值作為引數或者返回不確定的函式中,通常都預設為0,使用者可以選擇傳遞一個有效的指標給該函式。或者在返回的情況下,函式返回一個空指標表示執行失敗。對於錯誤情景,現代C++中常使用異常,但是在有些嵌入式平臺上不能工作,因此,(返回0)在C++的一些場合中也是一個有效的使用方式。同樣的,這裡也可以使用智慧指標,智慧指標可以扮演指標的操作控制程式碼。不過常常會導致堆上記憶體開銷(使用堆),或者並沒有替代不確定的角色。這需要使用一個可選值型別來代替,用於確定其儲存的值是否有效。Boost庫有一個boost::optional來表示可選值型別。因此,可以考慮在C++14中引入有一個類似的可選型別。所以,現在std::optional會被移入到技術預覽版(TS)中,將來會變成C++14或者C++1y的一部分。

當前的標準庫中已經使用了一些可選型別,比如std::set::insert會返回一個pair<iterator,bool>型別,其第二個參數列示請求值是否插入到set容器中。容器通常返回尾迭代器來表示無效,但是如果要求返還一個值時,這個角色過去通常都是用指標來表示,指標為0表示函式執行失敗,因此這裡的指標可以被可選型別替代:

因此,可選型別和智慧指標型別替代了指標的一部分語義,填充了其角色。但是它們是值語義,並大部分都在棧上使用。

有效的指標

在寫作我對C++指標用法的思考時,我主要關注於那些指標可以被其他(比如:智慧指標和可選型別等)替換的場景,但是低估了實際上有些場景指標仍然有用。感謝來自reddit,email和社交媒體的一些反饋。

非擁有者指標就是這樣一個例子,這裡未來的幾年還是需要使用指標。shard_ptr有對應的weak_ptr,但是unique_ptr沒有對應的夥伴。這裡就需要使用非擁有者原始指標。比如,在一個由父和子物件構成的樹或者圖中。但是,未來C++中會新增exempt_ptr來代替。

在處理函式中的傳遞的值時,指標還是具有用處的,Herb Sutter寫了一篇非常好的文章:《GotW about this in May》。Eric Niebler 在他的Meeting C++會議的筆記中也談及了,同時移動語義會影響你應該如何在函式中傳遞或者返回值。

Category

C++11

Input Arguments

small/POD/sink

pass by value

all others

pass by const ref

Output

return by value

Input/Output

non const ref / stateful Algorithm Object

這個表格來自 Eric Nieblers 的筆記, 請看幻燈片中的16/31 (建議你閱讀所有的幻燈片)

Eric Niebler說過,在能使用移動語義時儘可能使用移動語義。一個可選引數為例,vector::emplace_back接收一個引數,當其只是將把元素移動到適當位置,這時你應得使用移動語義。一些輸出引數返回一個值,編譯器可以使用移動語義或者CopyEllision(拷貝去除)的優化技術。針對一些以物件為輸入/輸出引數,非常引用也是可選擇性優化的,但是Eric在他的筆記中指出:物件演算法的狀態在建構函式中應使用槽引數。

在傳遞常量(非常量)引用時,指標可以做同樣的事情,不過有些不同,你需要對指標測試其是否為空。我個人更喜歡在函式/方法或者建構函式時傳遞引用而不是指標。

指標計算

之前我提到過,從我個人的觀點,指標只是一個普通的變數,其值指向一個地址,或者更精確地說,是其指向值得一個地址號碼。這個地址號碼可以被複制,你可以對其進行加或減法操作。這常常用於遍歷陣列或者計算兩個指標的的距離,這在使用陣列時很有用。這裡對陣列的便利其實就是迭代器,所以,在實際程式碼時,指標可以代替迭代器使用。但是,從我多年C++開發經驗來看,我幾乎沒有用到針對指標的計算操作。而且在C++中,指標的計算已經有了非常好的抽象。我的觀點是,理解指標計算是重要的,這有助於理解程式碼中指標的具體作用。

再見,指標?

理論上,C++可以不使用指標,但是由於指標是C/C++語言的核心概念,指標本身仍然會繼續存在。但是它的角色會變更,在你使用C++時,你不再需要考慮指標。隨著C++的繼續發展,C++11和C++14朝著更抽象,對開發者更友好的方向發展。使用智慧指標和可選型別,指標要麼被封裝從而更適用安全的值型別,要麼完全被它們替代掉。

相關文章