win32消息映射4-优化第二次的改进

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'库的最终源码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值