不知不觉接触虚幻4也快有一年了吧,这一年里对这款引擎或多或少都有一些了解。当使用C++编程时看到虚幻4对于宏的奇技淫巧的使用时,哪怕是现在也感到相当惊艳,因此查阅了一些资料,写篇博客记录一下。

类接口的相关工作

C++的目标之一就是把类的声明和定义分离开来,这对于项目的开发极其有利——这可以使开发人员不用看到类的实现就能看到类的功能。

但是,C++实现类的声明与类定义的分离的方法会导致一些额外的工作——每个非内联函数的表示都需要写两次,一次在类声明中,一次在类定义中。

代码如下:

// .h File
class Element
{
    void Tick ();
};

// .cpp File
void Element ::Tick ()
{
  // todo 
}

由于Tick的标识在两个地方都出现了,因此如果我们需要改变这个方法的参数的时候(改变函数名、返回类型或者加const),我们需要改变两个地方。

当然通常这没有什么工作量,但是有些情况下这个特性会带来不少麻烦。

举个栗子,如果我们有一个叫做BaseClass的基类,有三个从BaseClass继承而来的子类——D1D2D3.其中BaseClass声明了一个虚函数Foo()并且有一个缺省实现,并且D1D2D3中重载了Foo()函数。

现在,如果说我们给BaseClass::Foo()添加一个参数,但是忘了给D3中做相应的修改。

麻烦来了——编译可以通过,编译器会把BaseClass::Foo(...)D3::Foo()当成两个完全不同的函数。当我们想通过虚函数机制来调用D3的Foo的时候,这就容易出一些问题。

UE4中光继承自AActor类的类就有上千个,如果需要对AActor类做一个修改,那么如果使用传统方法,我们还要针对上千个派生类进行修改,而且万一有一个派生类没有修改,编译器也不会报错!

这么看来,理想的情况是我们希望一个函数的表示只在一个地方存在,如果说只声明BaseClass::Foo()一次,然后再它的派生类中不用再额外声明Foo就好了。

而且在效率方面来说,在C++中使用继承的时候我们经常会使用很多浅层次的类继承关系,一个父类往往有一堆子类。很多时候我们只需要把很多互不相关的功能集成到一个单独的类继承家族里面。

对于浅继承来说,我们只是把开始的父类声明为一个接口——也就是说它声明了一些虚函数(大部分是纯虚函数)。在大多数情况下,我们会在这个类家族里面有一个基类以及其余的派生类。

如果说我们的基类有10个函数,我们从这个基类派生了20个类,那么我们就需要额外做200个函数声明。但是这些声明的目的往往只是为了Implement基类中的那些方法而已,这就或多或少的容易使得头文件不好维护。

传统方法的实现

如果说我们有一个Animal的类,这个类被视为基类,我们希望从这个基类派生出不同的子类。在Animal中有3个纯需函数,如下所示:

class Animal
{
    public:

    virtual std :: string GetName () const = 0 ;
    virtual Vector3f GetPosition () const = 0;
    virtual Vector3f GetVelocity () const = 0;
};

同时,这个基类拥有三个派生类——Monkey,Tiger,Lion。

那么我们三个方法的每一个都会在7个地方存在:Animal中一次,Monkey、Lion、Tiget的声明和定义各一次。

然后假设我们做一个小改动——我们想将GetPosition和GetVelocity的返回类型改为Vector4f以适应Transform变换,那么我们就要在7个地方进行修改:Animal的.h文件,Lion、Tiger和Monkey的.h文件和.cpp文件。

使用宏的实现

有一种很妙的处理方法就是将这些方法进行包装,改成所谓接口宏的形式。我们可以试试看:

#define INTERFACE_ANIMAL(terminal)                          \
public:                                                     \
    virtual std::string GetName() const ##terminal          \
    virtual IntVector GetPosition() const ##terminal        \
    virtual IntVector GetVelocity() const ##terminal       

#define BASE_ANIMAL     INTERFACE_ANIMAL(=0;)
#define DERIVED_ANIMAL  INTERFACE_ANIMAL(;)

值得一提的是,##符号代表的是连接,\符号代表的是把下一行的连起来。

通过这些宏,我们就可以大大简化Animal的声明,还有所有从它派生的类的声明了:

// Animal.h
class Animal
{
    BASE_ANIMAL ;
};



// Monkey.h
class Monkey : public Animal
{
    DERIVED_ANIMAL ;
};


// Lion.h
class Lion : public Animal
{
    DERIVED_ANIMAL ;
};


// Tiger.h
class Tiger : public Animal
{
    DERIVED_ANIMAL ;
};

现在,不管我们什么时候想改动Animal的方法,我们都不用再去改动其派生类的头文件了。我们只需要改动这个接口宏而已。

但是我们仍然需要手工修改每个.cpp的实现,但是由于此时的声明已经变动了,此时编译器是会报错并且提示进行修改的。

再说了,这样另外好处还在于.h文件中的声明变得很清晰并且容易维护了。

后记

宏是一个C中一个相当强大的工具,但是它和goto一样被很多人误解了。很多人都认为宏已经过时了而且用之有害,早就被内联函数取而代之,可惜这种方法毕竟too simple, sometimes naive. 物尽其用,扬长避短,这是坠吼的(蛤蛤脸)!

<全文完>