3.优化第二次的改进。
在上一篇文章,我们讲到,在做消息映射时,每一次都要new一个msg_handler_base的派生类,这必然有个delete,为了delete这个派生类,在msg_handler_base的构造函数里,定义了一个全局变量g_manager,g_manager在析构时候把msg_handler_base的派生类全部干掉,这无疑会增加内存和性能开销,有没有办法避免这情况的发生呢?另外,在WndProc,做消息的匹配动作时候,用的是顺序查找,若能改做二分查找,性能立刻会上一个数量级,这有没有办法做到呢?在这篇文章里,我们来解决这个问题,或者说,做这个改进。
重看paint和destroy的定义:
struct paint : msg_handler_base
{
template< typename T >
struct function
{
typedef void ( T::*type )( HDC, const RECT & );
};
function< X >::type fun; // fun的类型是 void (X::*)( HDC, const RECT & )
virtual bool do_it( X &x, msg_struct &msg )const
};
struct destroy : msg_handler_base
{
template< typename T >
struct function
{
typedef void ( T::*type )( void );
};
function< X >::type fun;
virtual bool do_it( X &x, msg_struct &msg )const;
};
观看paint和destroy,会发现,它们都有类似之处,都有一个fun变量,但fun类型不同,都有do_it虚函数,但do_it的实现不同,假设fun类型和do_it实现都一样,我们就能用统一的结构来描述它,一旦能用统一的结构,我们就能做如下的定义:
static msgmap_t a={WM_PAINT,fun,&do_it};
先解决fun类型的问题。fun变量,是指向对象成员函数的指针,由于message对应的wParam和lParam的差异化,造成了fun类型的差异,但fun变量,本质是一个成员函数的地址,它的类型信息,是给编译器看的,编译器查看通过以后,类型信息就不存在了。所以,我们可以弱化fun的语义,把它看做是函数的地址:一个整数,没有函数的类型信息:
struct msgmap_t
{
// ...
size_t on_message; // 对应着paint::fun
};
再看do_it函数,do_it是msg_handler_base的纯虚函数,由派生类实现。我们可以在msgmap_t里,用一个成员函数指针指向这个函数,但由于这是个成员函数指针,调用的时候,需要一个"this"值,这会带来麻烦。由于msg_handler_base只有这一个纯虚函数,我们可以把这成员函数指针,改成全局函数指针:
struct msgmap_t
{
// ...
size_t on_message; // 对应着paint::fun
bool(*invoke)(const msgmap_t &a, X & x, msg_struct &); // 对应paint中的do_it
};
这样,我们就设计出了第一个版本的msgmap_t:
struct msgmap_t
{
UINT message;
size_t on_message; // 对应着paint::fun
bool(*invoke)(const msgmap_t &a, X & x, msg_struct &); // 对应paint中的do_it
};
原先paint的do_it实现,放在这里:
struct map_paint : msgmap_t
{
static bool on_map(const msgmap_t &a, X & x, msg_struct &)
{
// ...
return true;
}
};
这时,MyWindow::enable_msg_map()的实现就变成了:
void MyWindow::enable_msg_map()
{
typedef MyWindow self;
static msgmap_t entries[]= {
{WM_PAINT, &self::on_paint, &map_paint::on_map};
// ...
};
// ...
}
这样一来,我们就避免了new和delete开销,还减少了与此相关的代码,且这些值都是在编译时候可知,不会带来运行时的开销。
真正编译的时候,这段代码是通不过。&self::on_paint是个成员函数指针,而对应的on_message是个size_t类型,且对于msg_paint来说,它希望传入的函数指针类型是void (X::*)( HDC, const RECT & ),我们弱化了on_message,万一传入的函数类型不是msg_paint所期望的,这会带来极其严重的问题。这个问题,我们可以通过一个间接层解决:
struct map_paint : msgmap_t
{
template<typename T>
static inline size_t map_fun_addr(void (T::*f)(HDC, const RECT &))
{
return *reinterpret_cast<size_t *>(&f);
}
// ...
};
加入了一个map_fun_addr函数,入口参数验证成员函数的类型,里面将函数指针强行转换成整数,MyWindow::enable_msg_map()变成
void MyWindow::enable_msg_map()
{
typedef MyWindow self;
static msgmap_t entries[]= {
{WM_PAINT, map_paint::map_fun_addr(&self::on_paint), &map_paint::on_map};
// ...
};
// ...
}
理论上,编译器能去掉这间接层的运行开销。
WM_DESTROY消息同样处理
struct map_destroy : msgmap_t
{
template<typename T>
static inline size_t map_fun_addr(void (T::*f)())
{
return *reinterpret_cast<size_t *>(&f);
}
static bool on_map(const msgmap_t &a, X & x, msg_struct &)
{
// ...
return true;
}
};
MyWindow::enable_msg_map()变成如下:
void MyWindow::enable_msg_map()
{
typedef MyWindow self;
static msgmap_t entries[]= {
{WM_PAINT, map_paint::map_fun_addr(&self::on_paint), &map_paint::on_map};
{WM_DESTROY, map_destroy::map_fun_addr(&self::on_DESTROY), &map_destroy::on_map};
};
// ...
}
由于去掉了msg_handler_base,有些地方也要做相应的改动,我们把msg_value重命名成mapslot:
struct mapslot
{
void * wnd;
const msgmap_t * entries;
size_t entry_count;
void assign(void *map_to, const msgmap_t *entries1, size_t n)
{
wnd= map_to;entries=entries1;entry_count=n;
}
};
而basewnd:
class basewnd
{
public:
mapslot m_slot;
template<typename W>
void map_msg(W *map_to, const msgmap_t *entries, size_t n)
{
m_slot->assign(map_to, entries, n);
}
};
这样,完整的MyWindow::enable_msg_map如下:
void MyWindow::enable_msg_map()
{
typedef MyWindow self;
static msgmap_t entries[]= {
{WM_PAINT, map_paint::map_fun_addr(&self::on_paint), &map_paint::on_map};
{WM_DESTROY, map_destroy::map_fun_addr(&self::on_destroy), &map_destroy::on_map};
};
map_msg(this, entries, sizeof(entries)/sizeof(entries[0]));
}
在WndProc里,对应的改动
LRESULT CALLBACK WndProc(...)
{
// ...
if ( p != 0 )
{
// ...
const mapslot &slot= p->m_slot;
for( size_t j= 0; j < slot.entry_count; ++j )
{
const msgmap_t &v= slot.entries[j];
if ( v.message != msg.message )
continue;
if ( v.invoke(v, slot.wnd, msg ) )
return msg.result;
}
p->msg_default( msg );
return msg.result;
}
return ::DefWindowProc( hWnd, message, wParam, lParam );
}
重看void MyWindow::enable_msg_map()的实现,里面有个typedef,这typedef是否是必须的?在同一个entries数组里,里面对应的映射函数,必须来自同一类型。若类型不一样,比如&A::on_paint和&B::on_destroy,编译器不会报错,但运行时候就不是所期望的,这bug很难查找。另外,数组里,每一项,太长,不直观,所关注的信息,有些淹没在实现的细节上。思来想去,也没有什么好的解决方法,只能用宏解决。
#define WABC_BEGIN_MSG_MAP(thisClass) \
{ typedef thisClass map_class; \
static wabc::msgmap_t entries[] = {
#define WABC_END_MSG_MAP() \
}; \
(*this).map_msg<map_class>(this,entries, sizeof(entries)/sizeof(entries[0])); }
#define WABC_ON_DESTROY(f) \
{ WM_DESTROY, map_destroy::map_fun_addr<map_class>(f), &map_destroy::on_message },
#define WABC_ON_PAINT(f) \
{ WM_PAINT, map_paint::map_fun_addr<map_class>(f), &map_paint::on_message },
void MyWindow::enable_msg_map()
{
WABC_BEGIN_MSG_MAP(MyWindow)
WABC_ON_PAINT(&MyWindow::on_paint)
WABC_ON_DESTROY(&MyWindow::on_destroy)
WABC_END_MSG_MAP()
}
这代码的可读性,比以前好了不少。注意WABC_END_MSG_MAP()里的这句代码:(*this).map_msg<map_class>(...),显示的加上“map_class”,这是否必需?毕竟map_class可以由map_msg的第一个参数自动推断出来。前面说过,在同一个数组里,里面对应的映射函数,必须来自同一类型,显示的加入map_class,万一map_msg第一个参数不是map_class类型,编译器会立刻报错。这为正确的使用消息映射加了一层防范措施。
下面讨论第二个问题,做消息匹配的时候,如何应用二分查找。二分查找,必须是排序的。要实现排序,有两种方法:第一个,运行时做一次排序,第二,定义消息映射的时候,必须按消息大小顺序定义。在这里,选择第二个方法,一般情况下,映射的函数不会多,且使用多了,每个message的值都有记忆。常用的message就那几个。但既然要做二分查找,就必须保证有序。在debug版本里,加入调试代码,确保消息映射有序。
void mapslot::assign(void *map_to, const msgmap_t *entries1, size_t n)
{
// ...
#ifdef _DEBUG
for(size_t i=1;i<entry_count;++i)
assert(entries[i-1].message<entries[i].message);
#endif
}
由于WM_DESTROY<WM_PAINT,所以
void MyWindow::enable_msg_map()
{
WABC_BEGIN_MSG_MAP(MyWindow)
WABC_ON_DESTROY(&MyWindow::on_destroy)
WABC_ON_PAINT(&MyWindow::on_paint)
WABC_END_MSG_MAP()
}
在WndProc里:
struct msgmap_t
{
// ...
bool operator < (const msgmap_t &rhs)const
{
return message < rhs.message;
}
}
LRESULT CALLBACK WndProc(...)
{
typedef std::pair<const msgmap_t *, const msgmap_t *> pair_type;
// ...
if ( p != 0 )
{
// ...
const mapslot &slot= p->m_slot;
msgmap_t v;
v.message = msg.message;
pair_type pr = std::equal_range(slot.entries, slot.entries + slot.entries_count, v);
if (pr.first != pr.second)
{
const msgmap_t &v= *pr.first;
if ( v.invoke(v, slot.wnd, msg ) )
return msg.result;
}
p->msg_default( msg );
return msg.result;
}
return ::DefWindowProc( hWnd, message, wParam, lParam );
}
总结:
在这次优化,我们去掉了msg_handler_base,用纯c的方式去解决映射的问题,带来的好处也是显而易见,去掉了vtable,减少了代码,在内存的使用和运行的开销上都比原先的经济。但为了解决函数类型匹配的问题,又回到了c++的领域,利用了template的特性。做为一个公共库,当前所做的,还只是基础中的基础,这里,重要的是思路的描述,以及碰到问题时各种解决方案的利弊分析。
请点击这里下载'wabc'库的最终源码。