cexer

cexer
posts - 12, comments - 334, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

转帖请注明出处 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

Feedback

# re: C++编译期函数/变量检测技术,超越VC的某某关键字  回复  更多评论   

2008-07-06 23:54 by Bill Gates
不就是traits吗

# re: C++编译期函数/变量检测技术,超越VC的某某关键字  回复  更多评论   

2008-07-06 23:56 by cexer
不是。并不是涉及模板就能用traits来说事的。

# re: C++编译期函数/变量检测技术,仿真VC关键字__if_exists  回复  更多评论   

2008-07-08 18:29 by 周星星
好方法呀,俺也一直在寻找 __if_exists 的替代方法(用标准C++语法)

# re: C++编译期函数/变量检测技术,仿真VC关键字__if_exists  回复  更多评论   

2008-07-08 19:50 by cexer
你可以看看,boost有更好的实现。

# re: C++编译期函数/变量检测技术,仿真VC关键字__if_exists  回复  更多评论   

2008-07-10 23:13 by 梦在天涯
没用过,有难度,长见识!

# re: C++编译期函数/变量检测技术,仿真VC关键字__if_exists  回复  更多评论   

2008-08-19 12:47 by 蚂蚁终结者
不错,有点意思。

# re: C++编译期函数/变量检测技术,仿真VC关键字__if_exists  回复  更多评论   

2009-09-24 11:20 by pipilu
问一下怎么编译器比较两个常数来设置编译呢

#define AAA 12

// 如果 AAA > 12 则编译下面内容
bool bUse = 134;

# re: C++编译期函数/变量检测技术,仿真VC关键字__if_exists  回复  更多评论   

2012-06-01 01:09 by 春秋十二月
不错 受益了

# re: C++编译期函数/变量检测技术,仿真VC关键字__if_exists  回复  更多评论   

2013-05-30 14:01 by zirandeai
我觉得这只是语法糖,增加了复杂度,却并未让人看到实质性的好处

只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理