之前在《避免 memory leak:C++11 Smart Pointer》(上、下)這兩篇文章,已經大概介紹了 C++11 的智慧指標(smart pointer)了。而 C+11 提供的三種智慧指標裡面,可能被使用的機會最大的,應該還是 shared_ptr<>(參考)吧?
而最近在看 Boost 的 Beast 這個新的函式庫(官網)的時候,才注意到原來 C++11 還有提供一個 enable_shared_from_this<> 類別(參考),讓一個物件可以更安全地產生對應的智慧指標。
由於感覺還算滿有用的,所以這邊就稍微紀錄一下吧。
首先,enable_shared_from_this<> 這個 template 類別被定義在 <memory> 這個 header 檔裡,他是被設計來繼承用的,要使用的時候,基本上就是寫成下面的形式:
class CSharedClass : public std::enable_shared_from_this<CSharedClass> { public: std::shared_ptr<CSharedClass> getPointer() { return shared_from_this(); } };
可以看到,這邊就是在撰寫自己的類別的時候,去繼承 enable_shared_from_this<>;而這邊比較特別的,是本身的型別就是 enable_shared_from_this<> 的 template 參數。
而之後,就可以透過 enable_shared_from_this<> 提供的 shared_from_this() 這個函式,來產生自己的 shared_ptr<> 智慧指標了~
(另外 C++17 也還有 weak_from_this() 可以回傳 weak_ptr<>)
像上面的 getPointer() 函式,就是一個例子。不過實際上,因為 shared_from_this() 是公開的,所以可以直接由外部呼叫,不需要這樣特別重新封包就是了。
這樣做有什麼好處呢?在 cppreference 是有給一個例子,這邊 Heresy 稍作簡化:
#include <memory> #include <iostream> struct Bad { std::shared_ptr<Bad> getptr() { return std::shared_ptr<Bad>(this); } ~Bad() { std::cout << "Bad::~Bad() calledn"; } }; int main() { // Bad, each shared_ptr thinks it's the only owner of the object std::shared_ptr<Bad> bp1 = std::make_shared<Bad>(); std::shared_ptr<Bad> bp2 = bp1->getptr(); std::cout << "bp2.use_count() = " << bp2.use_count() << 'n'; } // UB: double-delete of Bad
這邊可以看到,Bad 這個結構並沒有繼承 enable_shared_from_this<>,而他的 getptr() 這個函式,則是把自身的指標(this)、重新用 shared_ptr<> 打包、然後回傳。
這樣的問題,就是當透過 getptr() 來取得 shared_ptr<> 這種智慧指標時,實際上內部的計數器會是重新開始計算的!
以上面的例子來說,bp1 和 bp2 雖然內部是指到同一個空間,但是兩者的的計數器卻是獨立的,所以當成 bp2 的 use_count() 會回傳是 1、而不是 2。
更進一步來的問題,就是在程式結束時,bp1 和 bp2 都會覺得只有自己指到這個實際上的資料,而都會去試著將資料刪除、造成多重 delete 的問題(Bad 的解構仔會被呼叫兩次)。
而如果這邊把 Bad 改寫成:
struct Good : std::enable_shared_from_this<Good> { std::shared_ptr<Good> getptr() { return shared_from_this(); } };
那就不會有上面的問題了。
但是,以上面的例子來看,其實 Heresy 自己也覺得沒什麼說服力…
因為,一般要在使用 shared_ptr<> 的時候,應該不會特別再去建立這種 getptr() 的函式,上面的 bp2 也只要直接改成:
std::shared_ptr<Bad> bp2 = bp1;
就什麼問題都沒有了啊!
在 Heresy 來看,enable_shared_from_this<> 的重點,是可以在類別的內部,存取自己的 shared_ptr<> 智慧指標,然後傳給外部;這類的使用需求,其實常常會出現在 callback function 這類的事件架構。
下面算是一個簡單、用來說明的例子(示意用,不要太講究細節):
#include <string> #include <iostream> #include <functional> class Session { public: std::function<void(Session*,const std::string&)> onMessage; void run() { while (true) { // Message Get std::string sInput; std::cin >> sInput; onMessage( this, sInput); } } void sendMessage(const std::string& sOutput) { std::cout << "SEND: " << sOutput << std::endl; } }; int main() { Session* pSession = new Session(); pSession->onMessage = [](Session* pSession, const std::string& sInput) { pSession->sendMessage(sInput); }; pSession->run(); }
這邊的 Session 可以視為 server-clinet 架構下、對應一個連線的 session,他本身會去處理接收訊息和傳送訊息的工作,通常會是由 server 來產生、管理;其中的 onMessage 就是一個在收到訊息時,會被呼叫到的函式。
而在 main() 裡面,則是直接去建立新的 Session,並將 onMessage 設定承在收到訊息後,就把他送出去、變成一個 echo server 的形式。
在這邊,都是使用 raw pointer 來處理 Session 的傳遞,但是如果要改用 shared_ptr<> 的話,程式可能會變成(黃底是修改的部分):
#include <memory> #include <string> #include <iostream> #include <functional> class Session { public: std::function<void(std::shared_ptr<Session>,const std::string&)> onMessage; void run() { while (true) { // Message Get std::string sInput; std::cin >> sInput; onMessage( this, sInput); } } void sendMessage(const std::string& sOutput) { std::cout << "SEND: " << sOutput << std::endl; } }; int main() { std::shared_ptr<Session> pSession(new Session()); pSession->onMessage = [](std::shared_ptr<Session> pSession, const std::string& sInput) { pSession->sendMessage(sInput); }; pSession->run(); }
但是,這個時候,就會發生在 run() 裡面要呼叫 onMessage 的時候,不知道該怎麼把 this 轉換成 shared_ptr<> 的狀況了!(上面紅底的部分)
如果直接從 this 建立新的 shared_ptr<> 的話,那就可能會因為 shared_ptr<> 內部的計數器已經變成獨立計算的,導致最後會多重釋放的狀況。
所以,解決方法就是使用 enable_shared_from_this<>,來解決這個問題~也就是把 Session 改寫成:
class Session : public std::enable_shared_from_this<Session> { public: std::function<void(std::shared_ptr<Session>,const std::string&)> onMessage; void run() { while (true) { // Message Get std::string sInput; std::cin >> sInput; onMessage( shared_from_this(), sInput); } } void sendMessage(const std::string& sOutput) { std::cout << "SEND: " << sOutput << std::endl; } };
這樣一來,就可以解決上面的問題了!
另外,如果是要把自己的 member function 透過 bind、或是 lambda 來封包成 callable object 的時候,這東西應該也是很實用的~
像是 Heresy 最近在研究的 boost beast 的 WebSocket Server Async 的範例中,其實就大量地使用這樣的概念(參考),下面就是節錄部分內容做為示意:
class listener : public std::enable_shared_from_this<listener> { public: void do_accept() { acceptor_.async_accept( socket_, std::bind( &listener::on_accept, shared_from_this(), std::placeholders::_1)); } void on_accept(boost::system::error_code ec); };
以前在內部使用 bind() 來處理成員函式的時候,大多是直接把 this 這個 raw pointer 傳進去,而如果是打算整個搭配 shared_ptr<> 的話,那改用 shared_from_this() 應該會是比較理想的方法。
而或者,個人其實更習慣用 lambda 的寫法:
void do_accept() { auto pThis = shared_from_this(); acceptor_.async_accept( socket_, [pThis](boost::system::error_code ec) { pThis->on_accept(ec); }); }
這篇關於 enable_shared_from_this<> 的紀錄大概就這樣了。
最後,也要提醒一下,如果有這樣寫,那這個類別基本上就一定只能用 shared_ptr<> 的形式來使用了~如果這樣定義後,卻沒有用 shared_ptr<> 的形式來使用的話,是會出問題的。