C++
的泛型編程是一種非常強(qiáng)大的武器拘央。但它看上去復(fù)雜的語法斋日,以及背后不明的原理渴杆,一直讓很多程序員望而生畏林艘。很多即便已經(jīng)使用了C++
很久的程序員也僅僅敢觸碰其非常表象的部分盖奈。還有一些大膽而莽撞的程序員則對其進(jìn)行濫用,讓本來可以簡單解決的問題狐援,變成秀技場钢坦。因而,泛型技術(shù)一直得到一個(gè)不公正的名聲:奇技淫巧啥酱。
但是爹凹,泛型編程對于很多問題的高效解決是不可或缺的。它是C++
最重要的元編程工具镶殷,也是很多框架制作的利器禾酱,也是組合式設(shè)計(jì)的主要手段之一。不掌握它绘趋,C++
的強(qiáng)大威力將大打折扣颤陶。
我們需要深入挖掘其背后的原理及本質(zhì),這樣才能理解它陷遮,掌握它指郁,在合適的場合使用它,避免濫用和無用拷呆。
C++
的泛型編程闲坎,其本質(zhì)其實(shí)非常簡單:為了做基于類型和常量的編譯時(shí)多態(tài)疫粥。(關(guān)于多態(tài)的目的和價(jià)值,請參閱《多態(tài)腰懂,F(xiàn)P與OO》)
而C++
的編譯時(shí)多態(tài)由如下四種類型構(gòu)成:
- 模版
- 函數(shù)(操作符)重載
- 模板特化
- Duck Typing
我們下面一一介紹梗逮。
1. 模版:參數(shù)化多態(tài)
先看一段簡單的haskell
代碼:
max :: (Ord a) => a -> a -> a
max x y = if x > y then x else y
在這個(gè)實(shí)現(xiàn)里,a
是類型變量绣溜,(Ord a)
表示對a
約束慷彤,意思是:對于所有屬于typeclass Ord
的類型a
,都可以實(shí)例化這個(gè)函數(shù)怖喻。
這種用法被稱作參數(shù)化多態(tài)底哗。很顯然,參數(shù)化多態(tài)是為了避免基于類型的重復(fù)代碼锚沸。
而回到C++
跋选,基于模版的實(shí)現(xiàn)則為:
template <typename T>
T max(T a, T b)
{
return a > b ? a : b;
}
參數(shù)化多態(tài),是C++
提供泛型的最初目的哗蜈。STL
的各種容器和算法前标,基本上都是基于這類目的設(shè)計(jì)的。這也是最容易理解距潘,被使用最為廣泛的C++
編譯時(shí)多態(tài)技術(shù)炼列。
Concept
上述haskell
代碼中的約束Ord a
,到C++14
為止尚未支持音比,但已經(jīng)確定要在C++17
中支持(被稱作Concept)俭尖。
高階類型
上述haskell
例子中的多態(tài)類型屬于rank 1 type
,但rank 1 type
無法支持這類的代碼:
g f = (f True, f 'a')
其中洞翩,參數(shù)f
作為一個(gè)函數(shù)目溉,在兩次調(diào)用時(shí)傳入的參數(shù)類型不同:分別為Bool
和Char
。這屬于Rank 2 Type
的范疇菱农。而為了支持Rank 2 Type
缭付,GHC
提供了一個(gè)擴(kuò)展`RankNTypes',并且要求程序員給出類型注解:
{-# LANGUAGE RankNTypes #-}
g :: (forall a. a -> a) -> (Bool, Char)
g f = (f True, f 'a')
而在C++
中循未,對應(yīng)的實(shí)現(xiàn)技術(shù)為Template Template Parameter
陷猫。比如:
template <typename T, typename G,
template <typename E> class Container >
struct Foo
{
Container<T> tContainer;
Container<G> gContainer;
};
這就是參數(shù)化多態(tài)的全部:非常簡單,因而被很多語言采納和引入的妖,也被程序員廣泛的使用绣檬。
2. 函數(shù)重載:Ad-hoc多態(tài)
學(xué)習(xí)任何一門語言的第一個(gè)例子,往往都是Hello World
:
std::cout << "Hello, 2016!" << std::endl;
如果我們讓程序稍微復(fù)雜一些,讓這段代碼可以對在任何一年都可以復(fù)用嫂粟,那么我們可以提供一個(gè)類似于下面的函數(shù):
void helloNewYear(unsigned int year)
{
std::cout << "Hello, " << year << "!" << std::endl;
}
在這個(gè)實(shí)現(xiàn)中娇未,對于字符串和整數(shù)這兩種不同類型的數(shù)據(jù),std::cout
可以用一致的方式來操作星虹。
按照多態(tài)四要素零抬,std::cout
即是客戶镊讼;同一外表是操作符<<
;而不同形態(tài)則是針對不同類型的不同實(shí)現(xiàn):
std::ostream& operator<<(std::ostream& os, const std::string& str);
std::ostream& operator<<(std::ostream& os, const int value);
// ...
而編譯器會根據(jù)類型進(jìn)行匹配(這是一種簡化版本的模式匹配)平夜,從而在不同形態(tài)間進(jìn)行選擇蝶棋。
因而函數(shù)重載是一種多態(tài),而這樣的多態(tài)被稱作ad-hoc
多態(tài)忽妒。
例子:雙重派發(fā)
雙重派發(fā)(Double Dispatch
)玩裙,是訪問者模式(Visitor Pattern
)依托的的實(shí)現(xiàn)技術(shù)。
假設(shè)段直,一顆語法樹吃溅,上面有多種類型的節(jié)點(diǎn)。比如:Statement
鸯檬,Expression
决侈。
而一個(gè)訪問者則必須提供針對各種類型節(jié)點(diǎn)的訪問函數(shù),比如:
struct Visitor
{
virtual void visit(Statement&) = 0;
virtual void visit(Expression&) = 0;
virtual ~Visitor() {}
};
所有的節(jié)點(diǎn)都需要實(shí)現(xiàn)accept
方法京闰,所以它們都需要實(shí)現(xiàn)下面的接口:
struct Element
{
virtual void accept(Visitor&) = 0;
virtual ~Element() {}
};
而每個(gè)具體的節(jié)點(diǎn)在實(shí)現(xiàn)此接口時(shí),分別調(diào)用Visitor
中針對自己類型的成員函數(shù):
struct Statement: Element
{
void accept(Visitor& visitor)
{
visitor.visit(*this);
}
// ...
};
struct Expression: Element
{
void accept(Visitor& visitor)
{
visitor.visit(*this);
}
// ...
};
所謂雙重派發(fā)甩苛,指的正是兩個(gè)多態(tài)結(jié)構(gòu):
- 訪問算法通過接口
accept
,在運(yùn)行時(shí)將visitor
派發(fā)給相應(yīng)的Element
; - 具體的
Element
再從visitor
的多個(gè)訪問函數(shù)中蹂楣,選擇歸屬于自己的那一個(gè),通過它讯蒲,將自己派發(fā)給visitor
痊土。
其中,前者使用的是運(yùn)行時(shí)多態(tài)墨林,后者使用的則是函數(shù)重載赁酝。有了函數(shù)重載,就完全沒必要讓每個(gè)具體的Element
重復(fù)的編寫完全相同的accept
實(shí)現(xiàn)旭等。所以酌呆,我們可以將上述代碼重構(gòu)為:
template <typename T>
struct AutoDispatchElement : Element
{
void accept(Visitor& visitor)
{
visitor.visit((T&)*this);
}
};
struct Statement
: AutoDispatchElement<Statement>
{
// ...
};
struct Expression
: AutoDispatchElement<Expression>
{
// ...
};
并非所有重載都是為了多態(tài)
需要強(qiáng)調(diào)的是:盡管函數(shù)(操作符)重載可以用來實(shí)現(xiàn)編譯時(shí)多態(tài),但并非所有的重載都是為了多態(tài)搔耕。大多數(shù)情況下隙袁,操作符的重載都是為了提高表達(dá)力,比如:
// 讓復(fù)數(shù)可以進(jìn)行 a + b 形式的操作
Complex operator+(const Complex& lhs, const Complex& rhs);
// 讓向量可以進(jìn)行 vector[5] 形式的操作
template <typename T>
T Vector<T>::operator[](unsigned int index);
至于那些參數(shù)個(gè)數(shù)不同的重載函數(shù)弃榨,與多態(tài)更是沒有什么關(guān)系菩收,比如:
void f(int a)
void f(short a, double b);
這種樣式的重載,往往是因?yàn)殚_發(fā)人員懶得為之取一個(gè)更準(zhǔn)確的名字鲸睛,從而濫用了語言提供的重載機(jī)制娜饵,其結(jié)果反而傷害了表達(dá)力。對于這樣的重載官辈,要盡量避免箱舞。
小結(jié)
這一部分我們首先介紹了C++
范型最為簡單的兩種多態(tài):以基于類型的重用為目的參數(shù)化多態(tài)和以函數(shù)重載為手段的ad-hoc多態(tài)遍坟。
在后續(xù)的文章里,會繼續(xù)介紹更為復(fù)雜的C++
編譯時(shí)多態(tài)技術(shù)褐缠。