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"> // </script>

Feedback

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值