如同 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 型別是一種投影型別。
這很重要
了解實 作類型 與 投影型別的區別非常重要。 關於基本概念和術語,務必閱讀 《Consume API with C++/WinRT 》和 《Author APIS with C++/WinRT》。
實作與投影之間的距離可能相當微妙,難以掌握。 事實上,為了讓實作感覺更接近投影,該實作會為其所實作的每一種投影型別提供隱含轉換。 但這並不代表我們可以輕易做到這一點。
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.
原始指標是危險且耗時的錯誤來源。 如果不需要就不要用。 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的風格。