c++11 boost技術交流群:296561497,歡迎大家來交流技術。
關於類型擦除,可能很多人都不清楚,不知道類型擦除是干啥的,為什么需要類型擦除。有必要做個說明,類型擦除就是將原有類型消除或者隱藏。為什么要擦除類型?因為很多時候我不關心具體類型是什么或者根本就不需要這個類型,通過類型擦除我們可以獲取很多好處,比如使得我們的程序有更好的擴展性、還能消除耦合以及消除一些重復行為,使程序更加簡潔高效。歸納一下c++中類型擦除方式主要有如下五種:
第一種:通過多態來擦除類型
第二種:通過模板來擦除類型
第三種:通過某種容器來擦除類型
第四種:通過某種通用類型來擦除類型
第五種:通過閉包來擦除類型
第一種類型隱藏的方式最簡單也是我們經常用的,通過將派生類型隱式轉換成基類型,再通過基類去多態的調用行為,在這種情況下,我不用關心派生類的具體類型,我只需要以一種統一的方式去做不同的事情,所以就把派生類型轉成基類型隱藏起來,這樣不僅僅可以多態調用還使我們的程序具有良好的可擴展性。然而這種方式的類型擦除僅僅是部分的類型擦除,因為基類型仍然存在,而且這種類型擦除的方式還必須是繼承方式的才可以,而且繼承使得兩個對象強烈的耦合在一起了,正是因為這些缺點,通過多態來擦除類型的方式有較多局限性效果也不好。這時我們通過第二種方式擦除類型,以解決第一種方式的一些缺點。通過模板來擦除類型,本質上是把不同類型的共同行為進行了抽象,這時不同類型彼此之間不需要通過繼承這種強耦合的方式去獲得共同的行為了,僅僅是通過模板就能獲取共同行為,降低了不同類型之間的耦合,是一種很好的類型擦除方式。然而,第二種方式雖然降低了對象間的耦合,但是還有一個問題沒解決,就是基本類型始終需要指定,並沒有消除基本類型,例如,我不可能把一個T本身作為容器元素,必須在容器初始化時就要知名這個T是具體某個類型。這時多么希望有一種通用的類型啊,可以讓我的容器容納所有的類型,就像c#和java中的object類型一樣,是所有類型的基類。c++中沒有這種object類型怎么辦?也許有人想到了,可以用boost.variant類型,是的,boost.variant可以把各種不同的類型包起來,從而讓我們獲得了一種統一的類型,而且不同類型的對象間沒有耦合關系,它僅僅是一個類型的容器。讓我們看看怎么用boost.variant來擦除類型。

struct blob { const char *pBuf; int size; }; //定義通用的類型,這個類型可能容納多種類型 typedef boost::variant<double, int, uint32_t, sqlite3_int64, char*, blob, NullType>Value; vector<Value> vt; //通用類型的容器,這個容器現在就可以容納上面的那些類型的對象了 vt.push_back(1); vt.push_back("test"); vt.push_back(1.22); vt.push_back({"test", 4});
上面的代碼就擦除了不同類型,使得不同的類型都可以放到一個容器中了,如果要取出來就很簡單,通過get<T>(Value)就可以獲取對應類型的值了。這種方式是通過某種容器把類型包起來了,從而達到類型擦除的目的。它的缺點是這個通用的類型必須事先定義好,它只能容納聲明的那些類型,增加一種新類型就不行了。通過第四種方式可以消除這個缺點,通過某種通用類型來擦除類型。類似於c#和java中的object類型。這種通用類型是通過boost.any實現的,它不需要預先定義類型,不同類型都可以轉成any。讓我們看看怎么用any來擦除類型的。
unordered_map<string, boost::any> m_creatorMap; m_creatorMap.insert(make_pair(strKey, new T)); //T may be any type boost::any obj = m_creatorMap[strKey]; T t = boost::any_cast<T>(obj);
需要注意的是,第四和第五種方式雖然解決了第三種方式不能徹底消除基本類型的缺點,但是還存一個缺點,就是取值的時候仍然依賴於具體類型,無論我是通過get<T>還是any_case<T>,我都要T的具體類型,這在某種情況下仍然有局限性。例如,有這樣一種場景:
我有A、B、C、D四種結構體,每個結構體中有某種類型的指針,名稱且稱為info,我現在提供了返回這些結構體的四個接口供外接使用,有可能是c#或者dephi調用這些接口,由於結構體中的info指針是我分配的內存,所以我必須提供釋放這些指針的接口。代碼如下:
struct A { int* info; int id; }; struct B { double* info; int id; }; struct C { char* info; int id; }; struct D { float* info; int id; }; //對外提供的刪除接口 void DeleteA(A& t) { delete t.info; } void DeleteB(B& t) { delete t.info; } void DeleteC(C& t) { delete t.info; } void DeleteD(D& t) { delete t.info; }
大家可以看到,增加的四個刪除函數內部都是重復代碼,本來通過模板函數一行搞定,但是沒辦法,c#可沒有c++的模板,還得老老實實的提供這些重復行為的接口,而且這種方式還有個壞處就是每增加一種類型就得增加一個重復的刪除接口,怎么辦?能統一成一個刪除接口嗎?可以,一個可行的辦法就是將分配的內存通過一個ID關聯並保存起來,讓外接傳一個ID,告訴我要刪那塊內存,新的統一刪除函數可能是這樣:
//內部將分配的內存存到map中,讓外面傳ID,內部通過ID去刪除對應的內存塊 map<int, T> mapT; template<typename R, typename T> R GetT() { R result{1,new T()}; mapT.insert(std::pair<int, T>(1, R)); return result; } //通過ID去關聯我分配的內存塊,外面傳ID,內部通過ID去刪除關聯的內存塊 void DeleteT(const int& id) { R t = mapT[id]->second(); delete t.info; }
很遺憾,上面的代碼編譯不過,因為,map<int, T> mapT只能保存一種類型的對象,無法把分配的不同類型的對象保存起來,我們可以通過方式三和方式四,用variant或者any去擦除類型,解決T不能代表多種類型的問題,第一個問題解決。但是還有第二個問題,DeleteT時,從map中返回的variant或者any,無法取出來,因為接口函數中沒有類型信息,而取值方法get<T>和any_cast<T>都需要一個具體類型。似乎進入了死胡同,無法只提供一個刪除接口了。但是辦法總還是有的。
方式五隆重登場了,看似無解的問題,通過方式五就能解決了。通過閉包來擦除類型很好很強大。在介紹方式五之前,我要先介紹一下閉包,閉包也可以稱為匿名函數或者lamda表達式,c++11中的lamda表達式就是c++中的閉包,c++11引入lamda,實際上引入了函數式編程的概念,函數式編程有很多優點,使代碼更簡潔,而且聲明式的編碼方式更貼近人的思維方式。函數式編程在更高的層次上對不同類型的公共行為進行了抽象,從而使我們不必去關心具體類型。關於函數式編程的優點就不多說了。下面看看如何使用方式五去解決上面的問題。
std::map < int, std::function <void()>> m_freeMap; //保存返回出去的內存塊 template<typename R, typename T> R GetResult() { R result = GetTable<R, T>(); m_freeMap.insert(std::make_pair(result.sequenceId, [this, result] { FreeResult(result); })); } bool FreeResultById(int& memId) { auto it = m_freeMap.find(memId); if (it == m_freeMap.end()) return false; it->second(); //delete by lamda m_freeMap.erase(memId); return true; }
總結:通過閉包去擦除類型,可以解決前面四種擦除方式遇到的問題,優雅而簡單!