如 使用 C++/WinRT撰寫 API 中所述,當您建立實作類型的物件時,應該使用 winrt::make 系列協助程式來執行此動作。 本主題深入探討 C++/WinRT 2.0 功能,協助您診斷在堆疊上直接分配實作型別物件的錯誤。
這類錯誤可能會變成神秘的當機或損毀,難以偵錯且耗時。 因此,這是一個重要功能,值得瞭解背景。
使用 MyStringable 設定場景
我們來看 IStringable的簡單實現。
struct MyStringable : implements<MyStringable, IStringable>
{
winrt::hstring ToString() const { return L"MyStringable"; }
};
現在假設您需要調用一個函數,從您的實作中,這個函數需要一個 IStringable 作為參數。
void Print(IStringable const& stringable)
{
printf("%ls\n", stringable.ToString().c_str());
}
問題在於,我們的 MyStringable 類型 並不是IStringable。
- 我們的 MyStringable 類型是 IStringable 介面的實作。
- IStringable 類型是預期類型。
這很重要
請務必瞭解 實作類型 與 投影類型之間的差異。 如需了解基本概念和術語,請務必閱讀 用 C++/WinRT 消費 API 和 用 C++/WinRT 開發 API。
實作與投影之間的空間細微而難以理解。 事實上,為了讓實作感覺更像是投影,實作會提供隱含轉換給它實作的每個投影類型。 這並不意味著我們可以簡單地這樣做。
struct MyStringable : implements<MyStringable, IStringable>
{
winrt::hstring ToString() const;
void Call()
{
Print(this);
}
};
相反地,我們需要取得參考,以便轉換運算符可用來作為解析呼叫的候選專案。
void Call()
{
Print(*this);
}
這樣可以。 隱含轉換提供從實作類型到投影類型的轉換(非常有效率),這在許多案例中都很方便。 如果沒有該設施,許多實作類型會證明非常繁瑣。 假設您只使用 winrt::make 函式範本 (或 winrt::make_self)來配置實作,則一切順利。
IStringable stringable{ winrt::make<MyStringable>() };
C++/WinRT 1.0 的潛在陷阱
不過,隱含轉換可能會讓您陷入困境。 請考慮這個無幫助的協助程式函式。
IStringable MakeStringable()
{
return MyStringable(); // Incorrect.
}
或者甚至只是這句看似無害的話。
IStringable stringable{ MyStringable() }; // Also incorrect.
不幸的是,這類程式代碼 確實 編譯C++/WinRT 1.0,因為該隱含轉換。 我們面臨的一個(非常嚴重)問題是,我們可能會傳回一個指向參考計數物件的預測類型,而該物件的底層記憶體位於暫時堆疊上。
以下是以 C++/WinRT 1.0 編譯的其他專案。
MyStringable* stringable{ new MyStringable() }; // Very inadvisable.
原始指標是危險且需要大量人力的 Bug 來源。 如果您不需要的話,請勿使用它們。 C++/WinRT 會盡其所能,讓一切更有效率,而不需要強迫您使用原始指標。 以下是以 C++/WinRT 1.0 編譯的其他專案。
auto stringable{ std::make_shared<MyStringable>(); } // Also very inadvisable.
這是幾個層級的錯誤。 我們針對同一個物件有兩個不同的參考計數。 Windows 運行時(以及其之前的傳統 COM)基於一種與 std::shared_ptr不相容的內部參考計數。 當然,std::shared_ptr 有很多有效的用途;但是在共用 Windows 執行階段 (和傳統 COM)物件時,這是完全沒有必要的。 最後,這也會使用 C++/WinRT 1.0 編譯。
auto stringable{ std::make_unique<MyStringable>() }; // Highly dubious.
這再次相當可疑。 唯一擁有權與 MyStringable的內部參考計數的共享存活期相對立。
C++/WinRT 2.0 的解決方案
使用 C++/WinRT 2.0 時,所有這些嘗試直接配置實作類型都會導致編譯程序錯誤。 這是最好的錯誤類型,而且比神秘的執行期錯誤更好。
每當您需要進行實作時,只要使用 的 winrt::make 或 的 winrt::make_self,即可,如上所示。 現在,如果您忘了這麼做,您將會突然收到一個編譯器錯誤,該錯誤暗示此問題並引用了一個名為 use_make_function_to_create_this_object的抽象函式。 它不是完全 static_assert,但很接近。 不過,這是偵測所有所描述錯誤的最可靠方式。
這確實表示我們需要對實作放置一些次要條件約束。 由於我們依賴缺少覆寫來偵測直接配置的情況,winrt::make 函式範本必須設法以覆寫來滿足抽象虛擬函式。 其方式是從提供覆寫的 final 類別衍生出來進行實作。 有一些關於此程序的觀察事項。
首先,虛擬函式只會出現在偵錯組建中。 這表示偵測不會影響優化組建中 vtable 的大小。
其次,由於 winrt::make 使用的衍生類別是 final,這表示即使您之前選擇不將實作類別標示為 final,優化器仍可能推斷並進行去虛擬化。 所以這是一個改進。 相反地,您的實作 不能 是 final。 同樣地,這不會造成任何後果,因為實例化類型一律會是 final。
第三,沒有任何專案會阻止您將實作中的任何虛擬函式標示為 final。 當然,C++/WinRT 與傳統 COM 和 WRL 之類的實作非常不同,其中有關實作的所有專案通常都是虛擬的。 在 C++/WinRT 中,虛擬分派僅限於應用程式二進位介面 (ABI)(一律 final),而您的實作方法依賴編譯時間或靜態多型。 這可避免不必要的執行期間多型,也表示在您的 C++/WinRT 實作中幾乎沒有虛擬函式的必要性。 這是一件非常好的事情,這樣可以使內嵌更加可預測。
第四,由於 winrt::make 插入衍生類別,因此您的實作不能有私用解構函式。 私有解構函式在傳統的 COM 實作中很受歡迎,這是因為所有東西都是虛擬的,通常會直接處理原始指標,因此很容易不小心呼叫 delete 而不是 Release。 C++/WinRT 特意設計為讓您難以直接處理原始指標。 你必須 真正 走出去,在C++/WinRT 中取得原始指標,您可能會呼叫 delete。 值語意意味著您正在處理值和參照,而很少與指標。
因此,C++/WinRT 挑戰我們先入為主的概念,即撰寫傳統 COM 程式代碼的意義。 這完全合理,因為 WinRT 不是傳統 COM。 傳統 COM 是 Windows 執行階段的組合語言。 它不應該是您每天撰寫的程序代碼。 相反地,C++/WinRT 可讓您撰寫更像是新式C++的程序代碼,而且更不像傳統 COM。