1. 程式人生 > >C++ —— 巨集對於簡化類介面的奇技淫巧

C++ —— 巨集對於簡化類介面的奇技淫巧

不知不覺接觸虛幻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. 物盡其用,揚長避短,這是墜吼的(蛤蛤臉)!

<全文完>