如何写出健壮的C++代码?

这个问题是没有完整的答案的,我们永远在路上。

还有一种说法是,要写出健壮的C++代码,你要先能写出健壮的代码。

扯远了,回到话题。

昨天写了个类,功能是从字符串中读取算术表达式,计算后返回结果。实际上就是Python的eval()函数,不过C++和AS3都没有这个功能,所以要自己写一个。实现也比较简单,首先是要解析字符串生成逆序波兰式,然后再递归运算逆序波兰式得到结果。每个波兰式节点有以下定义:

enum RPN_ENTRY_TYPE
{
    OPERATOR_TYPE = 0,
    OPERAND_TYPE,
}

struct RPN_ENTRY
{
    RPN_ENTRY_TYPE eType;
    union
    {
        char chOperator;
        int nOperand;
    }
}

每个节点有可能保存着操作符(operator)或者操作数(operand),有一个枚举用以标识当前节点是什么类型,随后跟着一个存放着操作符或操作数的联合体。

继续阅读

关于C++变长参数的一些细节

存在仅含变长参数的函数吗?
当然可以,至少编译器不会报错。你可以像这样定义一个仅含变长参数的函数:

void foo(...)
{
    //...
}

不过这样的函数基本上没什么用,主要原因是你无法取出它的参数。C++通过宏va_start以及va_arg从函数的变长参数列表中取值,其中va_start用以定位变长参数列表的起始地址,这需要借助于参数列表中最后一个固定参数:

var bar(int firstArg, ...)
{
    va_list pArgs;
    va_start(pArgs, firstArg);
    int nextArg = va_arg(pArgs, int);
}

当然,由于函数的参数都在栈中,因此也许有些十分诡异的手段可以直接从栈里将参数抠出来,不过这些用法太高端,我也不懂。
继续阅读

使用boost::any实现的“泛用”消息体

C/C++中传统的消息体通常是以C struct实现的。在这种消息体中,第一个int型的字段标识了该消息的类型(通常是枚举值),第二个int型的字段标识了该消息体的长度,再往下才是消息体本身。这样的消息体效率很高,但有几个缺点。首先是消息体的长度是固定的,第二是消息体内容的格式也是固定的,并且需要在C/C++中定义。

我希望SaberCore是脚本驱动的,如果需要在C++中定义消息体结构,那就实在是太煞风景了。我想到的第一个解决方案是在Lua脚本中定义消息的结构,但这种方法实现起来麻烦用起来也麻烦。第二个解决方案是定义一种“泛用”型消息体,可以胜任任何种类的消息数据。

一提起“泛用”型消息体,第一个想到的就是Android中的intent,intent中可以附带数据extra。它之所以是泛用的,是因为extra不但是变长的,而且你可以向extra中put入任意类型的数据。因为Java所有对象都派生自共同的基类Object,并且所有东西都是new出来的,所以这点其实很容易做到。C/C++中最大的问题在于通常情况下同一个容器中不能放置不同类型的数据。当然我们可以在里面放void *指针,但new出来的东西需要被delete,我们总不能惦记着一个消息体用完还要delete吧?

下面隆重介绍解决方案:boost::any,当当当当!简单直白地说,boost::any可以让C++变成弱类型语言。假如我们定义了这样的一个容器:

std::vector<boost::any> v;

那么你就可以这样放置数据:

v.push_back(1);
v.push_back("我是字符串");
v.push_back(true);
v.push_back(std::vector<int>())

可以这样取出数据:

int a = boost::any_cast<int>(v[0]);
std::string b = boost::any_cast<std::string>(v[1]);
bool c = boost::any_cast<bool>(v[2]);
std::vector<int> d = boost::any_cast<std::vector<int>>(v[3]);

是不是很碉堡

有了这样的一个工具,泛用消息体就很容易定义了。下面是其代码实现:

class scEvent
{
private:
    // 使用字符串而不是枚举型来定义事件类型
    std::string mName; 
    // 使用key - value的方式来索引数据
    std::map<std::string, boost::any> mItems;
public:
    // 各种get/set函数...
};

有兴趣的可以看看boost::any的源码实现,不得不感叹写boost的那些高手怎么可以想到C++还能这么写。

这样的一个泛用型消息体缺点当然大大地有,并且主要是在效率上的。它的体积会比传统的消息体大上几倍,因此拷贝需要消耗更多的时间。同时使用字符串而不是枚举值来区分事件类型,字符串比较也会消耗很多的时间。但便利性和效率向来是难以共存的~也许当性能出现瓶颈时,我会考虑使用传统的消息体。但现在嘛~ 🙂

SaberCore中的多线程与消息机制

经过这么多天艰难的摸索,SaberCore的多线程和消息机制终于初见雏形了。虽然实现得不是很高效,但丑媳妇总要见婆婆,还是在这里分享出来。

SaberCore中的多线程:

SaberCore是完全运行在时间轴上的引擎。我曾在这篇日志中介绍过时间轴动画系统的设计,其实动画时间轴是由普通的时间轴派生而来:

普通时间轴控制着调用频率,引擎中的各组件以回调函数的方式将自己注册进时间轴:

细心的人会发现我之前的截图中,窗口FPS永远都是62帧 : ),这是因为渲染模块所在的时间轴被限制了调用频率。

时间轴分为主轴和次轴,所有主轴运行在主线程上(即使调用频率不一样),所有次轴运行在分支线程上,并且任意两条次轴不会运行在同一个分支线程上。这样,SaberCore是一个天生的多线程引擎,但她也可以很容易地变为单线程的(只需要让所有的时间轴都跑在主线程上)。

SaberCore中的消息系统:

有点类似安卓,如果在其他线程上直接调用渲染线程的函数进行绘制,那肯定会出问题。因此,不同的时间轴之间通过传递消息来进行通信。消息的第一个字段是name,起到了id的作用。所有的消息统一由消息路由器负责分发管理,消息路由会保证任何两个不同类型的消息不会具有相同的name属性。

为了接受和发送消息,我们要做以下几件事:

  1. 创建一个消息队列用以接收消息,每个消息队列有一个独一无二的名字以作标识。
  2. 向消息路由中注册事件,其格式为(事件名称, 消息队列名称)。
  3. 向消息路由中put事件,路由会根据事件的名字将事件递送到指定的消息队列。
  4. 用户周期性地从自己的消息队列中fetch事件。

这是一个邮差模型,一条消息只有一个目的地,不支持广播。消息路由就像是邮差一样,将收集到的信件按照信件上的地址递送到不同的邮箱。其实,就游戏引擎内部而言,一条消息并不需要有多个接收者,所以这样的一个模型是够用的。

下面的图展示消息、消息队列和消息路由之间的关系。虽然画得有点龊,但应该够直白。

由于消息系统是为多线程设计的,因此每个消息队列都拥有自身的锁以防止不好的事情发生。云风的这篇博文中介绍了一种无锁的多线程消息队列,非常具有启发性。但要实现这样一个队列难度很大。首先它是用指针移动的原子操作来避免多线程下的竞争条件,但这个原子操作要怎么实现我还没想明白 – – 。再者就是我为了贪图方便,大量使用了stl中的容器以及boost中的智能指针。而这样的一个队列要想高效地实现,必须要使用原始的指针和自定义的数据结构。所以说,还是算了吧~


分享一下我在编码过程中遇到的一些小问题吧。

在EventRouter中,我将多个EventQueue放入一个std::map中管理,没想到出现了编译错误。我找了半天,终于发现原因出在boost::mutex上。我使用了boost::mutex作为多线程锁,boost::mutex是不可拷贝的,它继承自boost::noncopyable。当我把EventQueue放入std::map中时,会发生拷贝行为,于是就出现了编译错误。解决方法也很简单,就是放指针而不是放实例。

我之前在某本书上看到这么一句话“现代的C++程序中完全可以做到不出现delete”,结果一时头热在引擎中大量使用了boost::shared_ptr,包括消息。我的消息体并不是一个C struct而是一个类,不同类型的消息通过派生这个类来实现。我的消息队列中存放的不是消息体,而是消息的指针。当然,这个指针是经过shared_ptr包装的。
比如说我有一个叫做scTestEvent的消息是派生自scEvent的,所以我可以这样:

shared_ptr<scEvent> evt = shared_ptr<scEvent>(new scTestEvent());
scEventRouter::getSingleton().putEvent(evt);

那么当我将其从消息队列中取出来,并想转回scTestEvent类型,一开始我是这么做的:

shared_ptr<scTestEvent> e = shared_ptr<scTestEvent>(static_cast<scTestEvent>(evt.get()));

你们可以先无视这个蛋疼的语法。这里最大的问题在于,我将scTestEvent的原始指针从一个shared_ptr中取出来后,又拿去构造了另一个shared_ptr。这样,当其中一个指针先被销毁时,它会顺带销毁内部的原始指针。此时另一个智能指针内部的原始指针就变成了野指针。

下面是我随后的做法:

scTestEvent* e = static_cast<scTestEvent*>(evt.get());

这样就不会出问题了。

AutoWrapLuaApi v0.3

今天顺手把自动为lua包装C++的脚本给完善了下,总体来说v0.3版本有以下改进:

  1. 仅导出public区域的api
  2. 支持成员函数重载
  3. 支持运算符重载

还存在许多问题:

其一是依旧未实现导出类的static函数。

其二是成员函数重载有一些问题,但我觉得是luabind的问题。我的导出格式是没有问题的,但有一些重载函数无法通过C++的编译。例如以下这两条函数:

.def("ToAngleAxis", (void (Matrix3::*)(Ogre::Vector3 &,  Ogre::Radian &))&Matrix3::ToAngleAxis)
.def("ToAngleAxis", (void (Matrix3::*)(Ogre::Vector3 &,  Ogre::Degree &))&Matrix3::ToAngleAxis)

貌似luabind无法区分Radian和Degree两个类型,再比如下面两条函数

.def("ToAxes", (void (Quaternion::*)(Ogre::Vector3 *))&Quaternion::ToAxes)
.def("ToAxes", (void (Quaternion::*)(Ogre::Vector3 &,  Ogre::Vector3 &,  Ogre::Vector3 &))&Quaternion::ToAxes)

貌似在luabind眼中,一个Vector3 * 和三个Vector3 &是同一种东西?但下面这条加了const的又可以通过编译

.def("FromAxes", (void (Quaternion::*)(const Ogre::Vector3 *))&Quaternion::FromAxes)
.def("FromAxes", (void (Quaternion::*)(const Ogre::Vector3 &,  const Ogre::Vector3 &,  const Ogre::Vector3 &))&Quaternion::FromAxes)

这些依旧需要在C++中手工编辑一下

其三是luabind仅支持有限的几个运算符重载,分别是:
+   –   *   /   ==   <   <=
其余的运算符重载均无法导出。注意,以上的运算符均为二元运算符。

工具github地址:https://github.com/kidsang/LuabindAutoWrap

一个使用关键帧与属性结合的动画系统的实现

在上次的日志中说到,准备放弃从渲染引擎开始写一个游戏引擎。目前希望使用Ogre作为图像引擎,再和其它乱七八糟的组件一起“拼装”一个引擎出来。前一段时间在做实训的时候(使用了Ogre),对Ogre的动画模块有些失望。我希望用户可以非常方便地创建各种动画,因此我萌生了实现一个“通用”动画系统的想法。

受多方面的启发,我希望我的动画系统有着以下的一些特性。

  1. 动画挂载在时间轴上,通过对时间轴进行缩放,可以调节动画的速度。这是受到了GEA的启发。
  2. 我希望像WPF那样把动画做成属性动画,也就是通过改变对象的属性来完成动画。
  3. 我希望像Flash那样提供关键帧,关键帧之间的动画可以使用不同的插值方程(线性、二次等)。
  4. 我希望可以用XML来表示动画,就像WPF那样。

在两天的努力,以及亮哥大神的帮助下,我终于基本完成了!下面是对我的动画系统的小小演示。

我的测试环境用的是Ogre的基础项目工程,原本的基础工程场景创建的代码和效果图如下。

void scAnimationTest::createScene(void)
{
    Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");

    Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode();
    headNode->attachObject(ogreHead);

    // Set ambient light
    mSceneMgr->setAmbientLight(Ogre::ColourValue(0.5, 0.5, 0.5));

    // Create a light
    Ogre::Light* l = mSceneMgr->createLight("MainLight");
    l->setPosition(20,80,50);
}

现在,我为这个Ogre头增加两个动画,一个令他前后移动,一个令他自转。因此这个头现在会在前后移动的同时自转。

为了完成这个效果,我增加了几行代码。现在createScene()函数看起来是这样的:

void scAnimationTest::createScene(void)
{
    Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");

    Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode();
    headNode->attachObject(ogreHead);

    // Set ambient light
    mSceneMgr->setAmbientLight(Ogre::ColourValue(0.5, 0.5, 0.5));

    // Create a light
    Ogre::Light* l = mSceneMgr->createLight("MainLight");
    l->setPosition(20,80,50);

	// 创建时间轴
	scTimeLine* tl = mTimeLineManager->createTimeLine("me", 60);
	// 创建动画
	scVector3Animation* ani = tl->createVector3Animation([headNode](const Ogre::Vector3& position){headNode->setPosition(position);}, true);
	// 创建关键帧
	ani->createKeyFrame(2000, Ogre::Vector3(0, 0, -500));
	ani->createKeyFrame(4000, Ogre::Vector3(0, 0, 0));

	// 创建时间轴
	tl = mTimeLineManager->createTimeLine("me2", 5);
	// 创建动画
	scF32Animation* ani2 = tl->createF32Animation([headNode](f32 rad){headNode->yaw(Ogre::Radian(rad));}, true);
	// 创建关键帧
	ani2->createKeyFrame(0, 0.5f);
}

是不是非常简洁!实际上我只做了三件事:

  1. 创建一条时间轴。在上面我创建了两条时间轴,那是因为我希望两个动画可以分别以不同的频率被调用(60Hz和5Hz)。
  2. 创建特定类型的属性动画。为了使Ogre头前后移动,我创建了一个Vector3型的属性动画。为了使Ogre头旋转,我创建了一个F32(float)型的动画。
  3. 为动画添加关键帧。

动画一旦创建出来后,就由时间轴管理。时间轴又由时间轴管理类管理。动画可以被设置为循环播放(上面的两个动画都被我设置为循环播放)和播放n次(n为大于0的整数)。我还可以通过调整时间轴的缩放来控制动画的速度。事实上,我只要每按一次U键,Ogre头前后移动的动画速度就会加快一倍,而自转速度则不会增加,因为它们处于不同的时间轴上。

由于动画的创建非常简单,因此可以非常容易地使用XML文件描述它,然后再让引擎加载XML动画文件来创建动画。只不过这一步我还没有做~

下面就讲讲我的思路吧

时间轴

首先是时间轴。GEA一书中提到了时间轴的概念。时间轴的好处在于把游戏的逻辑时间线和系统的硬件时间隔离了开来,我们甚至可以通过操控时间轴的缩放来调节游戏运行的速度。(玩过SC或红警这一类RTS游戏的同学都有游戏速度这个概念吧?)

我的时间轴模型是这样的:

有一个管理类管理着所有的时间轴,这个管理类为单例。我们看到名为Physics的时间轴的调用频率(30Hz)比名为Render的时间轴(60Hz)低,因此它得到调用的机会将更少。设置调用频率的好处在于,我们可以将某些实时性不高的时间轴的调用频率设低(例如我可以将AI时间轴的频率设成2Hz),而将宝贵的CPU时间让位于实时性高的时间轴(如渲染时间轴,我们将它设为60Hz)。

时间轴有优先级。优先级高的时间轴将被优先调用。如果多条时间轴存在依赖关系,这确保了它们可以按顺序被调用。

每个时间轴在被调用时,会从管理类处查询到上一帧到这一阵持续的时间(如果是60Hz,那么这个时间大概是16毫秒)。时间轴然后会把这个时间片传给挂载在该时间轴上的动画。要完成时间缩放其实非常简单,只需要将时间片乘以一个缩放因子就可以了。例如,我先将时间片乘以2,再传给下面的动画。那么对于挂载在这条时间轴上的动画来说,时间的流逝速度就加倍了。

时间轴上面可以挂载动画,同一个时间轴显然可以挂载许多动画。动画一定是在调用点被调用,但动画的结束位置不一定是调用点。

属性动画

属性动画就是通过不断改变目标物体的属性,以达成的动画效果。比如有一个矩形,我每帧将其width属性增加,那么这个矩形就会不断变宽。WPF中大量使用了这种属性动画。

属性动画的一大优点是,只要我对某种类型的属性定义了一个动画,那么所有拥有该类型属性的对象都可以运用这个动画。例如,我创建了一个名为scVector3Animation的属性动画,它作用于类型为Ogre::Vector3的属性。那么我就可以用这个动画去改变模型的位置、模型的缩放、模型的朝向、摄像机的位置等等一切拥有类型为Ogre::Vector3属性的物体。

为了要完成属性动画,我需要将以下三个事物绑定在一起

  1. 特定类型的属性动画,如scVector3Animation;
  2. 需要被动画作用的对象,如SceneNode* node;
  3. 需要被动画所改变的属性,如node的position属性。

为了完成这个任务,我需要用到C++中的lambda表达式。下面我将列出scVector3Animation类简化版本的代码,但足以用来说明问题。

#include <functional>
class scVector3Animation
{
public:
    typedef std::function<void (const Ogre::Vector3&)> Vector3Setter;
    explict scVector3Animation(Vector3Setter const& setter)
        : mSetter(setter)
    { } 

    void run(u32 time)
    { mSetter(Ogre::Vector3(0, 0, 0)); }

private:
    Vector3Setter mSetter;
}

scVector3Animation中有一个类型为Vector3Setter的成员变量,这其实是一个仿函数(函数对象)。它对应的函数签名是void (const Ogre::Vector3&),这正是SceneNode::setPosition()函数(以及其他类似的setter函数)的签名。

这个仿函数会在构造函数中传入,并作为成员变量保存。在run函数中,它将被调用。在上面这个例子中,run函数只是不断地给setter传入0, 0, 0参数,看起来毫无意义。但在真实情况下,被传入的参数将会是两个关键帧之间差值的结果。

然后,我们要这么做

void main()
{
    SceneNode* node;
    scVector3Animation ani( [node](const Ogre::Vector& pos){node->setPosition(pos);} );
}

我在这里使用了lambda表达式。这个lambda表达式创建了一个匿名的仿函数。这个仿函数对应的函数签名为void (const Ogre::Vector3&),正好与我们设定的Setter符合。这个仿函数在创建的时候会从外部捕获node对象,并将其作为自己的成员。在它的“函数体”中,这个仿函数将会调用node对象的setPosition()函数,将外部传入的参数设置为node的position属性。

于是,只要我们这样

ani->run(1234);

参见上上面的代码块,run函数会调用仿函数(mSetter)的函数体,仿函数(mSetter)又会调用node对象的setPosition()函数。在上面的例子中,它把node的位置设为了0, 0, 0。至此,我们已经实现了对象、属性和动画三者的绑定。

关键帧动画

我们使用关键帧来控制动画的进程,任意两帧的关键帧之间的动画通过插值来完成。像Ogre::Vector3,float这样的类型都是可插值的。然而,有些类型,如bool、string,都是不可插值的。事实上,对于不可插值的动画,我们只需要使用前一关键帧的值就可以了。

使用关键帧的一个好处在于,我们只需要定义关键帧就行了,中间的过程都交给插值去做,因此非常适合使用XML来定义。

另一个好处在于,对于任意两帧之间的插值,我们可以任意选择插值算法。一般来说,我们只要用到5个参数,分别是

  1. 初始时间 t0
  2. 结束时间 t1
  3. 初始值 val0
  4. 结束值 val1
  5. 当前时间 t

使用这些参数作为接口,我们甚至可以让前两帧做线性插值,后两帧做二次插值,非常的方便。


由于只是对属性(数值)进行动画,所以与对象类型无关。这样的一套动画系统,既可以用在Ogre上,也可以用在其它的组件如MyGUI上。同时,时间轴不仅仅用来处理动画,还可以用来处理逻辑。

接下来我会修整修整,让实现变得更优雅一些。

由于entity和camera性质对立导致的封装性问题

昨天写了一天java终于把工作做完,从图书馆回到宿舍累的倒头就睡,从晚上八点睡到第二天早上10点。一想到再也不用写java真是神清气爽,爬起来继续写Camera类。Camera可以挂载在场景节点上,因此需要通过父节点的位置信息来更新自己的状态。毫无问题!我记得我的Movable类是拥有一个成员叫做mParentNode的。可是当我翻过去看的时候,却惊讶的发现这个成员已经被我自己注释掉了!

怎么回事呢?在我原来的设计中一个Movable只能属于一个SceneNode,但后来当我写到Entity的时候,我又希望一个Entity可以被多个SceneNode重用。因为SceneNode仅仅起到了一个计算位置的作用,所以只要在加入渲染列表的时候将自己的位置传给自己拥有的MovableObject就可以了。

Entity通过传入的节点来更新自身的位置信息,同时为了重用,Entity要将自身Renderable的部分复制一份。由于Entity同时派生自Movable和Renderable,因此这里会发生截断,不过这个截断正是我所希望发生的。

这样,一个Entity被创建出来以后,就可以同时被不同的SceneNode所有,因此在Entity上保存一个父节点指针是没意义的。然而悲剧的是,同样派生自Movable的Camera却需要一个这样的父节点指针,而且Camera并不会在同一时刻被挂到不同的场景节点上。同样可以预见到的另一个悲剧是Light,它和Camera有类似的性质。

遇到这种情况第一时间的反应当然是去看看Ogre是怎么解决这个问题的啦。嘿嘿,Ogre的方式真是简单粗暴:Ogre中,一个Entity不能被多个SceneNode重用。嘛~和我最初的想法蛮一致的嘛。那么如果玩家有一门速射机炮,每秒钟发射60发炮弹,你说该怎么办?Ogre说,嗯,这种情况下你应该调用第一个炮弹Entity的Clone()函数,来复制出剩下的59个。噢噢!真是简单的方法,我怎么没有想到?然而为什么我总感觉有些不对?擦!每个Entity都需要有一个独一无二的名字,因此我需要手工为每个复制出来的炮弹命名?!Ogre说,你个蠢货,Ogre不是会为没有人工指定名字的Entity自动生成一个独一无二的名字吗?我:T_T

于是我决定坚持我自己的方法,这回不听Ogre的!为什么呢,想想看,为了一颗小小的炮弹,居然每秒要生成60个字符串,真是蛋疼……好吧,其实是我实在不想写那个自动生成名字的类。那么我要怎么做呢?完全恢复Movable类里面的mParentNode是不可以的,因为将来可能会有人尝试使用Entity来获取自身的“父节点”。所以我决定部分恢复。

首先,将mParentNode变成protected成员(在saber core中我很少使用protected,极有可能这个会是第一个。即使是在继承关系中,子类也是通过inline的get/set函数来访问父类成员)。在Movable中,仅声明mParentNode的set方法。当Movable被Attach到某个SceneNode上面时,这个SceneNode会调用set方法把自己设为该Movable的父节点。然后在需要使用父节点的子类中,比如Camera或者是Light,我才声明get方法。由于Entity并没有get方法,所以就无法获取Entity的“父节点”了~

说实话这样做让我感觉很不舒服,不过先就这样吧……

ResourceManager,模板,和面向对象

昨天新写了mesh资源和mesh资源的管理类。为了减少重复劳动,我在TextureManager和MeshManager中提取了共用的部分,为他们写了个父类ResourceManager,也就是把之前计划中但未完成的部分给完成了。然而让我没有想到的是,当我写完ResourceManager后,却发现TextureManager和MeshManager只剩下一个由构造函数和解构函数组成的空壳。不过当时写得比较累,也没太多想。

当时我在蛋疼一个问题,那就是每个ResourceManager中都必须维护一个Resource的map。比如TextureManager就是维护Texture,MeshManager就是维护Mesh,而Texture和Mesh都是Resource的子类。如果我在ResourceManager的map中存放的是Resource,这样毫无疑问会发生类型截断。然而我又不想再Map中存放指针,因为这样就需要我手动在每个ResourceManager的解构函数中遍历Map并释放每个指针所指资源,如果哪天我忘记了那就发生内存泄露了。

我想到的解决方法是将ResourceManager声明为一个模板类,这样就可以很好地解决问题。这里有个小插曲,我写的模板类语法什么都对,但是却发生了一堆link error。感谢亮哥的指导!原来模板类无法像普通类一样分别写在.h和.cpp中,必须要处于同一个文件,这事情我现在才知道。

模板化的ResourceManager工作得很好,此时我也有心情来审视那两个只剩下空壳的类。它们长的大概是这个样子的:

#include "scResourceManager.h"
#include "scTexture.h"

class scTextureManager : public scResourceManager<scTexture>
{
public:
scTextureManager();
~scTextureManager();
};

我在凝视了它长达十分钟后,怀着复杂的心情把它改成了下面这个样子

#include "scResourceManager.h"
#include "scTexture.h"

typedef scResourceManager scTextureManager<scTexture>;

说是不适应也好,或者说是感受到了其中的魅力也好。因为我突然意识到,scTexture或scMesh,就算不是继承自scResource而是各自独立的个体,只要他们的接口符合scResourceManager中的描述,它们就可以填到那个尖括号里面去。于是scResource的其中一个功能“以父类的指针操控子类”,也就是运行期多态,失去了作用,取而代之的是模板提供的编译期多态。

“C++是由四种语言范式组成的语言联邦”,现在我对这句话有了更深入的了解。

STL中deque和list的效率比较

我在做项目的时候经常会遇到一些情况,是需要用到队列的。比如,我要维护消息队列,或者动画队列。STL中有两种容器胜任此项工作,它们分别是list和deque。list是基于链表实现的,而deque是基于动态数组实现的。《C++标准程序库》一书中介绍了两者的差别,但并没有比较两者的效率。由于今后我会经常地使用到这两种结构,因此我对两者的效率进行了一下测试。

测试的类型fuck定义如下:

struct fuck
{
int a,b,c;
}

测试平台为win7,环境为vc2010,stl版本为P.J. Plauger。

测试1:容器大小为十万个fuck,循环进行pop_back()和push_front()操作

int main()
{
int len = 100000;
list<fuck> l(len);
deque<fuck> d(len);

clock_t begin = clock();
for (int i=0; i {
d.push_front(d.back());
d.pop_back();
}
clock_t end = clock();
cout << end – begin << endl;

begin = clock();
for (int i=0; i {
l.push_front(l.back());
l.pop_back();
}
end = clock();
cout << end – begin << endl;
system(“pause”);
return 0;
}
测试结果:(毫秒)
deque   312    308    307    312
list        1046  1044  1043  1044

测试2:容器大小为十万个fuck,先使用pop_back清空,然后使用push_front插入十万个新的fuck

int main()
{
int len = 100000;
list<fuck> l(len);
deque<fuck> d(len);

clock_t begin = clock();
for (int i=0; i d.pop_back();
for (int i=0; i d.push_front(fuck());
clock_t end = clock();

begin = clock();
for (int i=0; i l.pop_back();
for (int i=0; i l.push_front(fuck());
end = clock();

return 0;
}
测试结果:(毫秒)
deque   97         96  
list        11756   11250 

这不科学……我也懒得再进行下去了

测试3:容器为空,然后使用push_front插入十万个新的fuck

int main()
{
int len = 100000;
list<fuck> l;
deque<fuck> d;

clock_t begin = clock();
for (int i=0; i<len; i++)
d.push_front(fuck());
clock_t end = clock();
cout << end – begin << endl;

begin = clock();
for (int i=0; i<len; i++)
l.push_front(fuck());
end = clock();
cout << end – begin << endl;
system(“pause”);
return 0;
}
测试结果:(毫秒)
deque   628    632    625    618
list         383    377    375    379

测试4:容器大小为十万个fuck,使用pop_back清空

int main()
{
int len = 100000;
list<fuck> l(len);
deque<fuck> d(len);

clock_t begin = clock();
for (int i=0; i<len; i++)
d.pop_back();
clock_t end = clock();
cout << end – begin << endl;

begin = clock();
for (int i=0; i<len; i++)
l.pop_back();
end = clock();
cout << end – begin << endl;
system(“pause”);
return 0;
}
测试结果:(毫秒)
deque   34         34  
list        10241   10326

事实证明,大规模的pop操作会使list慢得像吃屎,这让我很不解…… 当参照测试1的结果,我就更加不解了。希望有高手能够帮忙解答。

C++的默认拷贝构造函数(Copy Constructor)

何时我们该使用C++编译器提供的默认拷贝构造函数,何时该自己重写?

有一个Class X如下:

class X
{
public:
string _str;
X(string str) : _str(str) { } 
}

它的拷贝构造函数应该是长成这样子的:

X::X(const X& x);

可是这里我们并没有手动帮X类写出拷贝构造函数,所以编译器会在认为有必要的时候以inline的形式创建一份编译器版本的拷贝构造函数。它的样子应该是这样的:

X::X(const X& x)
{
this.str = x.str;
// 如果x类还有更多的成员
// 那么拷贝函数会依次拷贝它们
// 像这样:
// this.a = x.a;
// this.b = x.b;
// ...
}

并且,由于我们并没有手动重载赋值号(=),所以编译器会自动生成一份=的重载,实际上就是调用copy constructor。下面分情况研究一下。

void main()
{
X *a = new X("a");
X *b = a;
X c("c");
X d = c;
}

由于a和b都是指针,所以把a的值赋予b并不会调用X的copy constructor,没有任何复制操作发生,b现在仅仅把指针指向了a所指的位置。
但是对于c和d来说,就发生了复制操作。如果我们分别打印出&(c._str)和&(d._str),会发现他们确实指向了不同的位置。

但是考虑一下的一种情况,让我们修改一下X类:

class X
{
public:
string *_str;
X(string str) : _str(&string(str)) { } 
}

注意到X类中的成员已经变成了指针。那么当我们这样做时,会发生什么事情呢?

void main()
{
X *a = new X("a");
X *b = a;
}

答案是,复制过程确实发生了,但是由于类成员是指针,所以仅仅发生了指针的复制。也就是说,如果我们分别打印出&(c._str)和&(d._str),会发现他们指向了不同的位置。但当打印出a._str和b._str时,会发现它们其实是一样的。

分别是&(c._str),&(d._str),a._str,b._str

此时,问题的答案已经很明显了。当类中存在指针变量,而我们又想对这些指针所指的地方进行复制时,默认拷贝构造函数是无能为力的,可以考虑自己重写。