C++編譯期函數/變量檢測技術,仿真VC關鍵字__if_exists

轉帖請註明出處 http://www.cppblog.com/cexer/archive/2008/07/06/55484.html   VC當中有一個鮮爲人知的關鍵字,除了微軟自己的代碼,我從未在任何地方看到有人用過它。雖然它的功能很強大,不過除非設計上的問題或是一些無法排除的困難,否則幾乎從不會需要用到它的功能。但是有時候,它確實能作爲一個最簡單的解決方案而讓某些設計過程事半功倍。

  借用 CCTV10《走近科學》的語氣:那麼這個神祕的關鍵關鍵字到底是什麼呢?它又實現了什麼神奇的功能呢?帶着這一連串的疑問,讓我們先來看一個具體的例子。

  我在自己曾經寫的一個GUI框架當中,爲了實現消息與處理函數自動映射的,就需要求助於這種功能。比如說有一個窗口類,它包含若干消息處理函數和一個消息與處理函數的映射 map:(請無視當中的 show() 和 create() 函數,與主題無關)

    class Window
    {
        typedef UINT _Message;
        typedef LRESULT (Window::*_Handler)(_Message);
    
        map<_Message,_Handler> m_handlerMap;
    
    public:
        bool show();
        bool create();
    
    public:
        LRESULT onEvent( WindowEvent<WM_CREATE> );
        LRESULT onEvent( WindowEvent<WM_DESTROY> );
    };

     我需要利用模板元編程 從 0 到 WM_USER  進行循環檢測,檢測 Window 類是否存在該消息對應的處理函數。如果消息對應的處理函數存在,那麼就將消息與函數的映射放進 m_handlerMap 當中。比如說消息 WM_CREATE,我檢測類 Window是否存在 LRESULT onEvent( WindowEvent<WM_CREATE> ) 成員函數,在上例代碼中是存在的,於是我將這樣一個映射放進m_handlerMap:(真正實現的時候,還要考慮函數的類型。不同類型的函數,是不能直 接裝進 map 當中的。不過在這裏請無視例子當中涉及的所有類型轉換,與主題無關)

    pair<WM_CREATE,&Window::onEvent>

  這樣就達到了消息自動映射的目的。而不用像MFC一樣手寫宏去映射。(最後通過努力的確達到了我的目的,我的GUI框架能夠進行自動消 息映射了,然而可以預見,由於幾千個(0-WM_USER)循環,編譯期的速度受到極大影響。所以最終我還是拋棄了這種自動映射實現,而採用了更高效神奇 的方法,這是後話也與本主題無關就先不提)。

  要實現以上的自動映射功能就引出了這樣一個難題:如何編譯期檢測類的某特定名字的成員是否存在。

  功能不負有心人,經過爬山涉水翻山越嶺,我終於在 MSDN 一個偏遠角落裏找着了傳說當中那個神祕的關鍵字:__if_exists(其實還有一個 __if_not_exists)。MSDN 當中這樣說明:__if_exists (__if_not_exists)允許你針對某符號的存在與否條件性地執行語句。使用語法:(注意檢測的是“存在性”,而不是值)

    __if_exists ( /*你要檢測存在性的函數或變量的名字*/ ) { 
     //做些有用的事
    }
  MSDN當中的示例代碼如下:
    // the__if_exists_statement.cpp
    // compile with: /EHsc
    #include <iostream>
    
    template<typename T>
    class X : public T {
    public:
       void Dump() {
          std::cout << "In X<T>::Dump()" << std::endl;
    
          __if_exists(T::Dump) {
             T::Dump();
          }
    
          __if_not_exists(T::Dump) {
             std::cout << "T::Dump does not exist" << std::endl;
          }
       }   
    };
    
    class A {
    public:
       void Dump() {
          std::cout << "In A::Dump()" << std::endl;
       }
    };
    
    class B {};
    
    bool g_bFlag = true;
    
    class C {
    public:
       void f(int);
       void f(double);
    };
    
    int main() { 
       X<A> x1;
       X<B> x2;
    
       x1.Dump();
       x2.Dump();
    
       __if_exists(::g_bFlag) {
          std::cout << "g_bFlag = " << g_bFlag << std::endl;
       }
    
       __if_exists(C::f) {
          std::cout << "C::f exists" << std::endl;
       }
    
       return 0;
    }
    

  以上代碼的輸出如下:(未測試,此輸出爲MSDN的說明文檔當中的)

    In X<T>::Dump()
    In A::Dump()
    In X<T>::Dump()
    T::Dump does not exist
    g_bFlag = 1
    C::f exists

  大概很少人見過這個關鍵字吧。雖然它們的功能與我的需求是如此的接近,但是面對如此強憾的關鍵字,我還是隻能搖頭嘆息。我傷心地在文檔 裏看到說明,__if_exists(__if_not_exists)關鍵字用於函數的時候,只能根據函數名字進行檢測,而會忽略對參數列表的檢測,因 此沒有對重載函數的分辨能力,而正是我需要的。比如類 Window 有一個函數:

    LRESULT Window::onEvent( WindowEvent<WM_DESTROY> )
    {
      //做些有用的事
    }

  我用以下代碼來檢測 WM_CREATE 消息是否存在處理函數:

    __if_exists(Window::onEvent)
  {
      //添加消息映射
   }

  即使 Window 類當中不存在 LRESULT onEvent ( WindowEvent<WM_CREATE> ),以上測試也能通過。這是因爲 __if_exists 關鍵字是不管函數重載的,如果存在一個 onEvent ,那麼所有的檢測都能通過。這不是我想要的。我需要比 __if_exists 更強憾的檢測功能,強憾到能夠針對不同參數列表的同名函數(重載函數)做出正確的存在性測試。

  於是我繼續翻山越嶺地尋找,從 CSDN 到 MSDN,從 SourceForge 到 CodeProject。要相信那句老話:“有心人天不負”。最後我在 CodeProject 上面看到一篇讓我醍醐灌頂的文章:

  Interface Detection by Alexandre Courpron

  這篇文章從原理到實現,很詳細地說明地一種編譯期檢測技術,先說明一下,由於VC7.1數千個bug當中的一個,以下技術不能在VC++7.1或更低版本上使用。具體的實現在那篇文章當中說得很詳盡了,還是在這兒贅述一下。

  Alexandre Courpron的實現方式基於C++的這樣一個規則:Substitution Failure Is Not An Error (簡稱SFINAE)。它的含義我也理解得比較含糊,不過它作用於重載函數的時候,可以這樣理解:對於一個函數調用,在匹配函數的過程當中,如果最終能夠 有一個函數匹配成功,那麼對其餘函數的匹配如果失敗,編譯器也不會視爲錯誤。聽起來有些麻煩,看Alexandre Courpron給出的例子:

    struct Test 
    {
        typedef int Type;
    };
    
    template < typename T > 
    void f(typename T::Type) {}  // definition #1
    
    template<typename T> 
    void f(T){}                  // definition #2
    
    f<Test>(10); //call #1
    
    f<int>(10);  //call #2

     對於 call#1 編譯器直接匹配 definition#1 成功。對於 call#2,編譯器先用 definition#1 匹配 如下:

    void f( typename int::Type ) {}

  這顯然是不正確的。不過編譯器並沒有編譯失敗報告錯誤,因爲下面的 definition#2 匹配成功,根據 SFINAE的 規則,編譯器有權保持沉默 。

  雖然是個小小的規則,在平時幾乎不會注意它。然而在這兒,我們卻可以利用它實現編譯期檢測的強大功能了,一個最簡單的示例:

    #include <iostream>
    using namespace std;
    //
    struct TestClass
    {
        void testFun();
    };
    
    struct Exists { char x;};  
    struct NotExists    { char x[2]; }; 
    
    template <void (TestClass::*)()>
    struct Param ;
    
    template <class T>
    Exists isExists( Param<&T::testFun>* );
    
    template <class T>
    NotExists isExists( ... );
    //
    int main()
    {
        cout<<sizeof(isExists<TestClass>(0))<<endl;
    }

  上面的代碼會輸出 1。說明一下檢測的過程:

  1. 編譯器遇到 isExists<TestClass>(0) 這一句,會去匹配 isExists 的兩個重載函數。不定長的參數優先級更低,因此先匹配第一個函數。
  2. 第一個函數參數類型爲 Param<&T::testFun>*,在這裏是 Param<&TestClass::testFun>,編譯器在匹配這個參數類型的時候會嘗試實例化模板類 Param。
  3. 編 譯器嘗試用 &TestClass::testFun 去實例化 Param,因爲 TestClass 確實存在一個 void (TestClass::*)() 類型,且名爲 testFun 的成員函數。所以 Param 的實例化成功,因此參數匹配成功。
  4. 匹配第一個函數成功。編譯器決定 isExists<TestClass>(0) 這一句調用就是調用的第一個函數。
  5. 因爲第一個函數返回的類型爲 Exists,用 sizeof 取大小就是 1。

  如果是我們把 TestClass 的定義修改爲:(僅把函數的參數類型改爲 int )

    struct TestClass
    {
        void testFun(int);
    };

  這一次代碼會輸出 2。因爲在第3步的時候,由於 TestClass 沒有類型爲 void (TestClass::*)(),且名爲 testFun 的函數,所以實例化 Param 會失敗,因此匹配第一個函數失敗。然後編譯器去匹配第二個函數。因爲其參數類型是任意的,自然會匹配成功。結果會輸出 2。

  當然這只是個最簡單的示例,通過模板包裝類。可以實現更靈活更強大的功能。比如回到那個自動消息映射的例子,用以下代碼就能夠實現了:

//c++std #include <iostream> using namespace std;

//windows #include <windows.h>

//detector template<typename TWindow,UINT t_msg> struct MessageHandlerDetector { typedef WindowEvent<t_msg> _Event; struct Exists {char x;}; struct NotExists {char x[2];}; template<LRESULT (TWindow::*)(_Event)> struct Param; template<typename T> static Exists detect( Param<&T::onEvent>* ); template<typename T> static NotExists detect( ... ); public: enum{isExists=sizeof(detect<TWindow>(0))==sizeof(Exists)}; }; //test classes struct Window { LRESULT onEvent( WindowEvent<WM_CREATE> ); }; struct Button { LRESULT onEvent( WindowEvent<WM_DESTROY> ); }; //main int main() { cout<<MessageHandlerDetector<Window,WM_CREATE>::isExists<<endl; cout<<MessageHandlerDetector<Window,WM_DESTROY>::isExists<<endl; cout<<MessageHandlerDetector<Button,WM_CREATE>::isExists<<endl; cout<<MessageHandlerDetector<Button,WM_DESTROY>::isExists<<endl; return 0; }

  以上代碼會輸出:

    1
    0
    0
    1

  以上的示例代碼再加上模板元編程,可以很輕易地實現消息的自動映射,具體實現這個已不在本貼的討論範圍並且這種自動映射的實現,太過複雜,在編譯期沒有效率,且不夠靈活。不過在消息映射機制上來說,已稱得上是一種革命性的嘗試。

  在說完了這所有一切之後,再告訴你一個我最近才知道的祕密(不準笑我孤陋寡聞):其實 boost 庫當中已有相關功能的 MPL  工具存在,叫做 has_xxx。

  源文件:<boost/mpl/has_xxx.hpp>

  文檔:http://www.boost.org/doc/libs/1_35_0/libs/mpl/doc/refmanual/has-xxx-trait-def.html

posted on 2008-07-06 22:03 cexer 閱讀(810) 評論(4)  編輯 收藏 引用 所屬分類: utility
<script type="text/javascript"> // Sys.WebForms.PageRequestManager._initialize('AjaxHolder$scriptmanager1', document.getElementById('Form1')); Sys.WebForms.PageRequestManager.getInstance()._updateControls(['tAjaxHolder$UpdatePanel1'], [], [], 90); // </script>

Feedback

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