淺談Visual C++ 2015引入更新的C++ 特性

Visual C++ 2015 是 C++ 團隊付出巨大努力將現代C++引入windows平臺的成果。在最新的幾個發行版本里,VC++已經逐步添加了現代C++語言以及庫的特色,這些結合在一起會創造一個用於構建通用windows App和組件的絕對驚豔的開發環境。Visual C++2015建立在早期版本引入的驚人進步,提供了成熟的、支持大多數C++11特性以及C++ 2015子集的編譯器。你或許會懷疑編譯器支持的完整程度,公正地說,我認爲他能支持大部分重要的語言特性,支持現代C++將會迎來windows 程序庫開發一片新的天地。這纔是關鍵。只要編譯器支持一個高效優雅的庫的開發環境,開發者就能構建偉大的app和組件。

這裏我不會讓你看一個枯燥的新特性列表,或者走馬觀花地看下它的功能,而是會帶你瀏覽下一些傳統情況下的複雜代碼現在如何讓人相當愉快書寫。當然,這得益於成熟的Visual C++編譯器。我將會向你展示windows的一些本質,在現在或將來API中實際上都是很重要的本質。

頗具諷刺意味的是,對於COM來說,C++已經足夠現代了.是的,我在談論組件對象模型(COM),多年以來,它一直是大多數Windows API的基石.同時,它也繼續作爲Windows運行時的基石.COM無可爭辯的依附於C++的原始設計,借鑑了許多來自C++的二進制和語義約定,但是它從來都不夠優雅.C++的部分內容被認爲可移植性不夠,如dynamic_cast,必須避免使用它,以採用可移植的解決方案,這使得C++的開發實現更具挑戰性.近些年已經爲C++開發者提供了許多解決方案,讓COM變得更加可移植.C++/CX 語言拓展,可能是Visual C++團隊到目前爲止最具野心的.具有諷刺意味的是,這些提升標準C++支持的努力,已經將C++/CX棄之不顧了,也讓語言拓展變得冗餘.

爲了證明這點,我會展示給你如何完整的用現代C++實現IUnknown和IInspectable接口.關於這兩個接口沒有什麼現代的或吸引力的東西.IUnknown繼續成爲卓越API,如DirectX,的集中抽象.這些接口--IInspectable繼承自IUnknown--位於Windows運行時的中心.我將展示給你如何不用任何語言拓展來實現它們,接口表或其它宏--只需要包含大量類型信息的高效和優雅的C++,就可以讓編譯器和開發者擁有,關於如何創建所需的,優異的人機對話.

主要的問題是, 如何列出  COM 或 Windows Runtime 類需要實現的接口,網站製作而且要方便開發者使用, 和編譯器訪問. 比如, 列出所有可用類型, 以便編譯器查詢, 甚至枚舉出相應的接口. 要是能實現這樣的功能, 也許就能讓編譯器生成 IUnknown QueryInterface 甚至 IInspectable GetIids 方法的代碼. 這兩個方法纔是問題的關鍵. 按照傳統的觀念, 唯一的解決辦法涉及到語言擴展(language extensions), 萬惡的宏定義, 以及一堆難以維護的代碼.

兩種方法的實現, 都用到類需要實現的接口. 可變參數模板( variadic template)是首選:

    template <typename ... Interfaces>  
    class __declspec(novtable) Implements : public Interfaces ...  
    {  
    };

__declspec(novtable)拓展屬性可以防止構造函數和析構函數初始化抽象類的vfptr,這通常意味着減少大量的代碼.實現類模板包括一個模板參數包,這使它成爲一個可變模板.一個參數包即一個模板參數接受任意數目的模板參數變量.但是在這種情況下,我描述的模板參數將只會在編譯時進行查詢.接口將不會出現在函數的參數列表之中.

這些參數的一個使用已經顯而易見.參數包拓展後成爲公共基礎類的參數列表.當然,我仍然有責任到最後實現這些虛函數,但是此刻我會描述一個實現任意數目接口的一個具體類:

    class Hen : public Implements<IHen, IHen2>  
    {  
    };

因爲參數包拓展爲指定基礎類的列表,所有它等同於下面我可能會寫出的代碼:

    class Hen : public IHen, public IHen2  
    {  
    };

用這種方式結構化實現類模板的美妙之處在於,我現在可以,在實現類模板中,寫入各種樣版實現代碼,而Hen類的開發者則可以使用這種不唐突的抽象,同時大量忽略隱含的細節.

到目前爲止,一切都很好.現在,我將考慮IUnknown的實現.我應該可以在實現類模板中完整的實現它,並提供編譯器現在所擁有的類型信息.IUnknown提供了對於COM類非常重要的兩種工具,就像氧氣和水對於人類一樣.第一個可能簡單些的是引用計數,這也是COM對象跟蹤它們生命週期的方式.COM規定一種侵入式的引用計數,它藉助於每個對象,統計多少個外部引用存在,來負責管理自己的生命週期.這與智能指針,如C++ 11的shared_ptr類,的引用計數恰恰相反,智能指針對象並不知道它的共享關係.你可能會爭論這兩種方式的優缺點.但是,實際上COM的方法通常更高效,這也是COM的工作方式,你必須處理它.如果沒有其它的,你很可能會同意這點,在shared_ptr裏面包裝一個COM接口會是一件極不友好的事情!

我將以只有運行時的開銷作爲開始,它是通過實現類模板介紹的:

    protected:  
      unsigned long m_references = 1;  
      Implements() noexcept = default;  
      virtual ~Implements() noexcept  
      {}

默認構造函數並不是真正的開銷所在,它只是簡單的確保最終的構造函數--它將初始化引用計數--爲protected而不是public的.引用計數和虛構造函數都是protected的.讓派生類訪問引用計數,是爲了允許更復雜的類組合.大多數類可以簡單的忽略它,但是需要注意的是,我正初始化引用計數爲1.這和通常建議初始化引用計數爲0,形成鮮明的對比,因爲此時並沒有處理引用.這個方式在ATL中非常流行,明顯受到Don Box的COM本質論的影響,但是這是非常有問題的,ATL的源代碼的研究可以作爲佐證.開始於這個假設,即引用的所有權將會立即由調用者獲得,或者依附於一個提供更少錯誤構造處理的智能指針.

虛析構函數提供了很大的便利性,它允許實現類模板實現引用計數,而不是強制實現類本身來提供實現.另一個選項,是使用奇特的遞歸模板模式(Curiously Recurring Template Pattern)來避免使用虛函數.通常我會選擇這個方法,但是它會稍微增加抽象的複雜性,同時,因爲COM類本身有一個vtable,所以這裏也沒有什麼理由去避免使用虛函數.有了這些基本類型之後,在實現類模板中實現AddRef和Release將會變得非常簡單.首先,AddRef方法可以簡單的使用InterlockedIncrement來增加引用計數:

    virtual unsigned long __stdcall AddRef() noexcept override  
    {  
      return InterlockedIncrement(&m_references);  
    }

這不言自明.不要想出某些複雜的方法,通過使用C++的加減操作符來有條件的替換InterlockedIncrement和InterlockedDecrement函數.ATL通過極大的增加複雜性去做這個嘗試.如果你考慮效率,寧可爲避免調用AddRef和Release產生謬誤而多花心思.同樣的,現代C++增加了對move語義的支持,以及增加轉移引用所有權的能力.現在,Release方法只是略顯複雜:

    virtual unsigned long __stdcall Release() noexcept override  
    {  
      unsigned long const remaining = InterlockedDecrement(&m_references);  
      if (0 == remaining)  
      {  
        delete this;  
      }  
      return remaining;  
    }

引用計數減少後,結果被賦值給臨時變量.這很重要,因爲結果需要返回.但是如果對象銷燬了,引用此對象的成員變量就是非法的了.假定沒有其它未處理的引用,這個對象就通過前面說到的虛析構函數刪除了.這就是引用計數的結論,實現類Hen仍然和之前的一樣簡單:

    class Hen : public Implements<IHen, IHen2>  
    {  
    };

現在,到了想象一下QueryInterface的奇妙世界的時間了。實現IUnknown方法是一個很重要的實踐。在我的Pluralsight課程中,我廣泛的實現了它。你可以在Don Box編寫的<<COM本質論>>(Addison-Wesley Professional,1998)一書中,閱讀關於實現你自己的IUnknown的奇妙的和不可思議的方法。需要注意的是,雖然這是一本關於COM的優秀書籍,但是它是基於C++98的,並沒有呈現出任何現代C++的特徵。爲了節省時間,我假定你已經熟悉了QueryInterface的實現過程,並集中於如何用現代C++實現它。下面是虛函數本身:

    virtual HRESULT __stdcall QueryInterface(  
      GUID const & id, void ** object) noexcept override  
    {  
    }

給定一個GUID用來標識一個特別的接口之後,QueryInterface應該來決定一個對象是否實現了需要的接口。如果實現了,它必須減少這個對象的引用計數,同時通過外部參數來返回所需的接口指針。如果沒有實現,它必須返回一個空指針。因此,我將以一個粗略的輪廓來作爲開始:

    *object = // Find interface somehow  
    if (nullptr == *object)  
    {  
      return E_NOINTERFACE;  
    }  
    static_cast<::IUnknown *>(*object)->AddRef();  
    return S_OK;

QueryInterface首先會嘗試設法查找所需的接口。如果接口受不支持,則返回E_NOINTERFACE錯誤碼。請注意,我是如何按照要求處理接口指針不支持的情況。你應該把QueryInterface接口看作是二元的操作。它要麼成功找到所需的接口,要麼查找失敗。不要嘗試發揮創造性,只需要依據條件響應即可。儘管COM規範有一些限制項,但是大多數消費者都會簡單的假定接口不受支持,而不管你會返回何種錯誤碼。在你的實現中的任何錯誤,都毫無疑問的會導致你陷入調試的深淵。QueryInterface是非常基礎的,不能胡亂對待。最後,AddRef由接口指針再次調用,用來支持某種極少的而又允許的類組合場景。這些不受實現類模板的顯式支持,但是我情願在這裏做一個表率。重要的是,記住引用計數操作是面向接口的,而不是面向對象的。你不能 簡單的,在屬於一個對象的任意接口上面,調用AddRef或者Release。你必須依賴COM規則來管理對象,否則你會冒險引入以不可思議的方式崩潰的非法代碼。

但是我如何得知,請求的GUID是否就代表着類想要實現的接口呢?我需要回到實現類模板收集的類型信息的地方,其中類型信息通過它的模板參數包來收集。請記住,我的目標是准許編譯器爲我實現它。我希望最終代碼,和我手寫的一樣高效,甚至更好。我會通過可變函數模板集合來進行查詢,函數模板自身包括模板參數包。我將以BaseQueryInterface函數模板作爲開始:

    virtual HRESULT __stdcall QueryInterface(  
      GUID const & id, void ** object) noexcept override  
    {  
      *object = BaseQueryInterface<Interfaces ...>(id);

BaseQueryInterface本質上是IUnknown QueryInterface的現代C++版本。它直接返回接口指針而不是HRESULT類型。空指針則表明失敗的情況。它接受單一函數參數,GUID標識着要查找的接口。更重要的是,我拓展了類模板參數包爲完整模式,這樣,BaseQueryInterface函數就可以開始枚舉接口的處理過程。 起初你可能會認爲,由於BaseQueryInterface是實現類模板的成員函數,所以它可以簡單直接的訪問接口的鏈表,但是我需要准許這個函數剝離鏈表中的第一個接口,建設網站就像下面這樣:

    template <typename First, typename ... Rest>  
    void * BaseQueryInterface(GUID const & id) noexcept  
    {  
    }

按照這種方式,BaseQueryInterface函數能識別第一個接口,並且給接下來的搜索留有餘地。看吧,COM有一定數量的特殊規則來支持QueryInterface 實現或至少接受對象識別。尤其是請求IUnknown,必須總是返回確切相同的指針,客戶端才能確定兩個接口的指針是否來自同一個對象。因此,BaseQueryInterface函數最棒的地方就是實現了這些假設。所以,可以從首個代表類想要實現的第一個接口的模板參數的GUID請求的對比開始。如果不匹配,我會檢查IUnknown是否開始請求了:
 

    if (id == __uuidof(First) || id == __uuidof(::IUnknown))  
    {  
      return static_cast<First *>(this);  
    }

假設有一個匹配的,我直接準確無誤的返回了第一個接口的指針。static_cast 能確保編譯器基於IUnknown的多種接口不會引起歧義。cast只是校準了指針,讓類的vtable能找到正確的指針位置,因爲所有vtable接口是以IUnknown的三個方法開始的,這非常符合邏輯。

而我不妨同樣添加IInspectable查詢的可選支持。IInspectable相當變態,在某種意義上,它是Windows運行時接口,因爲每個Windows運行時預計編程語言(如 C# 和 JavaScript)必須直接來自IInspectable,而不僅僅只是IUnknown接口。相對於C++的工作方式和COM傳統的定義方式,以適應公共語言運行庫的方式實現對象和接口是不幸的事實。更不幸的是當對象組合的時候對性能的影響,我會在下文中討論。至於QueryInterface,我只需確保IInspectable能被查詢,它應該是一個Windows運行時類的實現,而不是一個簡單的典型COM類。雖然關於IUnknown的明確的COM規則不適用於IInspectable,我可以簡單的用相同的方式對待後者。但這兩個挑戰。首先,需要了解是否有任何IInspectable派生出來的接口實現。第二,需要了解接口的類型,這樣就可以正確的返回一個沒有歧義的調整過的接口指針。假定列表中的第一個接口都是基於IInspectable,那可以只更新BaseQueryInterface 如下:

    if (id == __uuidof(First) ||  
      id == __uuidof(::IUnknown) ||  
      (std::is_base_of<::IInspectable, First>::value &&  
      id == __uuidof(::IInspectable)))  
    {  
      return static_cast<First *>(this);  
    }

注意,我用的是C++ 11中的is_base_of 的特性,來確定第一個模板參數是一個IInspectable的衍生接口。萬一實現典型的COM類不支持Windows運行時,就能確保隨後的對照是由編譯器排除的。這樣我可以無縫地支持Windows運行時和經典的COM類,即沒有增加組件開發人員的語句複雜性,也沒有任何不必要的運行時開銷。但是,如果恰好遇列舉出來得首位不是IInspectable接口,就會有不容易察覺的Bug的隱患。所需要做的就是,用某種方法替代is_base_of來掃描整個接口的列表:

    template <typename First, typename ... Rest>  
    constexpr bool IsInspectable() noexcept  
    {  
      return std::is_base_of<::IInspectable, First>::value ||  
        IsInspectable<Rest ...>();  
    }

IsInspectable 也是基於is_base_of特性的,但是當前適用於匹配接口。如果沒找到基於IInspectable 的接口則終止:

    template <int = 0>  
    constexpr bool IsInspectable() noexcept  
    {  
      return false;  
    }

我會禁用掉稀奇古怪的默認參數。假定 IsInspectable 返回的是 true,我需要找到第一個IInspectable-based 接口:

    template <int = 0>  
    void * FindInspectable() noexcept  
    {  
      return nullptr;  
    }  
    template <typename First, typename ... Rest>  
    void * FindInspectable() noexcept  
    {  
      // Find somehow  
    }

再次使用 is_base_of 特性,但這次要返回一個真實匹配的接口指針:

    #pragma warning(push)  
    #pragma warning(disable:4127) // conditional expression is constant  
    if (std::is_base_of<::IInspectable, First>::value)  
    {  
      return static_cast<First *>(this);  
    }  
    #pragma warning(pop)  
    return FindInspectable<Rest ...>();

BaseQueryInterface 這時可以利用IsInspectable 和 FindInspectable 一起來支持查詢 IInspectable:

    if (IsInspectable<Interfaces ...>() &&   
      id == __uuidof(::IInspectable))  
    {  
      return FindInspectable<Interfaces ...>();  
    }

然後指定具體的 Hen 類:

    class Hen : public Implements<IHen, IHen2>  
    {  
    };

實現類的模板,可以確保編譯器能生成更高效的代碼,不管 IHen、Hen2 來自 IInspectable 還是 IIUnknown (或者其他接口)。現在,我可以最後實現 QueryInterface 的遞歸部分,以及任何追加的接口,例如上面例子中的 IHen2。BaseQueryInterface 是靠調用 FindInterface 函數模板結束的:

    template <typename First, typename ... Rest>  
    void * BaseQueryInterface(GUID const & id) noexcept  
    {  
      if (id == __uuidof(First) || id == __uuidof(::IUnknown))  
      {  
        return static_cast<First *>(this);  
      }  
      if (IsInspectable<Interfaces ...>() &&   
        id == __uuidof(::IInspectable))  
      {  
        return FindInspectable<Interfaces ...>();  
      }  
      return FindInterface<Rest ...>(id);  
    }

注意,我調用這個FindInterface函數模板,大致等同於我原來調用的BaseQueryInterface,在這個例子中,我向它傳遞接口的其餘部分。我特意再次擴大參數包,這樣它可以在列表的其餘部分識別第一接口。但會提示一個故障。由於模板參數包不是以函數實參來擴展的,這可能會變得棘手,編程語言寫不出來我想要的。更多的時候,這種“遞歸的”FindInterface可變模板正是你想要的:

    template <typename First, typename ... Rest>  
    void * FindInterface(GUID const & id) noexcept  
    {  
      if (id == __uuidof(First))  
      {  
        return static_cast<First *>(this);  
      }  
      return FindInterface<Rest ...>(id);  
    }

它會從模板參數的其餘部分中分離,如果有匹配就返回調整過的接口指針。另外,它也會調用自己,直到list取完。當我籠統地提及編譯期遞歸時,重要的是要注意這個函數模板,以及其他類似的實現類模板的例子,在技術上遞歸,而不是在編譯期。每個函數模板的實例調用不同的函數模板的實例。例如,FindInterface<IHen, IHen2> 調用 FindInterface<IHen2>, FindInterface<IHen2>調用 FindInterface<>。爲了讓它遞歸, FindInterface<IHen, IHen2>不需要調用FindInterface<IHen, IHen2>。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章