# C++ 實作 Reference Count Pointer [toc] ## 前言 主要就是 C++ 學了一陣子後,我發現我 C++ 的能力其實有點卡在一個瓶頸,大概就是那個學習曲線到了一個極限後要再進步就開始會有難度以及挑戰,我個人覺得自己不熟的地方大概就是多執行緒還有 Template 了。我想這應該也是很多人學 C++ 也比較容易會遇到的問題吧,反正就當一個學習歷程的紀錄。希望能勉勵自己努力,如果讀者發現有誤,也請不吝嗇歡迎糾正我 XD ## 動機 主要就是我其實很喜歡專研圖學渲染引擎的程式架構,要如何設計才是比較易維護以及解藕之類的,於是就開始專研 Nvdia 的 [dount](https://github.com/NVIDIAGameWorks/donut) 渲染引擎,而在研究的過程中,我發現一個很有趣的東西,那就是 `RefCountPtr`。 你可能覺得那個有什麼特別,阿不就只是一個 Reference-Counting Pointer (參照計數指標)嗎,對啊他就是XD,但問題是我很好奇,為什麼他們要自己實作一個參照計數指標呢?C++ std 函式庫不就有 `std::share_ptr<T>` 了嗎?而且去看他們的程式碼,註解上也說明了 NV 的 `RefCountPtr` 基本上就是~~抄~~微軟的 `ComPtr<T>`。到底為什麼? 好啦除了我本身想了解一下 Template 是怎麼運作的之外,我也很好奇他這樣的目的性主要是有什麼好處。先說一下我觀察到的,最主要在 NV 的程式碼中,他們任何一個物件不管是 `IBuffer`、`ITexture`、`IDevice`、`IInputLayout`、`IShader` 和 `ISampler` 等,這些 class(interface),全部都是有繼承 `IResource` class。 而 `IResource` class 很簡單,程式碼如下: ```cpp= class IResource { public: virtual unsigned long AddRef() = 0; virtual unsigned long Release() = 0; virtual Object getNativeObject(ObjectType objectType) { (void)objectType; return nullptr; } virtual ~IResource() { } }; ``` 這裡重點是 `IResource` interface 定義兩個 virtual function `AddRef()` 和 `Release()`,而且是強迫繼承的類別如果要建立實例必定要實作這些函數。 而我剛剛講的 `IBuffer` 和`ITexture` 這些主要就是定義 Interface,讓繼承這些類別的 class 可以符合規範,畢竟因為 NV 的 dount 引擎所支援的圖學渲染 API 有 DX11、DX12 和 VK。為了能在不同的圖學渲染 API 下都能執行,它們建立了一種抽象層稱之為 RHI(Rendering Hardware Interface),基本上現在各大廠商例如 Epic Game 的 Unreal 以及 Nvidia 自家引擎 dount 都有屬於自己的 RHI。 這也就是說你今天想要建立一個 Texture 的時候,你不用去煩惱說現在是使用哪一個圖學引擎、要怎麼建立等等,你只需要呼叫 interface 定義好的那些函數。而 NV 這邊的做法就蠻特別的,它們有一個檔案叫 `nvrhi.h`,裡面定義了各式各樣我剛剛說的 `IBuffer` 和`ITexture` 等,並且 `typedef` 這些介面然後建立別名,例如: ```cpp= typedef RefCountPtr<ITexture> TextureHandle; typedef RefCountPtr<IInputLayout> InputLayoutHandle; typedef RefCountPtr<IBuffer> BufferHandle; ``` 所以說外部程式碼存取 Texture 或是 Buffer 一定都是這種 Handle 的型別,而真正實作 `Texture` 或 `Buffer` 這些類別則是定義在各式對應圖學 API 的專案,例如 `nvrhi_d3d11`、`nvrhi_d3d12` 或 `nvrhi_vk`。 但還有一個東西,我舉例 D3D11 好了,比如說它的 `Texture`,如果你直接繼承 `ITexture` 的話,我上面提到的 `IResource` 的那兩個 `AddRef()` 和 `Release()` 就需要特別實作。這不是什麼太大的問題,但如果你每一個物件都要自己實作這些 function 可能重覆太多了。我自己是想到說一般來說應該是會把這實作寫在 `IResource` 上,但 NV 選擇的是搞一個 Template Class,叫 `RefCounter<T>`。 這邊我能理解的是一個高超的操作,它希望的是能把 `AddRef()` 和 `Release()` 函數獨立出來而不是相依於 `IResource`,如果是我自己來做的話,一般都會想說建立一個 class,這樣變成你 `Texture` 可能就要繼承兩個類別。但 NV 把 `RefCounter<T>` 寫成 Template 的形式,而那個 `T` 其實就是用來繼承,看一下程式碼: ```cpp= #include <atomic> template<class T> class RefCounter : public T { private: std::atomic<unsigned long> m_refCount = 1; public: virtual unsigned long AddRef() override { return ++m_refCount; } virtual unsigned long Release() override { unsigned long result = --m_refCount; if (result == 0) delete this; return result; } }; ``` > 注意那個 `m_refCount` 主要是為了支援多執行緒所以採用原子資料型態宣告,如果沒有打算玩多執行緒的話可以用 `unsigned long`。 所以在 `d3d11.h` 那邊的定義都是長這樣: ```cpp= class Texture : public RefCounter<ITexture> class Buffer : public RefCounter<IBuffer> ``` 再探討 `share_ptr` 跟 `RefCountPtr` 的差異之前,我們要先了解一下基本知識和微軟的 COM 物件。 ## 轉型、指標、繼承 我們先來看一下範例程式碼: ```cpp= struct Base { Base() = default; ~Base() = default; }; struct Derived : public Base { Derived() = default; ~Derived() = default; }; int main(int argc, char** argv) { Derived* ddd = new Derived(); Base* bbb = nullptr; bbb = ddd; delete ddd; return 0; } ``` 在 C++ 中,衍生類別可以隱式(Implicit)轉換成基底類別,而這種子類別往父類別的轉型叫 upcasting;反之如果基底類別要轉換成衍生類別,則稱之為 downcasting,程式碼為這樣: ```cpp= Derived* ddd = nullptr; Base* bbb = new Base(); ddd = bbb; // error: invalid conversion from 'Base*' to 'Derived*' ``` 不過這時候你的程式就編譯不過了,因為 C++ 不允許你使用隱式轉換成衍生類別,你必須要有 cast expression 才行,你可能想到兩種方法: ```cpp= // c style cast ddd = (Derived *)bbb; // static cast ddd = static_cast<Derived *>(bbb); ``` 這兩種編譯可以通過,程式也可以跑,但不建議這樣做,因為 `static_cast` 編譯器不會幫你檢查會不會有問題。我們可以嘗試修改一下 Derived 的結構,讓它多一個變數成員: ```cpp= struct Derived : public Base { Derived() = default; ~Derived() = default; unsigned int age = 5; }; // main() ddd = static_cast<Derived*>(bbb); std::cout << ddd->age << std::endl; ``` 此時雖然程式可以執行,但是危險的,因為 `age` 輸出會是亂數,這樣會導致程式出現不穩定的錯誤(可能會發生違規存取等問題)。解決的方法就是使用 `dynamic_cast` 來轉型,它會在執行階段做類型檢查,如果不安全的情況下就會回傳 `nullptr`。 不過根據上述的範例程式碼,如果你直接用的話編譯就會出錯: ```cpp= ddd = dynamic_cast<Derived*>(bbb); // error: cannot 'dynamic_cast' 'bbb' (of type 'struct Base*') to type 'struct Derived*' (source type is not polymorphic) ``` 原因是因為 `dynamic_cast` 它是在執行階段做轉換,它必須要有運行階段能查到的類型資訊,而這個資訊正式儲存在虛擬函數表中(vtable),也就是說你必須要有定義虛擬函數,那個類別(類型)才有對應的虛擬函數表產生,`dynamic_cast` 才能正常運作。 > `dynamic_cast` 只能針對 pointer or reference to class 來使用,也就是 upcasting 或 downcasting。 ## COM ```mermaid classDiagram ID3D11Device2 --|> ID3D11Device3 ID3D11Device1 --|> ID3D11Device2 ID3D11Device --|> ID3D11Device1 IUnknown --|> ID3D11Device IDXGIObject --|> IDXGIDevice IUnknown --|> IDXGIObject IUnknown --|> ID3D11DeviceChild ID3D11DeviceChild --|> ID3D11Resource ID3D11DeviceChild --|> ID3D11DeviceContext ID3D11Resource --|> ID3D11Texture2D ID3D11Resource --|> ID3D11Buffer IUnknown : AddRef() IUnknown : Release() IUnknown : QueryInterface() ``` ComPtr 有一個關鍵是它是專門搭配 `IUnknown`,然後 COM 物件是支援跨繼承樹的轉型,要使用 `QueryInterface()`。 ```cpp= ComPtr<ID3D11Device> dev; // D3D11CreateDevice()... ComPtr<ID3D11Device3> dev3; dev3 = dev; // error // downcasting hr = dev.As(&dev3); // 跨繼承樹 dynamic cast ComPtr<IDXGIDevice> dptr; dev.As(&dptr); // 其實 .As() 底層就是: dev->QueryInterface(__uuidof(ID3D11Device3), (void**)&dev3); ``` `QueryInterface` 可以讓呼叫此物件的程式可以確認此物件是否支援特定的介面,若是支援,則參考到此物件在特定介面下的實現。這個方法類似 C++ 的 `dynamic_cast<T>` 或是 Java 或是 C# 的 `casts`。 此方法在給定一個對應特定介面的全域唯一識別碼(一般也稱為介面識別碼或是IID)時,可以提供一個指定特定介面的指標。若 COM 物件不支援此介面,會回覆 `E_NOINTERFACE` 錯誤。 ## 實作 ## 參考資料 ### IUnknown https://zh.wikipedia.org/zh-tw/IUnknown https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iunknown ### COM https://learn.microsoft.com/en-us/windows/win32/com/component-object-model--com--portal ### Count Ref Pointer https://en.wikipedia.org/wiki/Reference_counting ### ComPtr vs share_ptr https://github.com/Microsoft/DirectXShaderCompiler/issues/1275 https://stackoverflow.com/questions/26770933/c-what-are-the-differences-between-microsofts-comptr-and-c-unique-ptr-shar https://stackoverflow.com/questions/9243520/shared-ptr-vs-ccomptr https://social.msdn.microsoft.com/Forums/SqlServer/en-US/5a376d34-f74b-4594-b87f-d070a3e00199/directx-comptr-vs-c-sharedptr?forum=windowsgeneraldevelopmentissues ### C++ Smart Pointer https://stackoverflow.com/questions/332030/when-should-static-cast-dynamic-cast-const-cast-and-reinterpret-cast-be-used https://stackoverflow.com/questions/38553744/tracking-down-owner-of-a-shared-ptr https://blog.xuite.net/tsai.oktomy/program/20019616# e8859487.pixnet.net/blog/post/402001658-%5Bc%2B%2B%5D-static_cast-用法說明---%28基礎篇%29 ### C++ Virtual medium.com/theskyisblue/c-中關於-virtual-的兩三事-1b4e2a2dc373 ### C++ Upcast and Downcast https://stackoverflow.com/questions/8223365/upcast-and-downcast-in-c ### RTTI zh.wikipedia.org/zh-tw/執行期型態訊息 https://stackoverflow.com/questions/4227328/faq-why-does-dynamic-cast-only-work-if-a-class-has-at-least-1-virtual-method ### RAII https://zh.wikipedia.org/zh-tw/RAII ### Is-a https://zh.wikipedia.org/zh-tw/Is-a ### lvalue and rvalue https://stackoverflow.com/questions/3601602/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues http://amitmason.blogspot.com/2018/09/c_30.html https://stackoverflow.com/questions/52104649/c11-rvalue-reference-vs-const-reference https://openhome.cc/Gossip/CppGossip/RvalueReference.html ### C++ definition and declaration https://stackoverflow.com/questions/1410563/what-is-the-difference-between-a-definition-and-a-declaration ###### tags: `C++`
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up