為什麼 intrusive_ptr 沒有進入標準庫?

我感覺intrusive_ptr比起通常的shared_ptr還是有很多優點的。

1:引用計數放在對象內部,執行效率好像是幾種引用計數裡面最高的。我猜可能是因為查找計數變數的額外操作少。而且計數和將要操作的對象在一個地方,存儲局部性強,利於CPU緩存。

2:便於寫其它語言的binding。當對象被腳本語言的變數hold/unhold的時候,簡單地加減計數就可以。

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

我的實際需求是:對象不但被自己系統所持有,還要提供給Perl介面,那麼它就很可能被Perl的系統所持有。管理這種持有關係,最簡單的方法就是:當它被一個Perl SV持有的時候,引用計數加一;當那個Perl SV被銷毀的時候,引用計數減一。如此不需要使用任何附加的機制,但必須能直接地控制引用計數。

所以我做了一個共同的基類,所有」複雜「的對象都繼承這個基類:

class RefCounted
{
public:

RefCounted();
virtual ~RefCounted();

inline void ref()
{
ref_count++;
}

inline void unref()
{
ref_count--;
if (ref_count == 0) delete this;
}

inline size_t get_ref_count()
{
return ref_count;
}

private:
size_t ref_count;
};

inline void intrusive_ptr_add_ref(RefCounted* self)
{
self-&>ref();
}

inline void intrusive_ptr_release(RefCounted* self)
{
self-&>unref();
}

然後,當一個對象要被Perl SV持有時(被typemap調用):

void store_refcounted(RefCountedDerivedClass* obj, SV* sv)
{
RefCounted* obj_base = static_cast&(obj);
obj_base-&>ref();
sv_setref_pv(sv, "Perl::Class::Name", obj);
}

當持有它的Perl SV被銷毀時(寫在XS代碼里):

void
genoeye::RefCounted::DESTROY()
CODE:
THIS-&>unref();


不妨說說看你遇到的需要使用它的場景,最好有代碼示例證明確實優於shared_ptr。

1. make_shared()

2. enable_shared_from_this.


看到 @KE meng的回答,讓我想起了連城的一篇舊文,shared_ptr的四宗罪。且不說兩者煽情的成分大於理性,單就觀點本身來說也是難於接受的。

先看這個例子:

A* raw_p=new A;
shared_ptr& p(raw_p);
shared_ptr& p2(raw_p);

連城也用了類似的例子來證明shared_ptr的第一宗罪

一旦對資源對象染上了shared_ptr,在其生存期內便無法擺脫。

這個例子用來說明shared_ptr不能這樣用還可以,但不能用來證明shared_ptr是個傻逼。這邏輯等同於「因為我要吃蘋果,而梨子不是蘋果,所以梨子是個傻逼」。這不是在耍流氓么?

至於回答中提到的兩次new導致的性能問題,這個真應該回去看看make_shared的文檔,裡面Note段解釋得非常清楚,實在不信,隨手測一遍就清楚了。結果通常是make_shared比intrusive_ptr慢一點(因為多了一層weak_ptr),而比shared_ptr快很多。

for (int i = 0; i &< 10000000; ++i) { make_shared&();
}
for (int i = 0; i &< 10000000; ++i) { shared_ptr&{new Foo};
}
for (int i = 0; i &< 10000000; ++i) { boost::intrusive_ptr&{new CountedFoo};
}

連城說的性能問題還靠譜一些,即一些不必要的shared_ptr copy constructor會導致性能下降,不過這個問題Herb Sutter在CppCon2014上的演示稿已經闡述得很清楚了。

回到題主的問題上來。

為什麼intrusive_ptr沒進標準?因為對RAII不友好,並且在絕大多數情況下,有make_shared和enable_shared_from_this就夠了。

PERL SV這個問題,涉及到C API,這導致無法用RAII來控制。而由於整個STL對非RAII方式都不太友好,所以shared_ptr相對於intrusive_ptr會比較彆扭,非要用shared_ptr的話(比如這些Object在C++代碼里已經大面積通過shared_ptr管理了),需要轉成RAII方式:

unordered_map&&> objs;

template &
void store_refcounted(shared_ptr& const sp, SV* sv)
{
auto o = static_cast&(sp.get());
objs[o] = [sp]{};
sv_setref_pv(sv, "Perl::Class::Name", o);
}

void unref(void* obj)
{
objs.erase(obj);
}


你這疑惑問的太好了,我本來想匿名,想想算了我就有話直說得了:

我老早就有疑惑了,不光是侵入式引用計數的問題,我特別特別特別特別想知道為什麼

STL裡面侵入式的東西如此之少,他們跟侵入式有仇嗎? boost也一樣,

就那麼幾個侵入式容器半死不活的半天也不見升級.

@陳碩 回答得好,"不妨說說看你遇到的場景",我先舉一個例子:

struct A {
int data;
};

int main()
{
A* raw_p=new A;
shared_ptr& p(raw_p);
shared_ptr& p2(raw_p);
return 250;
}

這是一段等著死的代碼,因為p和p2分別維護了兩個引用計數,創建時都加1,銷毀時都減1,結果把A

delete了兩次,當然,如果有人這麼寫,說明對shared_ptr理解有誤,但是如果是侵入式的情況則根本

無需考慮:

struct Counter {
int m_counter=0;
};

template&
struct BetterPtr {
BetterPtr(T* ptr=nullptr)
:m_raw_ptr(ptr)
{
static_assert(std::is_base_of&::value,"T必須繼承Counter");
assert(ptr!=this);
if(m_raw_ptr!=nullptr) ++m_raw_ptr-&>m_counter;
};
~BetterPtr()
{
if(m_raw_ptr!=nullptr) {
if (--m_raw_ptr-&>m_counter==0) {
delete m_raw_ptr;
}
}
};
operator T*() const { return m_raw_ptr; }
private:
T* m_raw_ptr;
};

struct A : public Counter {
int data;
};

int main()
{
A* raw_p=new A;
BetterPtr& p(raw_p);
BetterPtr& p2(raw_p);
BetterPtr& p3(raw_p);
BetterPtr& p4(p2);
BetterPtr& p5(raw_p);
BetterPtr& p6(p4);
return 1;
}

你看,根本就是隨意指,不管是raw指針和BetterPtr之間的複製還是BetterPtr之間都隨意複製,而且開銷比shared_ptr小多了! 首先BetterPtr里只有一個m_raw_ptr,其次省略了shared_ptr對計數器位置的查找,一個對象的計數器只有一個.還有一個最關鍵的! shared_ptr我知道至少在VS的實現下,它內部居然要為第一次創建的引用計數器調用new!!! 簡直他媽搞笑,我new出一個對象,為了這個對象實現引用計數居然還要再new出一個引用計數器! 雙倍的new開銷! 我不管這個計數器的new內部是直接malloc了還是有什麼內存池,你再內存池,效率也比侵入式一句m_counter=0;低吧?

再然後,還可以對上面的代碼BetterPtr添加一些功能,比如把計數器換成atomic&...直接線程安全了,還有可以檢查內存泄露:

struct Counter {
~Counter()
{
assert(m_counter==0,"你把同一個對象delete了多次,老闆準備開除你了");
}
int m_counter=0;
};

template&
struct BetterPtr {
BetterPtr(T* ptr=nullptr)
:m_raw_ptr(ptr)
{
assert(ptr-&>m_counter&>=0,"這個對象應該已經死了可是你還使用著它,明天不要來上班了");
static_assert(std::is_base_of&::value,"XXXXXX");
assert(ptr!=this);
if(m_raw_ptr!=nullptr) ++m_raw_ptr-&>m_counter;
};
~BetterPtr()
{
assert(ptr-&>m_counter&>0,"這個對象應該已經死了可是你還使用著它,老闆準備開除你了");
if(m_raw_ptr!=nullptr) {
if (--m_raw_ptr-&>m_counter==0) {
delete m_raw_ptr;
}
}
};
operator T*() const { return m_raw_ptr; }
private:
T* m_raw_ptr;
};

struct A : public Counter {
int data;
};

再來,我還可以為Counter類中添加更多的功能,比如監視多少個BetterPtr正在指著自己:

struct Counter {
~Counter()
{
assert(m_counter==0,"你把同一個對象delete了多次,老闆準備開除你了");
}
int get_ref_number() const { return m_counter; }
int m_counter=0;
};

通過這個,我們還可以實現一個簡單的不檢測循環引用的GC:

先做一個Counter的全局lockfree鏈表類,然後開這樣一條線程,偽代碼:

class 掃描器線程 {
static void 每隔一段時間掃描一下()
{
for(auto e:全局鏈表) { if(e.m_counter=0) { delete e; } }
}
private:
static Slist& 全局鏈表;

把BetterPtr中析構函數的delete 行為刪掉,只進行引用計數的操作,那麼

把Counter變成一個侵入式鏈表的元素:

struct Counter : public SlistNode&
{
Counter():m_counter(0)
{
掃描器線程::全局鏈表.push(this);
}
virtual ~Counter()
{
assert(m_counter==0,"你把同一個對象delete了多次,老闆準備開除你了");
}
int get_ref_number() const { return m_counter; }
int m_counter=0;
};

這樣就可以讓"掃描器線程"來完成所有delete工作了.以上功能的實現全部依賴於侵入式的引用計數和侵入式的鏈表,實際上如果觀察基於引用計數來GC的語言,比如python,所有類的基類PyObject就是侵入式引用計數.再比如比如AngelScript,wxwidgets裡面的wxObject ,Qt裡面的QSharedData,全是侵入式引用計數.

我承認侵入式的引用計數也有壞處,壞處就是在即使只new一個char也得至少佔sizeof(char)+sizeof(int)的大小,但是這個壞處對於好處來說絕對可以忽略不計,因為非侵入式的引用計數也得在某處佔sizeof(int)這麼大的空間,只不過不在對象內部罷了.

就說到這兒吧,容器什麼的我就不提了....其實說了半天光說了侵入式引用計數的好處,沒提"為什麼沒進入標準",題主問為什麼,我也不知道,可能是他們覺得python,angelscript,juce,FLTK,wxwidgets,Qt都不標準吧.


@藍色回答一下吧

The main reasons to use intrusive_ptr are:

  • Some existing frameworks or OSes provide objects with embedded reference counts;
  • The memory footprint of intrusive_ptr is the same as the corresponding raw pointer;
  • intrusive_ptr& can be constructed from an arbitrary raw pointer of type T *.

As a general rule, if it isn"t obvious whether intrusive_ptr better fits your needs than shared_ptr, try a shared_ptr-based design first.


推薦閱讀:

現代c++內存管理的方式有哪些?
Firefox和Chrome相比是否更適合小內存用戶?
c++字元串拷貝和內存問題?
比較有效的內存清理軟體?高手們都是怎麼手動優化系統和內存的?
Windows10 TH2中引入的內存壓縮技術是什麼原理?

TAG:內存管理 | C | 智能指針 | BoostC庫 |