清风竹林

ぷ雪飘绛梅映残红
   ぷ花舞霜飞映苍松
     ----- Do more,suffer less

基类角色之对象管理器

基类角色之对象管理器

版本:0.1

最后修改:2009-07-02

撰写:李现民


问题描述

C++程序设计中,保存一个生命周期不是由类对象自己维护的其它对象的指针通常是个坏主意,因为程序逻辑很难判断在使用该指针的时刻其所指对象是否已经被销毁。这种应用需求很常见,例如在网游设计中,由于华丽的装备加载需要进行大量硬盘I/O,因此加载过程通常由一个独立的加载线程执行,由于在装备加载完成的时刻该玩家很可能已经下线,因此加载线程就需要能够去判断此时玩家对象是否仍然有效。

为了解决该问题,通常会设计一个PlayerManager类用于跟踪管理当前所有的玩家对象,而加载线程通过提供玩家id以确认该玩家对象仍然存在。此种设计方案需要一个独立的PlayerManager类,并提供一个全局的PlayerManager类对象以跟踪当前的所有玩家对象。

出于代码复用的目的,我希望实现一个通用基类解决此类问题。该基类需要为子类对象至少提供以下几方面的能力:

  1. 为所有的对象分配一个全局唯一的index,通过该index能够(尽可能快的)获取到拥有该index的类对象(或NULL);

  2. 自动跟踪类对象的生成与销毁,不需要手工编写额外代码;

  3. 实现迭代器,提供遍历当前所有有效对象的能力;

  4. 提供“移除”接口,使得对象可以主动要求放弃被对象管理器跟踪;

  5. 各子类实现拥有完全独立的管理器逻辑;


解决方案

将实现代码保存为objectman.hpp,内容如下:

/********************************************************************
created:    2009-06-29
author:    lixianmin

purpose:    base class for object manager
Copyright (C) 2009 - All Rights Reserved
********************************************************************
*/
#ifndef _LIB_OBJECT_MAN_HPP_INCLUDED_
#define _LIB_OBJECT_MAN_HPP_INCLUDED_
#include 
<cassert>
#include 
<map>

namespace lib
{
    template
<typename T>
    
class objectman
    {
    
public:
        typedef 
int index_t;                                // 索引类型
        typedef std::map<index_t, T*>   object_map;         // 容器类型

        
enum { INVAID_INDEX= 0};                            // 无效索引

    
public:
        
// 迭代器
        class iterator
        {
        
public:
            iterator(
void): _iter(_mObjects.begin()){}                          // 构造函数
            bool has_next(voidconst { return (_mObjects.end()!= _iter); }     // 测试是否还有下一个对象
            T* next(void) { return has_next()? (_iter++->second): NULL;    }    // 获取下一个对象指针

        
private:
            typename object_map::iterator _iter;
        };

    
public:
        
// 构造函数
        objectman(void)
        {
            enable_index();
        }

        
// copy 构造函数
        objectman(const objectman& rhs)
        {
            enable_index();
        }

        
// 析构函数
        virtual ~objectman(void)
        {
            disable_index();
        }

        
// 赋值操作符
        objectman& operator= (const objectman& rhs)
        {

        }

        
// 通过索引获取对象
        static T* get_by_index(index_t index)
        {
            object_map::iterator iter
= _mObjects.find(index);
            
if (_mObjects.end()!= iter)
            {
                T
* pObject= iter->second;
                assert(NULL
!= pObject);
                
return pObject;
            }

            
return NULL;
        }

        
// 获取对象索引
        index_t get_index(voidconst { return _idxObject; }

        
// 生成索引(使能被对象管理器遍历到)
        void enable_index(void)
        {
            _idxObject
= ++_idxGenderator;
            assert(get_index()
!= INVAID_INDEX);
            assert(_mObjects.find(get_index())
== _mObjects.end());
            _mObjects.insert(std::make_pair(get_index(), static_cast
<T*>(this)));
        }
        
        
// 移除索引(使不能被对象管理器遍历到)
        void disable_index(void)
        {
            
if (get_index()!= INVAID_INDEX)
            {
                assert(_mObjects.find(get_index())
!= _mObjects.end());
                _mObjects.erase(get_index());

                _idxObject
= INVAID_INDEX;
            }
        }

    
private:
        friend 
class iterator;
        
static object_map    _mObjects;                          // 对象容器
        static index_t        _idxGenderator;                    // 索引发生器

        index_t                _idxObject;                      
// 对象索引
    };

    template
<typename T>
    typename objectman
<T>::object_map objectman<T>::_mObjects;

    template
<typename T>
    typename objectman
<T>::index_t objectman<T>::_idxGenderator= 0;
}

#endif

测试代码

测试代码如下:

#include <cassert>
#include 
"objectman.hpp"
// 声明一个类
class Player:public lib::objectman<Player>
{

};

int main(int argc, char* argv[])
{
    
const int idxDisabled= 5;
    
// 生成对象
    for (int i= 0; i< 10++i)
    {
        Player
* pPlayer= new Player;
        
if (idxDisabled== pPlayer->get_index())
        {
            
// 从对象管理器中移除该对象
            pPlayer->disable_index();
        }
    }
    
    
//使用迭代器遍历类对象
    Player::iterator iter;
    
while(iter.has_next())
    {
        Player
* pPlayer= iter.next();
        
const int idxPlayer= pPlayer->get_index();
        
// 断言之:遍历不到已经移除的对象
        assert(idxPlayer!= idxDisabled);
        
// 断言之:可以通过idxPerson取得对象指针
        assert(pPlayer== Player::get_by_index(idxPlayer));
        
        
// 回收对象
        delete pPlayer;
        pPlayer
= NULL;
    }

    
// 断言之:所有对象均已被删除
    Player::iterator iter2;
    assert(
!iter2.has_next());

    system(
"pause");
    
return 0
}

vs2008下所有断言均动作通过。


已知问题

  1. 在大量生成对象的情况下,index索引空间(代码定义为int的范围)有可能使用殆尽,甚至产生重复,这会导致两个对象拥有相同index的严重错误;

  2. std::map的查找速度不是特别另人满意;






posted on 2009-07-02 12:49 李现民 阅读(1981) 评论(11)  编辑 收藏 引用 所属分类: design

评论

# re: 基类角色之对象管理器 2009-07-02 14:10 Kevin Lynx

本质上就是通过让其他模块不保存这个object的直接指针,而是一个ID,然后通过一个manager由这个ID而获取到这个object*,是可以有效减少指针无效的问题。但是,面对的最大问题就在于这个查找过程。你这里用的是std::map,我们用的是stdext::hash_map,我自己都觉得有点速度问题。希望能在这个问题上找到良好的解决办法。:)  回复  更多评论   

# re: 基类角色之对象管理器 2009-07-02 15:20 饭中淹

我一般是用一个ID管理器来解决这个问题。
采用直接索引的办法。对于重复的ID使用一个顺序增加的KEY来做验证。

也就是一个ID,一个KEY来索引一个对象。

之前还有一个方法,就是采用一种LINK对象,LINK对象能链接到另外一个LINK对象,并互换HOST指针,并且有主从关系的LINK方式。
当一个LINK对象销毁的时候,会通知所有已经连接的LINK对象放弃自己的HOST指针。这样的话,别的对象里面保存的指针会随着原始对象的销毁而自动清空。
  回复  更多评论   

# re: 基类角色之对象管理器 2009-07-02 17:24 Kevin Lynx

虽然不是很明白饭中淹关于LINK对象的意思,但是我获得了灵感,写了下面的代码:
///
///
///
#include <stdio.h>
#include <list>

class ref_op
{
public:
virtual void be_null() { }
};

typedef std::list<ref_op*> RefOpList;
class ref_base
{
public:
~ref_base()
{
clear_ops();
}

void add_ref( ref_op *op )
{
_oplist.push_back( op );
}

void clear_ops()
{
for( RefOpList::const_iterator it = _oplist.begin();
it != _oplist.end(); ++ it )
{
(*it)->be_null();
}
}

private:
RefOpList _oplist;
};

template <typename _Tp>
class auto_null : public ref_op
{
public:
void fetch( _Tp *t )
{
_t = t;
t->add_ref( this );
}
auto_null<_Tp> &operator = ( _Tp *t )
{
fetch( t );
return *this;
}
void be_null()
{
_t = 0;
}
operator _Tp*()
{
return _t;
}
private:
_Tp *_t;
};

//////////////////////////////////////////////////////////////////////////////
class CMonster : public ref_base
{
};

class CMonsterAI
{
public:
void SetOwner( CMonster *pOwner )
{
m_Owner = pOwner;
}

void Test()
{
if( (CMonster*)m_Owner == NULL )
{
printf( "The owner is null.\n" );
}
else
{
printf( "The owner is NOT null.\n" );
}
}
private:
auto_null<CMonster> m_Owner;
};

int main()
{
CMonster *pMonster = new CMonster();
CMonsterAI *pAI = new CMonsterAI();
pAI->SetOwner( pMonster );
pAI->Test();
delete pMonster;
pAI->Test();
delete pAI;
return 0;
}

CMonster内部会保存一个CMonster的指针,当CMonster被删除掉时,会自动更新CMonsterAI内部的CMonster“指针”为NULL。接口比较简单:1)在会被引用(即被其他对象保存其指针)的类设计中派生(或组合)ref_base类,ref_base类简单来说就是保存一个引用类对象列表,通过基类ref_op可以使得ref_base保存不同类型的引用类(例如CMonsterAI可以保存CMonster, CRegion也可以保存CMonster),ref_base在析构时自动回调引用类对象的be_null函数,be_null函数在auto_null类中自动把CMonster*设置为NULL。
通过重载operator=,使得SetOwner函数里不需要做其他操作(看起来)。
  回复  更多评论   

# re: 基类角色之对象管理器 2009-07-02 18:02 李现民

@饭中淹
对于“也就是一个ID,一个KEY来索引一个对象”这种办法,我想可以通过增加ID(我我的实现代码是是index_t)的长度来代替,即定义:
typedef __int64 index_t; // 索引类型

我认为效果是等价的。

  回复  更多评论   

# re: 基类角色之对象管理器 2009-07-02 18:05 李现民

@Kevin Lynx
我感觉那个link的意思有点像观察者observer,即:当一个对象销毁时通知所有对其感兴趣的对象,告诉它们:我死了,不要再来找我

但这种方式,我个人觉得有点复杂了  回复  更多评论   

# re: 基类角色之对象管理器[未登录] 2009-07-02 21:51 尘埃

id用两个部分组合而成:数组索引、累加,64位或许应该够了,且短时间内很难发生碰撞吧。数组索引用来立即找出对应对象,再用累加部分验证一下,实在不放心把累加部分扩大到64位,对于客户端来说怎么都该够了;)  回复  更多评论   

# re: 基类角色之对象管理器 2009-07-03 09:55 zuhd

@Kevin Lynx
兄弟这段代码很经典啊,学习了!   回复  更多评论   

# re: 基类角色之对象管理器 2009-07-03 10:16 饭中淹

@Kevin Lynx
跟这个差不多。不过不用继承

xLink<Host, Target>


xLink.link( xLink<Target, Host> & lnk );
xLink.disLinkAll();

*
&
是重载的。
返回Host
  回复  更多评论   

# re: 基类角色之对象管理器 2009-07-03 10:18 饭中淹

@李现民
确实是类似观察者这种,不过更直接了
  回复  更多评论   

# re: 基类角色之对象管理器 2009-07-03 12:03 李现民

@Kevin Lynx
今天我看了下,你这种实现方式就已经是观察者了,其中CMonster 就是被观察的(observable),而所有使用auto_null<>对象的都是观察者(observer), 这种实现方法我觉得至少有两个缺点:
1,会强制observable都使用ref_base 基类,同时observer需要使用auto_null<>去封装对象,因此即使新设计的类可以使用auto_null,但旧有的、需要被观察的类无法自动成为observable,因为它们不曾继承ref_base
2,使用auto_null对象时,IDE的智能通常无法诊测出具体对象(CMonster)内部的函数名,只能诊测出像fetch()这些  回复  更多评论   

# re: 基类角色之对象管理器 2009-07-04 16:29 99网上书店

确实是类似观察者这种,不过更直接了  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理