C++ 在有用 event loop 的情況下,該用 unique_ptr 或 shared_ptr?

起因是看到 Chromium 的 Smart Pointer Guidelines

Ref-counted objects — use scoped_refptr<>, but better yet, rethink your design. Reference-counted objects make it difficult to understand ownership and destruction order, especially when multiple threads are involved. There is almost always another way to design your object hierarchy to avoid refcounting. Avoiding refcounting in multithreaded situations is usually easier if you restrict each class to operating on just one thread, and use PostTask() and the like to proxy calls to the correct thread.base::Bind(), WeakPtr<>, and other tools make it possible to automatically cancel calls to such an object when it dies.

當時覺得相當有道理,用 ref-counted object (以下用 shared_ptr 表示) 是不負責的行為,應該要明確定義物件之間的所有權。於是實踐了一陣子,踩了一堆雷,最後領悟了一些細節。

這裡先定義一些名詞方便後面討論。

  • 實踐 event loop 的 class 稱為 MessageLoop。
  • MessageLoop 提供 method PostTask(Task) 可以在之後執行 Task。
  • Task 包含 object、object 執行的 method address、method 需要的參數。執行 Task 就是呼叫 object 的 method。

unique_ptr 和 MessageLoop 的 life cycle 問題

假設 object 的擁有者透過 unique_ptr 管理它。擁有者死掉的時候,會透過 unique_ptr 自動刪掉 object。如果之後 MessageLoop 執行 Task 的時候存取到 object,程式就炸了。

要避免這個問題,得在刪除 object 前確保 MessageLoop 內已沒有 object 或要求 MessageLoop 不要存取 object。這裡提供其中一種解法:確定 MessageLoop 裡不可能有 object 的時候,再來刪除 object。

假設只會在一個 MessageLoop 使用 object,需要作的事如下:

  1. 建立 unique_ptr 時帶一個 deleter,deleter 會呼叫 object 的 DeleteThis(),這函式不會當下直接 delete object。
  2. 平時呼叫 PostTask() 放入 object 前,要先檢查 IsDeleted()。如果為真就不能放進去。
  3. DeleteThis() 和相關程式碼如下:

藉由 PostTask() 延後刪除的作法,可以確保刪除 object 的時候,MessageLoop 不會有任何 raw pointer 指向 object。

但是,如果這個 object 會跨多個 MessageLoop (等於跨多個 thread) 執行,該怎麼辦?雖然可以用同樣的概念 PostTask(),然後等有用到的 MessageLoop 都不可能有 object 時再呼叫 delete this,但是這樣作有點複雜,用 shared_ptr 簡單許多。如果想確保 object 最後會在正確的 thread 被刪除,可以在建立 shared_ptr 時帶入 deleter,由 deleter 來作。

反思

回頭來看, 單一 MessageLoop 的作法雖然「漂亮」,但是用 shared_ptr 相對省事。在有用 MessageLoop 的情況下,只要有人沒處理好 unique_ptr,就會存取到 dangle pointer,增加團隊日後除錯的時間。

這個準則後面提到:

Note that too much of our existing code uses refcounting, so just because you see existing code doing it does not mean it’s the right solution. (Bonus points if you’re able to clean up such cases.)

如果多數人沒法遵守這個準則,多少意味著準則太過複雜而不易被留意或被執行。想想使用 Java、Python、JavaScript、Objective C 等語言時,不需留意這些細節。C++ 提供了非常多彈性,如果全面使用 shared_ptr,透過其它方式降低使用它的缺點 (difficult to understand ownership and destruction order) [*1]。藉此少擔心一些事,反而會比較划算。

備註

  1. 關於 ownership,可以用註解說明。destruction order 要 case-by-case 討論。不過是跨 multi-thread 時問題才會變複雜。不管用 unique_ptr 還是 shared_ptr,程式都要處理好 ownership 和 destruction order。差別是沒處理好的時候,通常用 unique_ptr 程式會掛掉;用 shared_ptr 則是 memory leak。