转帖请注明出处 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。说明一下检测的过程:
- 编译器遇到 isExists<TestClass>(0) 这一句,会去匹配 isExists 的两个重载函数。不定长的参数优先级更低,因此先匹配第一个函数。
- 第一个函数参数类型为 Param<&T::testFun>*,在这里是 Param<&TestClass::testFun>,编译器在匹配这个参数类型的时候会尝试实例化模板类 Param。
- 编译器尝试用 &TestClass::testFun 去实例化 Param,因为 TestClass 确实存在一个 void (TestClass::*)() 类型,且名为 testFun 的成员函数。所以 Param 的实例化成功,因此参数匹配成功。
- 匹配第一个函数成功。编译器决定 isExists<TestClass>(0) 这一句调用就是调用的第一个函数。
- 因为第一个函数返回的类型为 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;
}
以上代码会输出:
以上的示例代码再加上模板元编程,可以很轻易地实现消息的自动映射,具体实现这个已不在本贴的讨论范围并且这种自动映射的实现,太过复杂,在编译期没有效率,且不够灵活。不过在消息映射机制上来说,已称得上是一种革命性的尝试。
在说完了这所有一切之后,再告诉你一个我最近才知道的秘密(不准笑我孤陋寡闻):其实 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。