程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 天方夜譚VCL:多態

天方夜譚VCL:多態

編輯:Delphi

天方夜譚VCL: 多態

蟲蟲

我們中國人崇拜龍,所謂“龍生九種,九種各別”。哪九種?《西游記》裡西海龍王對孫悟空說:“第一個小黃龍,見居淮渎;第二個小骊龍,見住濟渎;第三個青背龍,占了江渎;第四個赤髯龍,鎮守河渎;第五個徒勞龍,與佛祖司鐘;第六個穩獸龍,與神官鎮脊;第七個敬仲龍,與玉帝守擎天華表;第八個蜃龍,在大家兄處砥據太岳。此乃第九個鼍龍,因年幼無甚執事,自舊年才著他居黑水河養性,待成名,別遷調用,誰知他不遵吾旨,沖撞大聖也。”(注:鼍龍是文雅的說法,民間叫法是豬婆龍,也就是揚子鳄。)如果您沖著這九位說一聲“Let’s go”,那場面可壯觀了,有天上飛的,有水裡游的,也有地上爬的。同樣是“go”,“go”的具體形式卻各不相同,這正是多態“一個接口,多種實現”的典型例子。

多態的實現方法很多,其中C++直接支持的方式有:通過關鍵字virtual提供虛函數進行遲後聯編,以及通過模板(template)實現靜態多態性,它們都各有用武之地。我們比較熟悉的是虛函數,這是建構類層次的重要手段,我們也已經分析過虛函數的原理[1]。然而在有些情況下,虛函數的性能並不是最優,故VCL還提供了一種動態(dynamic)函數,用法和虛函數一模一樣,只要把virtual換成DYNAMIC就可以了。VCL的幫助文件裡說,動態函數跟虛擬函數相比,空間效率占優,時間效率不行,真的嗎?其實現原理又是如何呢?我們又應該如何權衡這兩者的使用呢?我們將從一個相當一般的角度來討論這些問題。

虛函數的苦惱

如下類層次來自一個圖形繪制程序的一部分。為了方便管理,界面與具體的圖形設計分離。各種圖形以動態連接庫的方式提供,作為插件的形式。這樣可以在不重新編譯主程序的情況,增加或減少各種圖形。


圖1 Shape類層次

最初Shape的聲明是

class Shape {
private:
	int x0, y0;
protected:
        Shape();
        virtual ~Shape();
public:
	int x() const;
	int y() const;
        virtual void draw(void *) = 0;
        virtual int move(int, int);
};
後來因為功能擴充,添加了兩個虛函數。
class Shape {
private:
	int x0, y0;
protected:
        Shape();
        virtual ~Shape();
public:
	int x() const;
	int y() const;
	virtual int move(int, int);
        virtual void draw(void *) = 0;
        virtual void save(void *) const = 0;
        virtual void load(void *) = 0;
};
後來又作過一些修改,又添加了若干虛函數。問題就在於,虛函數一但增加,虛擬函數表VFT就會發生變化,這時候,主程序就必須重新編譯。更糟糕的是,一旦版本升級,派生自不同版本Shape的圖形絕對不可以混用[2]。所以我們可以看到硬盤裡充斥著mfc20.dll、mfc40.dll、mfc42.dll……卻一個也不能刪除,這就是MFC升級所帶來的DLL垃圾。怎麼辦?
初步解決

我在網上問過這樣的問題,得到的答復主要有:

  • 用COM;
  • 預先多寫一些無用的虛函數,留出擴充空間。

其實上面的方法都能很好地解決這個問題。但是推廣看來,也有一定局限性。COM不適合解決類層次過深的情況,預留的空間則是不折不扣的“雞肋”。

追根究底,這個局限性是因為父類和子類的虛擬函數表VFT之間過強的關聯性:子類的VFT的前面一部分必須與父類相同。而當父類和子類不在同一個DLL或EXE中的時候,這個要求是很難滿足的。父類一旦改變,子類如果不重新編譯,就將導致錯誤。解決的方法,當然就是取消父類和子類VFT之間的關聯性。我設計了一個很笨的解決辦法,但可以取消這個關聯性,使虛函數保證始終只有2個。

#define Dynamic // Dynamic什麼都不是,只是好看一點

struct point
{
	int x, y;
};

class dispatch_error{};

class Shape {
private:
	int x0, y0;
protected:
        Shape();
        virtual ~Shape();
	virtual void dispatch(int id, void* in, void* out);
	// in和out是函數的輸入輸出參數,id是每個函數唯一的標記符號,即代號
	// 實際運用中,id不一定是整數,也可以是128位UUID,或者字符串等等
public:
	int x() const;
	int y() const;
	Dynamic int move(int dx, int dy)
	{
		int r;
		point p = {dx, dy};
		dispatch(-1, &p, &r);
		return r;
	}
        Dynamic void draw(void *hdc)		{dispatch(-2, hdc, 0);}
        Dynamic void save(void* o) const	{dispatch(-3, o, 0);}
        Dynamic void load(void* i)		{dispatch(-4, i, 0);}
};

void Shape::dispatch(int id, void* in, void* out)
{
        switch(id)
        {
                case -1:
                        ...
                case -2:
                        ...
		...
                default:
                        throw(dispatch_error()); // 若函數不存在則拋出異常
        }
}
如果子類Triangle要改寫Shape::draw,那麼只需要
void Triangle::dispatch(int id, void* in, void* out)
{
        switch(id)
        {
                ...
                case -2:	// 改寫Shape::draw
                        ...
		...
                default:
                        Shape::dispatch(id, in, out); //函數不存在則向父類找
        }
}
這樣的“Dynamic函數”就解決了前面的問題,只有析構和dispatch這兩個虛函數。父類和子類的VFT之間沒有關聯性,可以自由改動而不會互相影響。
評頭論足

我們來對這種解決方案作了評價:的確解決了虛函數的問題,但是也付出了不小的代價:時間效率和可讀性,由此也決定了該方案的應用面不廣,一般用於

  • 虛函數很少或幾乎不需改寫的情況。這樣有助於減少VFT的大小。至於運行速度則沒有什麼提高,畢竟VFT的訪問速度是常數級[vcl_chong.htm#33" name=3>3];
  • 父類需要經常更新而子類不方便同步更新,對效率要求又不高的情況。一般的應用程序都可以使用。

從模式(Patterns)的角度來看,這種方法是典型的職責鏈(Chain of Responsibility)模式[4]:調用請求從最低層子類開始一層層往上傳遞,直到被處理或者最後拋出異常。這種模式運用非常廣泛,比如VCL消息映射[5]和COM中IDispatch接口[6],與上述解決方案的形式都非常相似。

這個解決方案還可以作進一步的完善,以更好地適用於單根結構的框架。比如單根結構的類庫,如MFC和VCL,通過RTTI可以找到唯一的父類,那麼可以分離數據(函數代號和指針)和代碼(調配部分),以簡化結構。解決的方法就是典型的表格驅動,有不少書[7,8]都用此來優化COM中IUnkown接口的QueryInterface。我們引入類DMT來儲存函數的代號和指針。

#include 
using namespace std;

class DMT {
        char* const ptr;
        const DMT* const parent;
public:
        DMT(const DMT* const, const int, ...);
        ~DMT() {delete []ptr;}
        short size() const  {return *(short*)ptr;}
        const void* find(int) const;
};


圖2 類DMT圖解

需要特別注意的是DMT::ptr所分配的空間。在32位系統上,對於n個“Dynamic函數”,需要sizeof(short)字節儲存n(紅色部分),sizeof(void*)*n字節儲存函數代號(黃色部分),以及sizeof(void*)*n字節儲存函數指針(藍色部分),一共是sizeof(short) + 2*n*sizeof(void*)字節。子類和父類的DMT可以通過鏈表形式連接起來。下面我們看看DMT::find和DMT::DMT的實現。

const void* DMT::find(int i) const
{
	const int* begin = (int*)(ptr + sizeof(short)), *p;
	for(p = begin; p < begin + size(); ++p)
		if(*(int*)p == i)
			return *(void**)(p+ size());
			// 找到對應的函數代號後,向前跳DMT::size()則是相應的函數指針
	return (parent)? parent->find(i): 0;
}

DMT::DMT(const DMT* const p, const int n, ...)
	: parent(p), ptr(new char[sizeof(short)+2*n*sizeof(void*)])
					// ptr分配的空間大小如前所述
{
	int* i = (int*)(ptr + 2), c;
	*(short*)ptr = n;		// 往頭sizeof(short)字節寫入n(紅色部分)

	va_list ap;
	va_start(ap, n);

	for(c = 0; c < n; ++c)		// 往黃色部分寫入函數代號
		*(i++) = va_arg(ap, int);

	typedef void (DMT::*temp_type)();
	temp_type temp;

	for(c = 0; c < n; ++c)		// 往藍色部分寫入函數指針
	{
		temp = va_arg(ap, temp_type);
		*(i++) = *(int*)&temp;
	}

	va_end(ap);
}
下面我們在Shape類層次中應用DMT類。
class Shape {
private:						

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved