C++智能指針的enable_shared_from_this和shared_from_this機制
前言
之前學習muduo網絡庫的時候,看到作者陳碩用到了enable_shared_from_this和shared_from_this,一直對此概念是一個模糊的認識,隱約記著這個機制是在計數器智能指針傳遞時才會用到的,今天對該機制進行梳理總結一下吧。
如果不熟悉C++帶引用計數的智能指針shared_ptr和weak_ptr,可參考這篇文章:??深入掌握智能指針
這篇文章主要介紹C++11提供的智能指針相關的enable_shared_from_this和shared_from_this機制。
問題代碼
我們先給出兩個智能指針的應用場景代碼,這些代碼都有問題,仔細思考下問題原因。
代碼清單1
#include <iostream>
#include <memory>
using namespace std;
// 智能指針測試類
class A {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int *m_ptr;
};
int main() {
A *p = new A(); // 裸指針指向堆上的對象
shared_ptr<A> ptr1(p); // 用shared_ptr智能指針管理指針p指向的對象
shared_ptr<A> ptr2(p); // 用shared_ptr智能指針管理指針p指向的對象
// 下面兩次打印都是1,因此同一個new A()被析構兩次,邏輯錯誤
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代碼打印結果如下:
A()
1
1
~A()
~A()
free(): double free detected in tcache 2
Aborted (core dumped)
main函數中,雖然用了兩個智能指針shared_ptr,但是它們管理的都是同一個資源,資源的引用計數應該是2,為什么打印出來是1呢?導致出main函數把A對象析構了兩次,不正確!如果你有這樣的疑問,說明對于shared_ptr的底層原理還沒有完全搞清楚。
代碼清單2
#include <iostream>
using namespace std;
// 智能指針測試類
class A {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
// A類提供了一個成員方法,返回指向自身對象的shared_ptr智能指針。
shared_ptr<A> getSharedPtr() {
/*注意:不能直接返回this,在多線程環境下,根本無法獲知this指針指向
的對象的生存狀態,通過shared_ptr和weak_ptr可以解決多線程訪問共享
對象的線程安全問題,參考我的另一篇介紹智能指針的博客*/
return shared_ptr<A>(this);
}
private:
int *m_ptr;
};
int main() {
shared_ptr<A> ptr1(new A());
shared_ptr<A> ptr2 = ptr1->getSharedPtr();
/* 按原先的想法,上面兩個智能指針管理的是同一個A對象資源,但是這里打印都是1
導致出main函數A對象析構兩次,析構邏輯有問題*/
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代碼運行結果打印如下:
A()
1
1
~A()
~A()
free(): double free detected in tcache 2
Aborted (core dumped)
代碼同樣有錯誤,A對象被析構了兩次,而且看似兩個shared_ptr指向了同一個A對象資源,但是資源計數并沒有記錄成2,還是1,不正確。
shared_ptr原理分析
如果你能夠理解上面代碼的問題所在,那么直接跳到下一節看上面錯誤代碼的解決方案;如果不明白問題的所在,通過下面的源碼介紹,仔細理解shared_ptr的實現原理。
源碼上shared_ptr的定義如下:
template<class _Ty>
class shared_ptr
: public _Ptr_base<_Ty>
shared_ptr是從_Ptr_base繼承而來的,作為派生類,shared_ptr本身沒有提供任何成員變量,但是它從基類_Ptr_base繼承來了如下成員變量(只羅列部分源碼):
template<class _Ty>
class _Ptr_base
{ // base class for shared_ptr and weak_ptr
protected:
void _Decref()
{ // decrement reference count
if (_Rep)
{
_Rep->_Decref();
}
}
void _Decwref()
{ // decrement weak reference count
if (_Rep)
{
_Rep->_Decwref();
}
}
private:
// _Ptr_base的兩個成員變量,這里只羅列了_Ptr_base的部分代碼
element_type * _Ptr{nullptr}; // 指向資源的指針
_Ref_count_base * _Rep{nullptr}; // 指向資源引用計數的指針
};
_Ref_count_base記錄資源的類是怎么定義的呢?如下(只羅列部分源碼):
class __declspec(novtable) _Ref_count_base
{ // common code for reference counting
private:
/**
* _Uses記錄了資源的引用計數,也就是引用資源的shared_ptr的個數;
* _Weaks記錄了weak_ptr的個數,相當于資源觀察者的個數,都是定義成基于CAS操作的原子類型,增減引用計數時時線程安全的操作
**/
_Atomic_counter_t _Uses;
_Atomic_counter_t _Weaks;
}
也就是說,當我們定義一個shared_ptr<int> ptr(new int)的智能指針對象時,該智能指針對象本身的內存是8個字節,如下圖所示:
那么把智能指針管理的外部資源以及引用計數資源都畫出來的話,就是如下圖的展示:
當你做這樣的代碼操作時:
shared_ptr<int> ptr1(new int);
shared_ptr<int> ptr2(ptr1);
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
這段代碼沒有任何問題,ptr1和ptr2管理了同一個資源,引用計數打印出來的都是2,出函數作用域依次析構,最終new int資源只釋放一次,邏輯正確!這是因為shared_ptr ptr2(ptr1)調用了shared_ptr的拷貝構造函數(源碼可以自己查看下),只是做了資源的引用計數的改變,沒有額外分配其它資源,如下圖所示:
注意:兩個shared_ptr對象引用的是同一個引用計數對象_Ref_count_base,依次析構的時候,最終資源new int只釋放一次,正確
但是當你做如下代碼操作時:
int *p = new int;
shared_ptr<int> ptr1(p);
shared_ptr<int> ptr2(p);
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
這段代碼就有問題了,因為shared_ptr<int> ptr1(p)和shared_ptr<int> ptr2(p)都調用了shared_ptr的構造函數,在它的構造函數中,都重新開辟了引用計數的資源,導致ptr1和ptr2都記錄了一次new int的引用計數,都是1,析構的時候它倆都去釋放內存資源,導致釋放邏輯錯誤,如下圖所示:

注意:兩個shared_ptr對象都開辟了自己的引用計數對象_Ref_count_base,都記錄new int資源的引用計數為1,析構的時候引用計數減到0,都認為自己該釋放new int資源,錯誤!
上面兩個代碼段,分別是shared_ptr的構造函數和拷貝構造函數做的事情,導致雖然都是指向同一個new int資源,但是對于引用計數對象的管理方式,這兩個函數是不一樣的,構造函數是新分配引用計數對象,拷貝構造函數只做引用計數增減。
相信說到這里,大家知道最開始的兩個代碼清單上的代碼為什么出錯了吧,因為每次調用的都是shared_ptr的構造函數,雖然大家管理的資源都是一樣的,_Ptr都是指向同一個堆內存,但是_Rep卻指向了不同的引用計數對象,并且都記錄引用計數是1,出作用域都去析構,使得同一塊內存被析構多次,導致問題發生!
問題修改
代碼清單1修改
那么清單1的代碼修改很簡單,就是在產生同一資源的多個shared_ptr的時候,通過拷貝構造函數或者賦值operator=函數進行,不要重新構造,避免產生多個引用計數對象,代碼修改如下:
int main() {
A *p = new A(); // 裸指針指向堆上的對象
shared_ptr<A> ptr1(p); // 用shared_ptr智能指針管理指針p指向的對象
shared_ptr<A> ptr2(ptr1); // 用ptr1拷貝構造ptr2
// 下面兩次打印都是2,最終隨著ptr1和ptr2析構,資源只釋放一次,正確!
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代碼清單2修改 enable_shared_from_this和shared_from_this
那么清單2代碼怎么修改呢?注意我們有時候想在類里面提供一些方法,返回當前對象的一個shared_ptr強智能指針,做參數傳遞使用(多線程編程中經常會用到)。
首先肯定不能像上面代碼清單2那樣寫return shared_ptr<A> (this),這會調用shared_ptr智能指針的構造函數,對this指針指向的對象,又建立了一份引用計數對象,加上main函數中的shared_ptr<A> ptr1(new A());已經對這個A對象建立的引用計數對象,又成了兩個引用計數對象,對同一個資源都記錄了引用計數,為1,最終兩次析構對象釋放內存,錯誤!
那如果一個類要提供一個函數接口,返回一個指向當前對象的shared_ptr智能指針怎么辦?方法就是繼承enable_shared_from_this類,然后通過調用從基類繼承來的shared_from_this()方法返回指向同一個資源對象的智能指針shared_ptr。
修改如下:
#include <iostream>
#include <memory>
using namespace std;
// 智能指針測試類,繼承enable_shared_from_this類
class A : public enable_shared_from_this<A> {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
// A類提供了一個成員方法,返回指向自身對象的shared_ptr智能指針
shared_ptr<A> getSharedPtr() {
/*通過調用基類的shared_from_this方法得到一個指向當前對象的智能指針*/
return shared_from_this();
}
private:
int *m_ptr;
};
一個類繼承enable_shared_from_this會怎么樣?看看enable_shared_from_this基類的成員變量有什么,如下:
template<class _Ty>
class enable_shared_from_this
{ // provide member functions that create shared_ptr to this
public:
using _Esft_type = enable_shared_from_this;
_NODISCARD shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}
// 成員變量是一個指向資源的弱智能指針
mutable weak_ptr<_Ty> _Wptr;
};
也就是說,如果一個類繼承了enable_shared_from_this,那么它產生的對象就會從基類enable_shared_from_this繼承一個成員變量_Wptr,當定義第一個智能指針對象的時候shared_ptr<A> ptr1(new A()),調用shared_ptr的普通構造函數,就會初始化A對象的成員變量_Wptr,作為觀察A對象資源的一個弱智能指針觀察者(在shared_ptr的構造函數中實現,有興趣可以自己調試跟蹤源碼實現)。
然后代碼如下調用shared_ptr<A> ptr2 = ptr1->getSharedPtr(),getSharedPtr函數內部調用shared_from_this()函數返回指向該對象的智能指針,這個函數怎么實現的呢,看源碼:
shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}
shared_ptr<_Ty>(_Wptr),說明通過當前A對象的成員變量_Wptr構造一個shared_ptr出來,看看shared_ptr相應的構造函數:
shared_ptr(const weak_ptr<_Ty2>& _Other)
{ // construct shared_ptr object that owns resource *_Other
if (!this->_Construct_from_weak(_Other)) // 從弱智能指針提升一個強智能指針
{
_THROW(bad_weak_ptr{});
}
}
接著看上面調用的_Construct_from_weak方法的實現如下:
template<class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other)
{ // implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
// if通過判斷資源的引用計數是否還在,判定對象的存活狀態,對象存活,提升成功;
// 對象析構,提升失敗!之前的博客內容講過這些知識,可以去參考!
if (_Other._Rep && _Other._Rep->_Incref_nz())
{
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
return (true);
}
return (false);
}
綜上所說,所有過程都沒有再使用shared_ptr的普通構造函數,沒有在產生額外的引用計數對象,不會存在把一個內存資源,進行多次計數的過程;更關鍵的是,通過weak_ptr到shared_ptr的提升,還可以在多線程環境中判斷對象是否存活或者已經析構釋放,在多線程環境中是很安全的,通過this裸指針進行構造shared_ptr,不僅僅資源會多次釋放,而且在多線程環境中也不確定this指向的對象是否還存活。
最終代碼清單2修改如下:
#include <iostream>
#include <memory>
using namespace std;
// 智能指針測試類,繼承enable_shared_from_this類
class A : public enable_shared_from_this<A> {
public:
A() : m_ptr(new int) { cout << "A()" << endl; }
~A() {
cout << "~A()" << endl;
delete m_ptr;
m_ptr = nullptr;
}
// A類提供了一個成員方法,返回指向自身對象的shared_ptr智能指針
shared_ptr<A> getSharedPtr() {
/*通過調用基類的shared_from_this方法得到一個指向當前對象的智能指針*/
return shared_from_this();
}
private:
int *m_ptr;
};
int main() {
shared_ptr<A> ptr1(new A());
shared_ptr<A> ptr2 = ptr1->getSharedPtr();
// 引用計數打印為2
cout << ptr1.use_count() << endl;
cout << ptr2.use_count() << endl;
return 0;
}
代碼打印結果如下:
A()
2
2
~A()
打印完全正確,A對象構造一次,析構一次,引用計數為2。
總結
以上就是對enable_shared_from_this和shared_from_this機制的介紹。這東西主要解決了該問題:當返回某對象時,由于智能指針調用常規的構造函數導致引用計數類的多次構造,從而導致在釋放內存時,多個智能指針對同一塊內存進行多次釋放,出現Core dump。使用該機制則可返回指向某對象的智能指針,這樣就調用的是智能指針的拷貝構造函數而非常規的構造函數,使得引用計數類不會被多次構造,避免出現同一內存多次釋放的情況。

浙公網安備 33010602011771號