配合STL算法编写类的成员函数
我们在使用C++的开发工作中,经常会编写许许多多的类,当然也会编写大量的成员函数。但是对于如何设计类的结构,如何确定类的成员,这将会是一个艰难的抉择过程。在编写C++类的过程中,我们都希望使我们的成员函数集合最小化,但是常常很难做到这一点。本文就来详细的讨论一下这个问题!
在游戏开发中有一个单元类:
class Unit
{
public:
void Update();// 更新函数
};
这个类被装到了STL的容器vector中,考虑一下该如何依次调用Unit的Update函数?
vector<Unit> v(10);// 容器中有10个Unit对象
最简单的方法:
for(int i=0;i<v.size();i++)
v[i].Update();
上面的方法的确很直接,而且很简单。但是在STL中有一个更加简单的方法:
for_each(v.begin(),v.end(),mem_fun_ref(&Unit::Update));
通过上面STL的for_each算法就可以直接调用Unit的成员函数Update(通常来说Update都是虚函数)。在此我们看到,只需要一条语句就可以满足我们的需求。实际上如果充分利用STL为我们提供的算法,编写的代码会更加简洁,高效,不容易出错等等。
那么再来考虑一下vector容器中盛放的不是对象而是对象的指针的情况:
vector<Unit*> v(10);// 容器中有10个Unit对象指针
一般来说,只有在避免代码依赖,以及有从Unit派生的多个类的情况下需要采用这里的指针容器。对于这种情况,类似前面的方法,采用STL算法for_each如下:
for_each(v.begin(),v.end(),mem_fun(&Unit::Update));
认真对比前面和这里的两个for_each算法,可以看出,当容器中盛放的是对象的时候就需要采用mem_fun_ref(引用对象的成员函数),而盛放的是指针的时候就可以直接采用mem_fun(默认为对象指针对象的成员函数)。
讨论到这里,还没有接触到本文的主题:关于类的成员函数的设置问题!前面的讨论是一个伏笔,现在就来一步一步的接近我们的主题:
我在开发游戏AI的过程中,出现了两种情况:
1. 从周围的单元中挑选出一定距离之内的并且在一定视角内的邻居单元
2. 从敌军的单元中找出对某个单位威胁最大的敌军单元
关于这两个问题,实际上可以直接给出两个成员函数分别处理。也曾尝试这样处理过,但是在此之后的开发中,遇到了一系列的类似的问题,各个成员函数之间出现了功能重叠。不仅如此,由于上面的两个成员函数的功能都非常强大,所以所需要的参数也非常多,因此也导致了代码臃肿,模样丑陋的问题。关于这种状况的解决,在经历了一段时间之后,俺发现了以前不知道那里看到的关于开发C++类的成员函数的原则有:
尽可能的使成员函数的功能单一
这条原则初次看的时候,没觉得什么神奇,在没有应用STL算法之前,感觉必要性也不是很强,经过这一次的开发,让我深刻的感觉到了这条规则的重要性。因此在此也写出来和大家分享一下:)
先来考虑第一个问题,很明显第一个问题可以被分解为两个问题的“与”运算:
1. 从周围单元中找出和自己的距离在一定的半径内的其他单元
2. 从周围单元中找出和自己的视角在一定角度内的其他单元
鉴于此,就直接给Unit类定义了两个成员函数:
class Unit
{
… …
float Distance(Unit*pkUnit);
float View(Unit*pkUnit);
… …
};
有了这两个成员函数之后就可以非常方便的解决第一个问题了:
list<Unit*> t; // 所有的单元列表
copy(v.begin(),v.end(),back_inserter(t));// 将所有的单元指针复制一份出来
t.remove_if(fgx(bind2nd(greater_equal<float>(),500.0f),bind2nd(mem_fun(&Unit::Distance),pkSelfUnit)));// 移除所有的单元和pkSelfUnit的距离在500.0f之外的单元指针
t.remove_if(fgx(bind2nd(greater_equal<float>(),PI/2),bind2nd(mem_fun(&Unit::View),pkSelfUnit)));// 移除所有的单元和pkSelfUnit的视角在90°之外的单元
经过上面的两次remove_if之后最后的结果就是我们需要的结果了;)这里的两个成员函数也非常自然,功能非常单一。这里引用了一个fgx函数,这是我定义的一个仿函数生成器,类似于make_pair函数:
template<class FUNC>struct PP1// 模板参数预处理器
{
typedef typename FUNC::argument_type A;
typedef typename FUNC::result_type R;
};
template<class F,class G>class fgx_t
:public std::unary_function<typename PP1<G>::A,typename PP1<F>::R>
{
F f;
G g;
public:
fgx_t(const F&_f,const G&_g):f(_f),g(_g){}
typename PP1<F>::R operator()(const typename PP1<G>::A&x)const
{
return f(g(x));
}
};
template<class F,class G>inline fgx_t<F,G> fgx(const F&f,const G&g)
{
return fgx_t<F,G>(f,g);
}
实际上,上面的这个fgx和fgx_t采用的就是SGI的STL中的compose的概念,虽然说Boost很有可能成为标准,但是还没有成为标准。能够用不太大的气力把STL很容易的链接起来的compose也是不错的选择。类似的还有另外三种:fgxhx_t、fgxhy_t、fgxy_t以及他们的辅助函数fgxhx、fgxhy、fgxy,在此不一一列举,通过名字就可以看出代码是什么样子的:)
我们可以把上面的这四个链接剂和高中数学中的数列的递推公式做一个类比,这里的链接剂就是递推公式,我们知道,知道了数列的递推公式,我们就可以知道数列的一切性质。但是也要知道,从递推公式得到数列通项也是一个极其费事的过程。所以我们在此就采用到“递推公式”为止,功能上面已经完全满足了,剩下的就是如何用好的问题了;)
再来考虑第二个问题:找出周围的敌军单元中对自己威胁最大的单元。
那么如何定义威胁呢?不同的策略会导致不同的威胁定义,如果用一个函数来表示威胁似乎很难满足所有的需要,因此就需要对这里的威胁进行分解,例如:距离在一定范围之内,在敌军视角之内的威胁最大。这样的话我们就可以在不给Unit增加成员函数的情况下直接采用之前的两个成员函数来实现之J。是不是很简单啊;)
class Dangerous:public std::binary_function<CSDUnit*,CSDUnit*,bool>
{
CSDUnit*m_pkCompare;
public:
explicit Dangerous(CSDUnit*pkUnit):m_pkCompare(pkUnit){}
bool operator()(CSDUnit*pkUnit1,CSDUnit*pkUnit2)const
{
float D1 = m_pkCompare ->Distance(pkUnit1);
float D2 = m_pkCompare ->Distance(pkUnit1);
return D1 > D2;// 距离越近威胁越大
}
};
有了上面的仿函数之后,就可以采用STL的max_element算法来实现了:
list<Unit*>::iterator it = max_element(v.begin(),v.end(),Unit::Dangerous(pkSelfUnit));
上面的代码是不是可读性非常强啊:)
在此也可以看出,虽然fgx之类的链接剂也可以实现上面的功能,但是定义一个仿函数也是一个很不错的选择哈;)
从前面的这些讨论中,可以看出,尽可能保持类的成员函数集合的最小化将会极大的增强类的功能:)当然要配合STL的算法了;)如果将来Boost成为标准库之后,这里的代码编写将会更加简化;)期待ing…
最后也要特别强调一下,熟悉STL的算法将会改变你对C++的看法;)