Posts tagged Ogre
Hermite Curve 在绘制轨迹中的应用
0
在3D游戏的粒子系统中,往往可能有这样一类需求——伴随着人的动作,一条光带划过,或者伴随着武器的劈砍动作,一条刀光划过。
如果我们简单的在每次Tick的时候采样当前的位置朝向,并创建一截新的面片,往往会形成类似于如下图的粗陋效果:
之所以会产生一截一截的感觉,是由于在每次进行采样的时候,我们只能得到当前时刻的位置信息。因此尽管在从上一次到本次采样的过程中,轨迹实际经过的路程是一条平滑的曲线,可由于每一帧之间的时间不可能小到足够提供平滑曲线的采样精度,我们所能获得的总是一截截的折线。
怎样才能够通过为数不多的关键点,创造出一条平滑的曲线来呢?
Charles Hermite 提出的Hermite Curve解决了上面的问题。通过一系列的三维空间关键点,可以得到每个点处的切线,再经过Hermite Base Function的多项式计算,则可以得到插值点。
如图:
设P1,P2为平面上的两个点,T1,T2为两点处的切线,由点及点的切线,Hermite Spline就可以给出一个平滑过渡的曲线。
Hermite Base 多项式的参数如下表,其中t是从上一个点到当前点的插值系数。上一个点处为0,当前点为1。
Ogre在其Ogre::SimpleSpline类中实现了HermiteCurve。感兴趣的同学可以做以参考。
| expanded | factorized | |
| h00(t) | t3 − 3t2 + 1 | 1 + 2t)(1 − t)2 |
| h10(t) | t3 − 2t2 + t | t(1 − t)2 |
| h01(t) | − 2t3 + 3t2 | t2(3 − 2t) |
| h11(t) | t3 − t2 | t2(t − 1) |
事实上,上述做法也不过就是对Hermite Curve类型的样条曲线的利用而已。样条曲线实际上并不陌生,贝塞尔曲线就是一种我们关注最多的样条曲线。尽管其不适用于我们前述的场合——原因是贝塞尔曲线通过控制点计算出的曲线并非一条依次经过控制点的曲线。如下图所示(图片来源于:http://escience.anu.edu.au/lecture/cg/Spline/printCG.en.html):
参考资料:
1. Hermite Curve Interpolation. Hamburg (Germany), the 30th March 1998
Ogre源码剖析3–可扩展性&插件机制
0Ogre是一个跨操作系统平台的开源3D引擎,既支持DirectX,也支持使用OpenGL,支持可替换的场景管理算法(BSP, OCT)。为Ogre提供这些灵活可扩展性的基础之一就是其面向插件的设计。
很多常用的软件大都提供了插件接口,用以扩展应用程序设计者最初未想到的功能,比较常见的譬如PhotoShop的滤镜,After Effect中的各种插件(最有名的比如shine),3dMax的插件譬如渲染器,魔兽世界的辅助插件等等。
通常,插件本身通常也需要实现主应用程序所需要的必要接口,从而使得插件可以被应用程序加载执行。此外,插件的实现也需要由主应用程序提供一些接口api,通过这些接口,插件可以对主应用程序的功能进行调用。
插件可以是动态链接库(win32平台上为DLL文件),也可以是以脚本的形式提供的,比如魔兽世界中的插件就是使用lua编写的,插件也可能是某种应用程序自定义的文件,只要该应用程序提供了创建该类文件的方法并实现解析、执行功能即可。(不同的实现形式各有利弊,具体需要参考插件及应用程序所处的运行环境进行取舍)
采用插件的一个巨大的好处,以及很多应用程序中使用插件的主要目的就是,我们可以在不需要改动应用程序本身的情况下扩展应用程序的功能。
在Ogre中,插件被用来提供渲染子系统(RenderSystem),不同类别的图形API被封装在不同的渲染子系统的视线当中,Ogre默认提供了DX和OpenGL的实现,甚至,如果我们乐意,甚至可以只用绘点函数实现一套纯软件的渲染子系统提供给Ogre使用。以此为例,用简单的形式来展示这种实现大概类似于下面这样:
class RenderSystem
{
// … operations that a render system need to support
};
// In DX_RenderSystem.dll (in plugin dx rendersystem )
class DX_RenderSystem : public RenderSystem
{
// … implementation & override of the operations from RenderSystem using DirectX
};
// In GL_RenderSystem.dll (in plugin openGL rendersystem)
class GL_RenderSystem : public RenderSystem
{
// … implementation & override of the operations from RenderSystem using OpenGL
};
// …. We could implement any other rendersystem as we like
在引擎的内部(OgreMain),插件在初始化的时候,将一个渲染子系统的实例创建出来(可能是DX_RenderSystem,也可能是GL_RenderSystem,也可能是其他),并将之挂接到OgreMain的Root对象当中。此后,OgreMain中的Root所有的渲染操作也就可以通过该接口访问插件中创建的渲染子系统了。
本文接下来分析Ogre是如何实现插件的,以及插件是如何与OgreMain主引擎进行配合的。
事实上,想编写一个插件是很简单的,我们只需要约定几个接口,插件将这些接口实现了,主应用程序通过某种机制(配置文件/遍历某个目录)加载插件,并查找接口在插件中是否被实现了,如果实现了,则调用之即可。
一个简单的例子如下:
// in xx.dll
__declspec(dllexport) extern “C” // __declspec(dllexport)告知编译器需要将该函数导出
// extern “C” 告知编译器不要对函数做C++名字重整
const char* GetPluginName(void)
{
return “Test Plugin”;
}
// in application
HMODULE hInst = LoadLibrary(“xx.dll”);
if (!hInst) return;
typedef const char* (*GetPluginNameFunc)(void);
GetPluginNameFunc pFunc = GetProcAddress(hInst, “GetPluginName”);
if (!pFunc) return;
const char* szPluginName = pFunc();
// … do other things
事实上,在C语言里,我们就可以通过这种方式,约定一套需要实现的接口,交给插件实现即可。在这里,插件的功能都是在dll中实现的,我们需要加载dll,保存句柄,并在插件卸载的时候释放相应的资源。在C++中,我们显然可以做的更好一点,比如将dll模块相关的功能以及资源封装起来:
class DynLib
{
public:
bool Load(const char* szPluginPath);
void* GetProcAddress(const char* szProcName);
private:
HMODULE m_hInst;
};
bool DynLib::Load(const char* szPluginPath)
{
m_hInst = LoadLibrary(szPluginPath);
return m_hInst != NULL;
}
void* DynLib::GetProcAddress(const char* szProcName)
{
return ::GetProcAddress(m_hInst, szProcName);
}
这样一来,我们就有了一个DLL的简单封装。
但是一个dll里未必只能有一个插件,假若我们简单的把插件接口设计为一套C导出函数,我们就不得不面对这种局限。经典的做法依旧是抽象,Ogre中定义了一个插件接口,每一个实现了插件接口的实例都代表了一个插件,如下:
class Plugin
{
public:
virtual const String& getName() = 0;
virtual void install() = 0;
virtual void initialize() = 0;
virtual void shutdown() = 0;
virtual void uninstall() = 0;
};
Ogre的每一个插件都需要实现以上接口。该接口提供的功能包括:
install 插件被加载时调用。
initialize 插件被初始化时调用。
shutdown 插件被反初始化时调用。
uninstall 插件被卸载时调用。
不过,单有这个接口,插件还是无法工作。插件中还需要提供两个约定的C导出函数。这个稍后再说。
Ogre在Root对象中统一管理插件,因此Root对象提供了以下与插件相关的接口:
class Root
{
// …
void loadPlugin(const String& pluginName);
void unloadPlugin(const String& pluginName);
void installPlugin(Plugin* plugin);
void uninstallPlugin(Plugin* plugin);
// 以及对应的处理多个插件的接口
// …
};
当loadPlugin函数被调用时,Ogre将加载该插件的dll,从中查找并调用名为dllStartPlugin的导出函数。
对应的,当unloadPlugin函数被调用时,Ogre将从该dll中调用dllStopPlugin函数。并将该dll卸载。
每个插件在实现的时候,都必须提供上述的两个C导出函数。其中,dllStartPlugin负责创建插件实例,并调用Root::installPlugin,将创建好的插件指针传递给Root对象。在这里,dllStartPlugin实际可以创建多个插件实例,并依次调用Root::installPlugin,这样我们就可以在一个DLL中包含多个插件了。在dllStopPlugin时,则需要调用Root::uninstallPlugin,并将插件DLL中创建的plugin实例释放掉。
一个典型的dllStartPlugin的实现像这样:
class D3D9Plugin : public Plugin
{
//…
};
D3D9Plugin* plugin;
__declspec(dllexport) extern “C”
void dllStrartPlugin(void)
{
plugin = new D3D9Plugin();
Root::getSingleton().installPlugin(plugin);
}
__declspec(dllexport) extern “C”
void dllStopPlugin(void)
{
Root::getSingleton().uninstallPlugin(plugin);
delete plugin;
}
Root::getSingleton().installPlugin(plugin)会负责调用插件的install以及initialise操作,并将插件的指针存放起来,以备卸载时使用。
Plugin::install以及Plugin::initialise则分别负责创建OgreMain提供扩展功能接口的实例,以及将创建好的对象挂载到应用程序当中。
一个典型的Plugin的行为像这样:
void BspSceneManagerPlugin::install()
{
mBspFactory = new BspSceneManagerFactory();
}
void BspSceneManagerPlugin::initialise()
{
Root::getSingleton().addSceneManagerFactory(mBspFactory);
}
其中,BspSceneManagerFactory是继承自OgreMain中的SceneManagerFactory的派生类。
( class BspSceneManagerFactory : public SceneManagerFactory {…} )
通过将场景管理器工厂添加到Root对象当中,插件动态的将其功能添加到了OgreMain当中。
PS: SceneManagerFactory是一个用于创建SceneManager的工厂。SceneManager则是被用于场景管理的管理器。具体的SceneManager也根据算法不同而不同,Ogre自带提供了两种类型的场景管理算法插件:Bsp以及Octree(实现了两个对应的插件,分别是Plugin_BSPSceneManager以及Plugin_OctreeSceneManager)。
通过上述方式,OgreMain核心引擎并不需要关心场景管理算法的具体实现,不需要关心渲染子系统的具体实现,不需要关心粒子系统的具体实现,等等。一切这些扩展性的功能都通过插件实现,并在加载时动态挂载到OgreMain当中,供OgreMain的引擎核心使用。
使用插件时几个需要注意的地方:
1, 调用插件DLL方法创建的对象需要交由插件DLL释放。(因为不同的链接单元可能具有不同的内存管理上下文环境,此处的new与彼处的new在语义上未必等同)
2, 调用插件DLL方法获取的插件内对象的引用或指针,在插件DLL卸载之后就是无效的,必须保证不再使用。(比较容易引发问题的一个典型例子是从插件中传递回一个引用计数字符串,当DLL被卸载后,字符串内指向实际数据的指针已经无效,但是在该对象析构时,仍需要访问该指针)
Singleton#1——关于单件对象初始化的探讨
0凡是使用C++进行开发的人,大都或是了解,或是直接使用过Singleton模式,但是Singleton的多种实现方式有什么差异?不同的实现细节背后究竟蕴含着什么意义?本文试图列举常见的几种不同的Singleton实现方式,考察这些不同的实现方式中的细节差异,并剖析其好处与缺点,试图对Singleton的实现方式做一个小结。
我们在使用C++编写实际项目的时候,往往会有对全局对象/变量的访问需求。对全局变量的不加封装的访问,会导致许多麻烦的问题:譬如在调试中,由于对全局变量的修改被分散在程序的各个角落里,使得我们无法准确判断一个错误是在何时发生的;再比如,在多线程环境下,如果多个线程都需要访问一个变量,则需要做线程同步操作,在没有良好的封装的情况下,这种状况可能会导致完全莫名的coredump。不过本文并不是想说Singleton有什么好处或者有什么作用,仅仅只是想总结Singleton的不同实现方式之间的异同,以及适用场合。
我第一次接触到Singleton模式,是在《Game programming all in one》一书中,该书的示例源代码中采用了以下这种Singleton的实现方式:
class A
{
public:
A()
{
ASSERT(!ms_Singleton);
ms_Singleton = this;
}
static A* GetSingleton() { return ms_Singleton; }
protected:
static A* ms_Singleton;
};
A* A::ms_Singleton = 0;
代码段-1
这种Singleton在使用的时候,需要由使用者进行初始化。并且,如果A类在程序中尚未被实例化之前,其他代码在调用A::GetSingleton的时候,会得到一个空指针。此外,在必要的时候我们可能还需要对类A实例进行显式的释放。
针对Singleton初始化时机的问题,有一种RAII(Resource Acquire Is Initialization)的技术可以保障无论何时,只要我们调用A::GetSingleton,就必然会获取一个有效的A实例指针。
对上述代码段-1我们只需要做少许改动,即可构建一个RAII的Singleton:
class A {
public:
static A* GetSingleton()
{
if (!ms_Singleton)
ms_Singleton = new A;
return ms_Singleton;
}
protected:
A() {}
static A* ms_Singleton;
};
A* A::ms_Singleton = 0;
代码段-2
上述做法相比代码段-1中所展示的做法有一个好处:无论何时我们调用A::GetSingleton,我们都能获得一个有效的指针。而且类A的实例化被类A自身管理,外界不再需要显式构造一个A的实例,与此同时,我们将A的构造函数的访问限制符设置为protected,以防止外界对A的显式构造。
但是,代码段-2中这种做法也存在一个问题,当外界代码不再需要显式构造A的实例时,显式的delete A::ms_Singleton指针的做法非常难堪。虽然我们可以把释放操作放在A的另一个static方法中,但这实际上又是要求我们在程序中的某个地方去显式的调用这一释放函数,与显式的释放内存无异。
上述问题的实质是内存管理问题,解决问题的思路其实也可以从内存管理的角度出发。我们知道程序中的内存分配方式有以下几种:数据段,堆,栈。
代码段-2中的RAII的实现是将A实例化在堆内存中。栈因为其自身特性(仅适合于局部变量的特性),并不适用于Singleton对象的存放需求,那么数据段是否可行呢?
下面一种实现,使用类作用域静态对象,将A的实例存储于数据段上,因为没有将数据置放于堆内存中,因此可以回避上述实现中遇到的内存释放的问题。
class A
{
public:
A() {}
static A* GetSingleton() { return &s_Instance; }
protected:
static A s_Instance;
};
A A::s_Instance;
代码段-3
这种做法是将单件实例用一个全局静态对象保存。全局静态对象会在程序入口点之前被构造。这带来一个好处:我们可以确保程序入口点之后的任何代码访问到该单件对象时,它都已经被构造完毕了。但这种实现方式同时这也带来一个问题:两个全局静态对象的初始化如果存在依赖关系,我们无从确定哪一个会先被构造。
用一个例子来说明,如果我们无法确信两个静态对象中的哪一个会被先初始化,那么我们可能会遇到什么问题:
class MySingleton1
{
public:
MySingleton1() : m_bIsInited(true) { }
static MySingleton1* getSingleton() { return &s_Instance; }
bool IsInited() { return m_bIsInited; }
private:
static MySingleton1 s_Instance;
bool m_bIsInited;
};
MySingleton1 MySingleton1::s_Instance;
class MySingleton2
{
public:
MySingleton2() { m_strBasePath = MySingleton1::getSingleton()->IsInited() ? “C:\” : “”; }
static MySingleton2* getSingleton() { return &s_Instance; }
private:
static MySingleton2 s_Instance;
std::string m_strBasePath;
};
MySingleton2 MySingleton2::s_Instance;
代码段-4
上述代码是一个简单的例子,MySingleton2依赖MySingleton1中的一个状态。以此决定自身初始化的结果。这个例子是虚构的,但是在实际项目中,如果我们采用类似的方法来初始化配置文件以及相关对象的话,那么也同样会遇到类似的问题。MySingleton2必须要在MySingleton1之后被构造,我们如何保障这一点呢?
显然类作用域或全局作用域的静态对象无法为我们提供这种保障。
既然类作用域的静态对象无法为多个Singleton对象相互依赖的情况提供支持,那么函数作用域的方法如何呢?
下面这种Singleton的实现方法,我是从云风老大的《游戏之旅——我的编程感悟》中看到的。对代码段-3的做法也只需要做小小的改动即可:
class A
{
protected:
A() {}
public:
static A* GetSingleton();
};
A* A::GetSingleton()
{
static A _instance;
return &_instance;
}
代码段-5
在GetSingleton()函数中,我们定义了一个函数作用域的静态实例A _instance。由于静态数据会由编译器自动为其在数据段中留存空间,因而我们同样不需要显式的为其分配内存(这里的“显式”是指调用new operator将A实例构造于堆空间中)。而且,前面的依赖问题,在这里不复存在了。由于静态对象被定义在函数作用域当中,因此,该对象会在GetSingleton被第一次访问时构造出来,因此类似于代码段-4中的情况,在这里就不再成为问题了。当MySingleton2对象被构造时,会显式调用MySingleton1::getSingleton函数。此时,如果MySingleton1已经被构造,则直接返回地址,若没有被构造,则会先调用MySingleton1的构造函数,再将其对象地址返回。无论如何,MySingleton2对MySingleton1的依赖都会被正确处理。
为了证明这一点,可以看一下下面这个程序的运行结果:
运行环境是WinXPSP2,编译环境是VC2005。
#include <iostream>
#define CTOR_MSG(className)
cout<<#className<<” Constructed”<<endl
#define DTOR_MSG(className)
cout<<#className<<” Destructed”<<endl
using namespace std;
class Singleton_Impl1 {
protected:
Singleton_Impl1()
: m_bSomeConf(true)
{ CTOR_MSG(Singleton_Impl1); }
public:
~Singleton_Impl1()
{ DTOR_MSG(Singleton_Impl1); }
bool GetSomeConfiguration() { return m_bSomeConf; }
static Singleton_Impl1* getInstance();
private:
bool m_bSomeConf;
};
Singleton_Impl1* Singleton_Impl1::getInstance()
{
static Singleton_Impl1 _instance;
return &_instance;
}
class Singleton_RelyOnImpl1
{
protected:
Singleton_RelyOnImpl1()
{
CTOR_MSG(Singleton_RelyOnImpl1);
if (Singleton_Impl1::getInstance()->GetSomeConfiguration())
cout<<”Do the initialization as Singleton_Impl1′s configuration is true”<<endl;
else
cout<<”Do the initialization as Singleton_Impl1′s configuration is false”<<endl;
}
public:
~Singleton_RelyOnImpl1()
{ DTOR_MSG(Singleton_RelyOnImpl1); }
static Singleton_RelyOnImpl1* getInstance();
};
Singleton_RelyOnImpl1* Singleton_RelyOnImpl1::getInstance()
{
static Singleton_RelyOnImpl1 _instance;
return &_instance;
}
int _tmain(int argc, _TCHAR* argv[])
{
cout<<”Enter Main”<<endl;
Singleton_RelyOnImpl1::getInstance();
cout<<”Leave Main”<<endl;
system(“pause”);
return 0;
}
运行结果如下:
Enter Main
Singleton_RelyOnImpl1 Constructed
Singleton_Impl1 Constructed
Do the initialization as Singleton_Impl1′s configuration is true
Leave Main
可以看到,函数作用域的静态对象在该函数被第一次调用时初始化,并且初始化依赖被正确的处理了。
上面的实现中,当我们每次使用Singleton的时候,都不可避免的要定义一个静态对象或者静态对象指针,以及一个静态类成员函数用以获取单件对象的引用或指针。有没有办法把这些重复的编码消除掉呢?
我们首先想到的就是继承:C++中的继承可以将一些共有的操作和成员放置于基类当中,派生类继承自基类之后,就自然拥有了基类已有的成员函数和成员变量。像这样:
class SingletonBase
{
public:
SingletonBase* getSingleton();
};
class MySingleton1 : public SingletonBase
{
};
现在MySingleton1就继承了来自SingletonBase中的getSingleton()函数。但是,讨厌的事情发生了,我们在使用MySingleton1::getSingleton()时,得到的是一个SingletonBase*的指针。这是件麻烦事,我们不想在获取了指针之后再写讨厌的转型代码,我们希望getSingleton直接返回一个符合我们想要的类型的指针。(MySingleton1::getSingleton()直接返回一个MySingleton1*)。考虑这个需求,我们想到的是:或许可以重载?然而这个念头转眼就被否决了,因为C++无法根据返回值类型进行重载决议。何况,如果我们需要在MySingleton1中写重载函数,那为什么还需要SingletonBase呢?本来在我们的期望中,SingletonBase应该将这些讨厌的事都做了的,我们需要的只是派生一个,然后拿着就用就好~
幸好我们有模板。C++强大的模板功能,为我们提供了这样的可能(以下实现方案的思想来自于Ogre::Singleton的实现,但在具体细节上做了简化。关于Ogre的其他方面的分析文章可以参看这里)。
如果我们将SingletonBase设计为一个模板类,那么上述的头痛问题就迎刃而解了。
template<typename T>
class SingletonBase
{
public:
SingletonBase() { s_pInstance = static_cast<T*>(this); }
~SingletonBase() { assert(s_pInstance); s_pInstance = 0; }
static T* getSingleton() { return s_pInstance; }
protected:
static T* s_pInstance;
};
class MySingleton : public SingletonBase<MySingleton>
{
};
这样一来,我们的MySingleton就立刻拥有了一个static MySingleton* s_pInstance的声明,以及一个static MySIngleton* getSingleton()的定义。
MySingleton的实现文件中还需要给出s_pInstance指针的定义,如下:
template<> MySingleton* SingletonBase<MySingleton>::s_pInstance = NULL;
对于指针s_pInstance的初始化,我们不必操心,因为它在SingletonBase的构造函数中被赋予this的地址。因此只需要构造完成一个MySingleton对象,在其基类部分构造完成时,s_pInstance就已经指向MySingleton的首地址了。(由于早期的编译器在对象内存布局上的一些不符合C++标准的细节,因此在实做中,Ogre在赋值的部分做了一些预编译处理)。
不过这种做法我们虽然摆脱了每次写重复代码的状况,却陷入了必须显式初始化每一个单件对象的境地。
Ogre中是在root对象里对所有的Manager对象(Ogre引擎中的单件对象)进行初始化的。对这类Singleton对象的初始化也非常简单,只需一行代码如下即可:
MySingleton* pMySingleton = new MySingleton;
这样做的好处在于:我们可以完全显式的控制所有的Singleton对象的初始化顺序,而不是像代码段5中的做法一样,将初始化顺序交付给其他代码对Singleton对象的getSingleton函数的调用顺序。
实际上这种做法的本质与代码段1中的做法极其类似,而好处则是:我们不必再写重复代码了。
Singleton的实现有许多种,可能存在许多别的实现方式是本文中没有提及的,针对不同的实现方式,我们应当在实际使用中,根据实际环境以及项目需求,采取合适的做法。
最后,本文也没有涉及在多线程环境下,对Singleton对象的同步问题的说明。事实上,如果Singleton对象被应用在多线程环境中,并应用于跨线程的资源管理,这方面的问题是必须考虑的。
附加:推荐一个wiki链接:http://en.wikipedia.org/wiki/Singleton_pattern
这里对Singleton在各个不同语言环境中的使用做了一个总体概览。其后的Reference等延伸阅读也很有价值。
Ogre源码剖析1:任意类型类Any
3有些时候我们可能想做这样一件事:
float f = 1.f;
int n = 2;
std::vector<X> myContainer; // X是一个虚构的用户定义类型
myContainer.pushback(X(f));
myContainer.pushback(X(n));
我们想在一个容器里保存两种乃至多种不同的数据类型。
但是,显然普通的模板参数如std::vector<int>,或者std::vector<float>都无法满足我们的需求。
也可能存在下面这样的情况:
float fVal = 1.f;
X xMsg = fVal;
PushMessage(xMsg); // PushMessage发送了一个异步消息
// 另一个地方(可能是另一个线程),回调函数被调用:
OnMessageXXX(X xMsg)
{
float f = static_cast<float>(xMsg);
// do something with value f
}
针对这种需求,我们想要一种类型,它可以接受任意的类型,并在需要的时候把我们放入的真正的类型取出来,它可以被放置到容器中,可以被拷贝,以及串行化(前提要求被置入其中的类型可以被输出到标准输出流–std::ostream当中)。
如何设计这种类型呢?
一种容易想到的方案如下:
class Any {
public:
enum {
ANYTYPE_INT,
ANYTYPE_FLOAT,
ANYTYPE_LONG,
// … other data types
};
union {
int nIntData;
float fFloatData;
long lLongData;
// … other data types
};
Any(int nData) : nIntData(nData), m_type(ANYTYPE_INT) {}
Any(long lData) : lLongData(lData), m_type(ANYTYPE_LONG) {}
Any(float fData) : fFloatData(fData), m_type(ANYTYPE_FLOAT) {}
// … other constructors
};
现在我们可以这样写:
Int nData = 3;
Any myVal(nData);
为了让Any可以被赋值,我们需要给Any提供一个operator =。
Any& Any::operator = (const Any& rhs)
{
if (&rhs == this)
return *this;
m_type = rhs.m_type;
switch(rhs.m_type)
{
case ANYTYPE_INT:
nIntData = rhs.nIntData;
break;
case ANYTYPE_LONG:
lLongData = rhs.lLongData;
break;
case ANYTYPE_FLOAT:
fFloatData = rhs.fFloatData;
break;
default:
ASSERT(0);
};
}
这样Any就有了相互之间被赋值的能力了,如下:
Any myVal1(1);
Any myVal2(5.6f);
myVal1 = myVal2;
然而我们还希望Any可以直接用各种数据类型赋值。因此,针对需要支持的每一种数据类型,都应该重载一个operator =。如下:
Any& operator = (int inData)
{
nIntData = inData;
m_type = ANYTYPE_INT;
return *this;
}
Any& operator = (long inData)
{
lLongData = inData;
m_type = ANYTYPE_LONG;
return *this;
}
等等。
当然如果嫌这种写法过于冗长,出于节省代码的考虑,我们可以使用一个宏来代替:
#define OPEQUAL(type, typeID, varName)
Any& operator = (type nData)
{
varName = paramName;
m_type = typeID;
return *this;
}
这样就可以将上述代码转换成:
OPEQUAL(int, ANYTYPE_INT, nIntData);
OPEQUAL(long, ANYTYPE_LONG, lLongData);
OPEQUAL(float, ANYTYPE_FLOAT, fFloatData);
此外,作为一种数据的承载,我们还希望在需要的时候把实际的数据取出来,因此我们需要重载一系列获取函数:
operator int() const { return nIntData; }
operator long() const { return lLongData; }
operator float() const { return fFloatData; }
有了这些类型operator之后,当需要从Any中取出我们想要的数据时,即可以通过:
Any myAnyVal(25);
int nIntData = myAnyVal;
这种形式得到想要的值。但是需要注意的是,这里不可以对Any的隐式转换做正确性的假定。即,我们不能写下如下代码:
Any myAnyVal(25);
float fData = myAnyVal;
我们不能指望这里返回正确的值。因为Any中保存的数据实际为int,而当这个赋值发生时,根据重载决议,编译器会调用Any的operator float()返回一个float值。而由于float在内存中的放置是基于IEEE浮点格式的,int则是2进制的数据,最后返回的数据就不可能正确了。
基于此,我们要求在使用Any的时候,存放数据的位置和取出数据的位置,都必须由程序员指定对应好的数据类型,并且寄希望于程序员知道自己在做什么。
上述以上的实现方法有2个好处:
1、 节省内存,对于不同大小的数据类型,通过union的形式共用了存储空间。
2、 速度快,没有使用动态分配内存的模式,而是直接将对象放置在union空间中。
但是如果我们想让这个Any支持std::string,就不能像上述实现得那么直接了。
因为union中不能支持non-trivial的数据类型(因为对于non-trivial的数据类型,编译器在为其分配内存空间之后,还要调用其构造函数)。我们必须另外想一种方法。
一种简单的容易想到的方法是保存指针,如下:
union {
int nIntData;
std::string * ptrStr;
};
这样,我们需要在operator =以及copy constructor中手动管理内存。当Any初始化为String类型时,需要动态申请内存,而如果构建好的Any中所含的类型在operator =中由String转为其他类型时,需要将其动态释放,在由其他类型转为String时,则需要动态申请。如果这里的其他类型是另外的动态类型,且也是通过指针保存在union结构中的,则也需要做相应的释放&申请处理。
如下:
Any& operator = (int inData)
{
switch (m_type)
{
case ANYTYPE_STRING:
delete ptrStr;
nIntData = inData;
break;
// other cases
};
m_type = ANYTYPE_INT;
}
Any& operator = (std::string& inData)
{
switch (m_type)
{
case ANYTYPE_INT:
ptrStr = new std::string(inData);
break;
// other cases
};
m_type = ANYTYPE_STRING;
}
除了以上所述的operator = 中所增加的代码之外,constructor以及copy constructor针对std::string重载的版本也需要做内存分配。destructor中也需要根据数据的实际类型判断是否释放相应的指针。
现在,这种做法对于C++内置类型像之前一样是直接支持的。但对于用户自定义类型,则需要通过指针的形式,动态的创建以及释放。当Any类中包含的数据从基本类型切换到non-trivial类型或者反向切换的时候,需要释放或者分配内存;但是在基本类型之间切换的时候,不需要做动态内存分配&释放。
这种不一致性导致了代码需要根据类型不同做出不同的处理,随着Any支持类型的增多,不可避免的代码膨胀发生了,而且每增加一种新类型,需要修改所有重载版本的operator =,以及新增一份constructor及copy constructor,并在destructor中增加对应类型的判断。
最为让人恼火的是,Any在处理trivial类型和non-trivial类型时的行为不一。这非常容易导致错误。
下面,我们尝试把问题简化一下,通过让Any始终保存指针来避免行为不统一的问题。无论从何种类型切换到另一种类型,我们都确保必须释放先前的内存,并为目标类型分配新的内存。
新的数据结构可以设置如下:
class Any {
public:
void * m_pointer;
int m_nType;
// supporting types
enum {
ANYTYPE_INT,
ANYTYPE_FLOAT,
ANYTYPE_STRING,
//… other supporting types
};
Any(int inData) : m_nType(ANYTYPE_INT), m_pointer(new int(inData)) {}
Any(float inData) : m_nType(ANYTYPE_FLOAT), m_pointer(new float(inData) {}
Any(const std::string& inData) :
m_nType(ANYTYPE_STRING), m_pointer(new std::string(inData)) {}
Any(const Any& rhs) : m_nType(rhs.m_nType) {
switch (m_nType)
{
case ANYTYPE_INT:
m_pointer = new int(*((int *)rhs.m_pointer));
break;
case ANYTYPE_FLOAT:
m_pointer = new float(*((float *)rhs.m_pointer));
break;
case ANYTYPE_STRING:
m_pointer = new std::string(*((std::string *)rhs.m_pointer));
break;
}
}
Any& Operator = (int inData) {
if (m_nType == ANYTYPE_INT)
*(int*)m_pointer = inData;
else {
switch (m_nType)
{
case ANYTYPE_FLOAT:
delete (float*)m_pointer;
break;
case ANYTYPE_STRING:
delete (std::string*)m_pointer;
break;
}
m_pointer = new int(inData);
};
}
// other operator equals…
};
现在的情况比之前好很多,我们不用再为Any在对待trivial类型数据与non-trivial数据时的行为不一而头痛了。
但是问题依旧很麻烦,因为使用void指针消除了存储时的类型信息,所以当delete指针时,我们需要人为的指定每一处指针所指代的类型,从而使编译器得以调用正确的析构函数。
从而,每一个重载版本的operator =当中,我们都需要判断当前的类型是否与传入参数的类型相符,若不相符,需要根据存储的类型标识符m_nType对m_pointer转型,并使用delete operator完成内存释放的工作,而后再为传入参数分配新的内存。
来回转型可能令你觉得厌烦。或许你会想到将不同种类的指针放在union当中,这样就不必为void指针转型了。但这样做行不通,原因是我们依旧需要根据m_nType的类型决定使用union中的哪个成员,实际上依旧等价于上面的做法,只不过省却了转型操作符(将转型操作符的工作移交到union的定义当中了)。
难道没有更好的方法吗?
在思考如何实现Any的过程中,我们发现了它的两个特点,譬如:
1、 我们需要保存类型信息:m_nType以及相应的enum定义;
2、 在使用Any时,我们需要明确的指出其contain的数据类型,以此得到正确的数据。(例如,使用int构建的Any,从Any中把数据拿出来时,目标也应当是一个int,而不能是float,否则会调用到错误的重载函数,从而得出错误结果)
3、 我们需要针对Any支持的所有类型实现constructor & operator = & operator T的重载版本。
为什么不利用C++自身的设施完成这种工作呢?
由第1、2点,我们发现,Any在存数和取数的时候,需要使用对应的数据类型,从而调用匹配正确的构造函数/operator =以及operator T() (这里的T指代各种类型如int, float)的重载版本。而且,Any被赋予一个值之后,再未被再次赋予其他数据类型的值之前,类型信息是始终保存在Any当中的。而当Any保存的数据类型变动时,对应的类型ID也需要更新。在这里,C++的运行时类型信息(runtime type info)正是用武之地。
在实现针对不同类型的重载函数时,我们发现几乎绝大多数工作都是重复或者类似的,在早期的版本中,甚至可以用宏来节省代码编写的工作量。C++中的模板正暗合了这里的需求。
下面,我们开始随着Ogre::Any的设计思路前行。(Ogre::Any使用了Boost::Any,但在数值Any以及stream操作上做了扩展)
如果利用模板,我们就不必再为每一种需要支持的类型写一个重载版本的constructor & operator = & operator T了。
C++的成员函数模板使我们可以写下类似下面这样的代码:
class Any {
public:
template <typename ValueType>
Any(const ValueType& v);
template <typename ValueType>
Any& operator = (const ValueType& v);
template <typename ValueType>
operator ValueType() const; // 实际实现中并没有定义这个
};
但是operator ValueType()的约束太过于宽泛了,有了它的存在,现在Any可能被用在任何我们意想不到的地方。所以实际的实现中Ogre::Any并没有定义转型操作符,而是使用了名为any_cast的函数,当我们需要在某个地方从Any中取出我们想要的数据时,我们必须清楚这个Any里放着的是什么,并且明确的把它cast出来。
any_cast的声明如下:
template <typename ValueType>
ValueType* any_cast(Any* anyVal);
template <typename ValueType>
const ValueType* any_cast(const Any* anyVal);
template <typename ValueType>
ValueType any_cast(const Any& anyVal);
这样我们就可以用像使用static_cast一样的方法,使用any_cast,如下:
Any anyVal(3246);
int nVal = any_cast<int>(anyVal);
OK,模板现在节约了我们大量的重复劳动,一个模板就涵盖了所有的可能,我们不必再为以后需要新增加的数据类型而头痛了。
基于成员函数模板的Any看起来似乎不错。但是究竟应该如何存储数据呢?
首先,想到的是在Any中置放一个模板成员变量,像这样:
class Any {
public:
template <typename ValueType>
Any(const ValueType& v);
private:
T m_val;
};
但是这样做显然行不通,因为这样一来Any类就必须是一个模板类了。这不符合我们对Any的期望。况且,一旦确定了Any的模板参数,他也就成了一个只能承载确定类型的wrapper了。这不是我们想要的。
那么如果不保存模板成员,而是使用模板成员指针是否可行呢?答案依旧是不可行。因为形如T* m_val;的定义依旧需要在编译期获知T的准确类型。一旦在编译期确定了准确的类型,我们就无法在运行期动态改变他了。
我们需要的是一个运行期可以动态变化的模板成员。
由于前面的分析,直接将一个模板成员存储于Any当中是行不通的,但是我们却可以保存一个确定类型的指针,并让这个指针应该指向实际存储我们需要的模板数据的实例。
通过类似于以下这样的继承关系:
class placeholder;
class holder<T> : public placeholder;
我们拥有了承载无穷种数据类型的可能性。
该继承关系如下图:
有了这样的数据承载类之后,Any中只需保存一个placeholder接口的指针即可。而在constructor / operator = 的时候,只需删除此前的数据,并为新的数据类型创建对应的holder<T>实例即可。
placeholder需要定义clone接口,用于在copy constructor中生成数据的拷贝。
需要定义getType接口,用于在any_cast中识别当前保存的数据类型与目标类型是否一致。(getType的实现可以采用此前的enum + m_nType的方法,但是这种方法的局限性在于我们需要为每一个可能的类型增加一个标识符,因此更好的做法是使用C++的运行时类型识别信息RTTI:Runtime type info / Runtime type identify)
实际的placeholder定义如下:
class placeholder {
public:
virtual ~placeholder() {} // 虚析构函数用以保证派生类的析构函数得以调用
virtual placeholder * clone() const = 0;
virtual const std::type_info& getType() const = 0; // 返回rtti信息
virtual void writeToStream(std::ostream& o) = 0; // 串行化支持
};
真正的承载数据的类模板holder定义很简单,实现相应的基类接口即可,如下:
template<typename ValueType>
class holder : public placeholder
{
public: // structors
holder(const ValueType & value) : held(value) { }
virtual const std::type_info & getType() const { return typeid(ValueType); }
virtual placeholder * clone() const { return new holder(held); }
virtual void writeToStream(std::ostream& o) { o << held; }
ValueType held;
};
这样一来,Any的定义就自然而生了:
class Any {
public:
Any() : mContent(0) {}
template<typename ValueType>
explicit Any(const ValueType & value)
: mContent(new holder<ValueType>(value)) { }
Any(const Any & other)
: mContent(other.mContent ? other.mContent->clone() : 0) { }
virtual ~Any() { delete mContent; }
Any& swap(Any & rhs)
{
std::swap(mContent, rhs.mContent);
return *this;
}
template<typename ValueType>
Any& operator=(const ValueType & rhs)
{
Any(rhs).swap(*this);
return *this;
}
Any & operator=(const Any & rhs)
{
Any(rhs).swap(*this);
return *this;
}
bool isEmpty() const { return !mContent; }
const std::type_info& getType() const
{ return mContent ? mContent->getType() : typeid(void); }
inline friend std::ostream& operator <<
( std::ostream& o, const Any& v )
{
if (v.mContent) v.mContent->writeToStream(o);
return o;
}
protected:
placeholder* mContent;
template<typename ValueType>
friend ValueType * any_cast(Any *);
};
以上就是Any的几乎所有的定义了。此前介绍的holder以及placeholder由于只在Any中被使用到,因此在实做中被定义为Any的嵌套类。
any_cast的定义具体如下:
template<typename ValueType>
ValueType * any_cast(Any * operand)
{
return operand && operand->getType() == typeid(ValueType)
? &static_cast<Any::holder<ValueType> *>(operand->mContent)->held
: 0;
}
需要判断被cast的Any中所贮存的数据的类型是否与目标类型一致,判断采用了C++的RTTI中的typeid。当类型不一致时,返回的结果为0。这一点的行为与dynamic_cast类似(dynamic_cast在正常的转型失败时会返回0)。使用any_cast而不是用类型转换操作符的原因,一方面在于基于模板的类型转换操作符过于随意,另一方面在于any_cast的使用方式与static_cast等几乎完全一致,符合C++的使用习惯,且当需要查找程序中有多少地方使用了any_cast时,一个grep就可以简单的给出结果。
另外两个重载版本的any_cast皆是调用上述指针版本的any_cast完成的。
至此,关于Any的实现的探讨告一段落。
最终Ogre::Any利用C++的模板特性,RTTI特性实现了一个非常具有实用价值的任意类型构件。由于采用了指针存储数据,如果大量的使用Any,会比基于union以及基本类型的实现方式速度缓慢一些。
然而对于期待高速度的多类型组合结构,在Boost中存在另一个构件可以达到相应目的,即Boost::Variant。
Boost::Variant可以像这样使用:Boost::Variant<int, float, std::string, vector<int> > myVariant;
具体Boost::Variant的使用以及实现方式,不在本文的探讨范围内。如果对此感兴趣,可以参阅Boost的文档:http://www.boost.org/doc/libs/1_37_0/doc/html/variant.html
另外,关于Boost::Any的实现,刘未鹏还撰写过一篇非常优秀的文章:
http://blog.csdn.net/pongba/archive/2004/08/24/82811.aspx
附:Ogre中除了使用Boost::Any之外,还对Any在数值上的应用做了扩展:
Ogre在Any的基础上,实现了+-*/的数学运算操作符,构成了AnyNumberic类。
Any::placeholder接口实现了getType,clone,writeToStream以实现对于实际数据的取类型,拷贝,写流操作。
在AnyNumberic中,numplaceholder的实现则需附加定义一套加减乘除的操作接口,从而使得AnyNumberic可以实现+-*/等运算符。(Any的结构此前已经分析过了,通过调用placeholder的接口实现对实际数据的操作,而placeholder接口背后的实现,则是一个根据初始化Any的实际类型实例化的模板类,在AnyNumberic中,这一结构相同)
class numplaceholder : public Any::placeholder {
virtual ~numplaceholder() {} // override virtual destructor
virtual placeholder add(placeholder* rhs) = 0;
virtual placeholder subtract(placeholder* rhs) = 0;
virtual placeholder multiply(placeholder* rhs) = 0;
virtual placeholder multiply(float rhs) = 0; // override version
virtual placeholder divide(placeholder* rhs) = 0;
};
template<typename ValueType>
class numholder : public numplaceholder {
//…
};
利用这一实现,AnyNumberic即可采用如下方式实现operator运算符:
AnyNumeric AnyNumeric::operator+(const AnyNumeric& rhs) const
{
return AnyNumeric(
static_cast<numplaceholder*>(mContent)->add(rhs.mContent));
}
其中mContent为继承自Any的数据成员,类型为Any::placeholder的指针。
AnyNumberic没有定义新的数据成员,仅仅是提供了一些新的接口,并通过派生Any::placeholder在不改动旧有功能的基础上,提供了新的数学运算的能力。
