不知不覺接觸虛幻4也快有一年了吧,這一年里對這款引擎或多或少都有一些了解。當使用C++編程時看到虛幻4對於宏的奇技淫巧的使用時,哪怕是現在也感到相當驚艷,因此查閱了一些資料,寫篇博客記錄一下。
類接口的相關工作
C++的目標之一就是把類的聲明和定義分離開來,這對於項目的開發極其有利——這可以使開發人員不用看到類的實現就能看到類的功能。
但是,C++實現類的聲明與類定義的分離的方法會導致一些額外的工作——每個非內聯函數的表示都需要寫兩次,一次在類聲明中,一次在類定義中。
代碼如下:
// .h File
class Element
{
void Tick ();
};
// .cpp File
void Element ::Tick ()
{
// todo
}
由於Tick的標識在兩個地方都出現了,因此如果我們需要改變這個方法的參數的時候(改變函數名、返回類型或者加const),我們需要改變兩個地方。
當然通常這沒有什么工作量,但是有些情況下這個特性會帶來不少麻煩。
舉個栗子,如果我們有一個叫做BaseClass的基類,有三個從BaseClass繼承而來的子類——D1、D2和D3.其中BaseClass聲明了一個虛函數Foo()並且有一個缺省實現,並且D1、D2、D3中重載了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. 物盡其用,揚長避短,這是墜吼的(蛤蛤臉)!
<全文完>
