shared_ptr 的輔助類別 enable_shared_from_this


之前在《避免 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() called\n"; }
};
 
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<> 這種智慧指標時,實際上內部的計數器會是重新開始計算的!

以上面的例子來說,bp1bp2 雖然內部是指到同一個空間,但是兩者的的計數器卻是獨立的,所以當成 bp2use_count() 會回傳是 1、而不是 2。

更進一步來的問題,就是在程式結束時,bp1bp2 都會覺得只有自己指到這個實際上的資料,而都會去試著將資料刪除、造成多重 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<> 的形式來使用的話,是會出問題的。

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.