智能指針有什麼不足之處?

我能想到的

1 循環引用

2 基於引用計數的一些性能損耗

還有其他的缺點嗎?相對比來說GC比智能指針又有什麼優勢呢?


智能指針的設計理念是沒有問題的,各位說風格不一致問題的,是因為還在用老的理念用新東西。

和普通指針相比,智能指針通過引用計數,實現了資源回收。智能指針是為了減輕程序員直接管理內存。和new混雜使用是習慣問題,不是智能指針的問題。

圍繞引用計數,帶來輕微的性能損失,如多線程下,引用計數的管理。另一個是,類型轉換問題。

大家需要的是,與時俱進,而不是用老眼光看待新東西。


我覺得herb sutter已經把c++指針這個問題講完了,什麼時候raw,什麼時候unique ,什麼時候shared,參考他的演講吧,貌似叫leak freedom by default

當然還有什麼value ptr,和內存管理關係不是很大


1. 出現的太晚了

2. 第一印象被auto_ptr搞臭了

3. 讓某些問題浮出水面而不是暗藏水底,從而招致反感。最典型的問題就是對象的所有權,在裸指針new加delete時代,對象的所有權是暗含在誰new誰delete的邏輯上的,哪怕代碼被寫的所有權關係暗流涌動都隨便,反正最後能delete就行,真有了暗圈也不過是撈住一個入口delete一下罷了。到了智能指針時代所有權問題浮出水面,有人看到亂象了、看到有圈了,瞬間感覺不能忍,「這一定是智能指針的問題!」


我來說個實際點的,Covariant Return Type:

class B {
public:
virtual B* clone() const {
return new B;
}
};
class D : public B {
public:
D* clone() const override {
return new D;
}
};

對於 smart pointer 是不支持的:

class B {
public:
virtual std::shared_ptr& clone() const {
return std::make_shared&();
}
};

class D : public B {
public:
std::shared_ptr& clone() const override {
return std::make_shared&();
}
};


最大的不足之處就是:本來分明應當做成語言特性的東西,為了和C保持兼容性,必須TMD做成一個庫。

C++大概一半左右的蛋疼,都是因為這個原因。


先做個孔乙己,題主問的應該是使用引用計數的shared_ptr這種吧,其實廣義上說只要是把指針用一個指針like的類封裝起來,然後利用一些語法特性(構造,析構等)進行管理都算smart,引用計數算是最直觀的,但也有其他做法的哦

具體到shared_ptr的不足,其實重要的是看你想把它用到什麼程度,比如說有答主認為weak_ptr可以解決(雖然個人認為只是一種緩解)循環引用,當然這會給代碼設計和開發帶來一些不便,但只要你能接受,就可以不認為它是一種「不足」,所以我這裡說的幾點不足也是有程度之分

首先是性能問題,這個有比較大的爭議,個人看法是不太可能成為最大瓶頸,但也不會到了可以忽視的地步,和你具體的代碼相關

然後是循環引用,這個在引用計數範疇是沒法徹底解決的,必須引入其他一些演算法或設計,或程序員自己保證不要出現循環引用,這塊眾所周知了也不細說

在C++中可能存在一個裸傳this導致的問題,比如這個代碼(不要在意可能的細節錯誤,當偽代碼看就好):

shared_ptr& p;
class T {
int member;
void m() {
//仍然只有p引用這個對象,但是實際上this也引用了
p = NULL; //導致main中new的T對象被銷毀
this-&>member; //訪問了非法的地址
}
}
int main() {
p = new T; //給p初始化T的對象,這時候只有p引用這個對象
p-&>m();
}

其實就是當你通過shared_ptr去調用方法的時候,由於C++將this作為一個隱式的方法參數,用裸指針傳入,而原則上這時候應該對其引用計數+1,但是沒加,假設在方法中直接或間接地使得this指向對象被銷毀,那就掛了

解決這個問題需要在調用方法時增加this的引用,簡單點就改成:

shared_ptr&(p)-&>m();

用臨時對象來保管一個引用,C++規定臨時對象在full expression結束才銷毀,所以至少能保證在方法執行完成是安全的

注意這裡用全局變數做例子,其實還有其他一些情況,比如p存在堆上,而m中或m調用的其他函數中可能對其做修改,都有這個風險

但是呢,如果每次調用方法都copy出一個臨時shared_ptr,太麻煩了,再說這個情況可能也非常少見,所以這事挺矛盾的,做了性價比不高,不做的話又總有一個理論上的崩潰可能留在那

下一個問題並不是智能指針本身的問題,但我覺得也有一些關係,假設我們用C++寫個單鏈表:

class Node {
Node *next;
~Node() {
if (next != NULL) {
delete next;
}
}
}

於是我們只需要delete head,就可以利用析構鏈式釋放整個表,但是顯然這是一個遞歸,當鏈表很長的時候,遞歸過深就有問題了

雖然看上去和shared_ptr沒關係,但我個人覺得,用引用計數管理對象有時候很容易出這種事情,比如這個python代碼:

a = None
for i in xrange(N):
a = (a,)
a = None

其實就是上面case的py版本,用tuple做node,構造長度為N的鏈表,然後直接釋放,CPython用的引用計數,所以N足夠大且運行棧不大(比如win下好像只有1M)的時候這裡會崩掉

怎麼說呢,這不是智能指針和引用計數的問題,但是用引用計數的場景下,這個問題可能又容易被人忽視,當然有經驗的程序員會避開這個坑了

派生類的智能指針向基類轉換的問題有答主說了,貌似有的shared_ptr模板庫提供了解決辦法,不過我沒去看怎麼解決的

應該還有一些其他問題,暫時還沒想到或想起來,我最不爽的是寫起來太麻煩了,如果能像java之類的語法寫代碼,然後底層自動用shared_ptr不是很好么,所以我做了這個項目(但shared_ptr的麻煩並不是其主要動機):

maopao-691515082/coc-lang

這個語言通過編譯到C++來執行,其中對象管理用的就是基於引用計數的智能指針,針對上面的一些問題,在編譯器上做了優化解決,比如在函數嵌套調用的時候,如果上層函數棧已經維持了某對象的引用,則這個對象被當做是參數傳入下層被調用函數時,盡量使用裸指針(因為棧是後入先出,所以這個對象不可能被提前釋放),再比如上面說的裸傳this問題,也會在編譯期做識別,只在必要的時候做shared_ptr的copy

====================

補充,想起來還有個重要的地方是,引用計數在多線程環境下不好弄,因為引用計數的更新不是原子操作,需要加鎖,哪怕不考慮鎖的數量問題(每個對象一個互斥鎖是不可取的),頻繁加解鎖性能也接受不了,當然,也有一些輕量級的辦法,比如cas或利用其它辦法實現的無鎖,但在我看來這也算是一種廣義的鎖互斥,畢竟面對的問題是一個互斥問題嘛

也正是因為引用計數的這個缺點,CPython使用了「臭名昭著」的GIL,噴的人很多,但很少有人把它和GC聯繫起來


1.智能指針只能表示所有權,如果遇到某些複雜的數據結構,或者所有權不明確的場景,還是得裸指針來。

2.雖然它本身不是侵入式數據結構,但實際上在代碼設計時,一處用到智能指針,便處處用到智能指針,可算是一種「感染性」。考慮到不同的智能指針無法混用,標準庫恐怕和老代碼的調和程度也很差。


shared_ptr想用成瓶頸是不可能的,C#的new之所以比C++快也是因為人家內存池做的好,而不是因為少了interlocked refcount操作。最大的不足之處就是你得好好區分shared_ptr和unique_ptr還要避免連成環。你看C#就沒有這個問題,想怎麼寫就怎麼寫,程序員什麼都不懂也沒關係。


編譯器無法提示 unique_ptr 被 move 之後又被使用了。這種錯誤使用不會產生任何編譯錯誤或警告。


循環引用可以通過多注意一下或者循環引用檢查或者弱引用來解決。

至於性能,智能指針性能主要耗費在內存分配和釋放上,適當加個內存池可以事半功倍。這個可以參考我寫的Mozart Any,內部資源是引用計數器持有的,加了內存池之後性能提高了至少30%。

RC相對於GC優點在於回收開銷是均攤的,如果不用並發回收的話全面停頓/短停頓GC對於要求實時性的程序來說是災難性的。


1. 語法醜陋

尤其是new對象的異常控制和shared,weak混用都時候, 語法醜陋讓人不願意使用。

而且智能指針並不智能,還是需要人去精心控制,並不能像java,c#完全不需要關心析構。

丑是最大的bug, 為了屁大點的事情,需要考慮一堆問題,比手動new/free還麻煩。

2.破壞c++原則

c++有個原則,不使用會帶來額外開銷的特性。

但是智能指針不管是否多線程情況,都使用原子操作。普通指針可以常駐register不影響inline優化。

而且shared ptr的計數器地址和對象指針地址並不在一起(兩次new),在cache上也是不友好的。

(評論區有人提到make-shared可以保證內存連續)

覺得右值引用才是真正的c++版智能指針


跨語言時十分麻煩,比如lua,還得轉換成裸指針userdata傳過去中轉一下再傳回來,得保證丟失引用後不被釋放,這是很蛋疼的事情。


1 智能指針比裸指針多敲字元,太丑了。

2 所有權系統與數據結構天生不太相合,不過這也說不上缺點。

總結:想要既美觀又強大的智能指針嗎?快來用Rust啊。(屎蛋口音)


我覺得也就是unique好用, 要用到weak或者share, 十有八九你就需要找個好點的內存管理庫了, stl那些拿夠折騰的.


我覺得最大的不足,就是標準出來的太晚

特別是有時候需要非同步的時候,傳遞對象在一個地方new,另一個地方delete,我覺得很不美觀


在棧上new出空間之後 在智能指針真正接管裸指針之前 如果出現異常 會內存泄露吧


C++11新的標準所有的標準,我都推薦在工程中使用,但是智能指針除外。

1. RAII想法是好的,但是並不是所有都能控制的。

2. 智能指針的傳染性很大,要替換的話,就要替換所有的指針,風險很大。

3. 智能指針如果出現相互應用(就是循環)很可怕。

1. 裸指針很好控制(使用歷史久),只要按照規範來,不會出現問題,即使出現問題也比較好修改。


推薦閱讀:

有講C/C++代碼優化和編譯器優化的書或文章嗎?
像c++ primer這樣的計算機專業書籍,大家都是在那裡買的,報價都不便宜啊?
c/c++開發轉嵌入式(軟體/驅動)工程師好轉嗎?
mfc中CString如何轉化為const char*類型?

TAG:C | CC | GC垃圾回收計算機科學 | 智能指針 |