1 問題的起因
1.1 T**
或 T&*
? C++ 的智能指針可以通過 get() 和 * 的重載得到原始指針 T*,遇到這樣的 C 風格的函數的時候:
void Process(Foo *ptr);std::unique_ptr<Foo> sp = ...;Process(sp.get()); //調用 Process 函數
Process() 函數以非搶奪的方式使用 Foo *,大家都相安無事。但是,C++ 的智能指針都是非侵入式的智能指針,如果要修改指針自身,則只能通過顯式的手段,比如 reset() 成員函數。所以 C++ 的智能指針遇到這樣的 C 風格的函數時,就很棘手:
bool MakeObject(Foo **pptr);
bool RefreshObject(Foo& *pptr);
沒有 const 約束,大家都明白這樣的接口是返回一個指針或重置一個指針。遇到這種情況,C++ 只能分步做這個事情:
std::unique_ptr<Foo> sp;
Foo *ptr = nullptr;
if(MakeObject(&ptr)) {sp.reset(ptr);
}
但是這么做就有個問題,在 MakeObject() 函數完成一個 Foo * 的初始化,到外部 sp 托管這個指針之間就有一段間隙,在這期間發生任何異常,Foo 對象和 Foo 對象分配的存儲空間就隨風而去,自由地飛翔。
1.2 傳統解決方案
? Raymond Chen 在資料 [2] 中給出了一種代理類的解決方法,通過代理類在智能指針和對象指針之間建立一個橋梁。就上一節的 MakeObject() 函數的使用,借助于 Raymond Chen 的思路,我們可以這樣設計一個代理類:
template<typename T>
struct UniquePtrProxy {UniquePtrProxy(std::unique_ptr<T>& output): m_output(output) { }~UniquePtrProxy() { m_output.reset(m_rawPtr); }operator T** () { return &m_rawPtr; }UniquePtrProxy(const UniquePtrProxy&) = delete;UniquePtrProxy& operator=(const UniquePtrProxy&) = delete;std::unique_ptr<T>& m_output;T *m_rawPtr = nullptr;
};
UniquePtrProxy 類通過構造函數關聯一個 T 類型的智能指針,通過對 T** 的重載,使得這個類可以適配需要 T** 參數的場合,給出的 T** 可被修改,并且在代理銷毀的時候 reset 關聯的智能指針。
std::unique_ptr<Foo> spFoo;
if (MakeObject(UniquePtrProxy<Foo>(spFoo))) {std::cout << "value = " << spFoo->GetValue() << std::endl;
}
? 貌似天衣無縫,即使 MakeObject 內部出現了異常,只要 Foo * 的指針是有效的,利用 RAII,UniquePtrProxy 都可以正確設置智能指針,從而避免資源泄露。不過,正如 Raymond Chen 在資料 [2] 中給出的例子那樣,用戶如果這樣寫代碼就很郁悶了:
if (MakeObject(UniquePtrProxy<Foo>(spFoo)) && spFoo) {std::cout << "value = " << spFoo->GetValue() << std::endl;
}
這其實是一種很合理的做法,在使用智能指針之前先檢查一下指針的有效性。但是 if 表達式中的整個求值完成之前,UniquePtrProxy 創建的臨時對象在 MakeObject() 函數調用完成后,會像鬼魂一樣繼續飄蕩一段時間,當檢測 spFoo 的時候,它還沒有析構,spFoo 還沒有被 reset,結果就是 if 代碼塊永遠也走不到。
? 資料 [4] 提出一種侵入式智能指針,允許直接修改內部指針,比如資料 [1] 的 retain_ptr 的實現。但是更多的庫采用的是智能指針適配器的方式,通過適配器完成智能指針的侵入式操作。典型的就是 WRL 庫的 ComPtrRef ,它其實是對 ComPtr 的適配器。C++ 的標準庫采用的就是適配器方案。
2 C++ 23 的智能指針適配器
2.1 out_ptr_t 和 inout_ptr_t
? C++ 23 引入了兩個智能指針適配器,即 out_ptr_t 和 inout_ptr_t,分別用于應對上一節的 MakeObject() 類型 C 函數和 RefreshObject() 類型 C 函數(有時候 RefreshObject() 類型的 C 函數也是用 T** 類型參數)。對于 MakeObject() 類型 C 函數,我們可以這樣使用 std::out_ptr_t 適配器:
int main() {std::unique_ptr<FooTest> spFoo;if (MakeObject(std::out_ptr_t<std::unique_ptr<FooTest>, FooTest*>(spFoo))) {std::cout << "value = " << spFoo->GetValue() << std::endl;}
}
2.2 out_ptr 和 inout_ptr
? 正如上一節的例子,直接使用適配器需要顯式指定模板參數,非常麻煩,所以 標準庫還提供了兩個全局函數,std::out_ptr() 和 std::inout_ptr(),這兩個函數的作用就是根據函數參數推導參數類型,然后返回一個相應的適配器對象,可以簡化這兩個適配器的使用,比如上一節的例子,可以改成這樣:
int main() {std::unique_ptr<FooTest> spFoo;if (MakeObject(std::out_ptr(spFoo))) {std::cout << "value = " << spFoo->GetValue() << std::endl;}
}
2.3 注意事項
? C++ 標準并不建議直接使用 std::out_ptr_t 或 std::inout_ptr_t 構建一個有聲明周期的臨時對象,因為這樣很容易導致問題,比如 2.1 節的例子,如果代碼寫成這樣:
int main() {std::unique_ptr<FooTest> spFoo;auto&& rrr = std::out_ptr_t<std::unique_ptr<FooTest>, FooTest *>(spFoo);if (MakeObject(rrr)) {std::cout << "value = " << spFoo->GetValue() << std::endl;}
}
編譯器不抱怨,但是運行異常,因為 rrr 延長了 std::out_ptr_t 臨時對象的生命周期,使得它的析構在使用 spFoo 指針之后,導致 spFoo 在它消失之前一直是無效的狀態。
? 另外需要注意的是 std::inout_ptr_t 做的事情是釋放智能指針原來的所有權,然后重新初始化這個智能指針。這樣的操作需要獨占所有權的智能指針,所以它不能用于 std::shared_ptr。還有就是 1.2 節所提到的代理或適配器的生命周期問題,std::out_ptr_t 或 std::inout_ptr_t 也存在,所以這樣的代碼依然是有問題的:
int main() {std::unique_ptr<FooTest> spFoo;if (MakeObject(std::out_ptr(spFoo)) && spFoo) {std::cout << "value = " << spFoo->GetValue() << std::endl;}
}
這也是使用智能指針適配器需要注意的地方。實際上,資料 [2] 中作者提出了幾種解決方案,但是都不是很優雅的方案,大家感興趣的話可以看一下這篇文章。
3 總結
? 智能指針適配器的引入,起到了三個作用:
-
安全封裝原生指針的傳遞
為許多 C 函數提供安全適配器,避免手動管理指針和資源的所有權
-
與智能指針無縫協作
允許 C++ 的智能指針直接用于需要 T** 和 T&* 參數的函數交互,解決智能指針需要手動釋放和重置的問題,解決了此類 C 風格的 API 安全返回資源的問題
-
統一資源管理接口
將現有的 C 風格的資源獲取和釋放行為整合到 C++ 的 RAII 模型中,逐步替換為 RAII 風格,減少資源泄露的風險
總之,這些適配器工具是 C++ 進一步強化與 C 互操作性和資源管理的重要改進,通過對智能指針的自動適配,降低此類開發場景的資源泄漏風險,通過對智能指針的隱式管理,也使得代碼更簡潔。
參考資料
[1] retain_ptr: https://github.com/slurps-mad-rips/retain-ptr
[2] Raymond Chen: Spotting problems with destructors for C++ temporaries
[3] ComPtrRef Class: From Microsoft Windows Runtime Library
[4] P0468: A Proposal to Add an Intrusive Smart Pointer to the C++ Standard Library, 2018
[5] P1132: out_ptr - a scalable output pointer abstraction
關注作者的算法專欄
https://blog.csdn.net/orbit/category_10400723.html
關注作者的出版物《算法的樂趣(第二版)》
https://www.ituring.com.cn/book/3180