1、何謂泛型編程
泛型編程(Generic Programming)關注於產生通用的軟 件組件,讓這些組件在不同的應用場合都能很容易地重用。在C++中,類模板和函 數模板是進行泛型編程極為有效的機制。有了這兩大利器,我們在實現泛型化的 同時,並不需要付出效率的代價。
作為泛型編程的一個簡單例子,讓我們 看一下在C庫中如何讓memcpy()函數泛型化。一種實現方法可能是這樣的:
void* memcpy(void* region1, const void* region2, size_t n)
{
const char* first = (const char*)region2;
const char* last = ((const char*)region2) + n;
char* result = (char*)region1;
while (first != last)
*result++ = *first++;
return result;
}
這個memcpy()函數已經 在一定程度上進行了泛型化,采取的措施是使用void*,這樣該函數就可以拷貝不 同類型的數組。但如果我們想拷貝的數據不是放在數組裡,而是由鏈表來存放呢 ?我們能不能擴展這個概念,讓它可以拷貝任意的序列?看看memcpy()的函數體 ,這個函數對傳入的序列有一個_最小需求_:它需要用某種形式的指針來遍歷這 個序列;訪問指針正指向的元素;把元素寫到目的地;比較指針以判斷何時停止 拷貝。C++標准庫把這樣的需求進行分組,稱之為概念(concepts)。在這個例子 中就有輸入迭代器(對應於region1)和輸出迭代器(對應於region2)這兩個概 念。
如果我們把memcpy()用函數模板重寫,使用輸入迭代器和輸出迭代器 作為模板參數來描述對序列的需求,我們就可以寫出一個具有較高重用性的copy ()函數:
template <typename InputIterator, typename OutputIterator>
OutputIterator
copy(InputIterator first, InputIterator last, OutputIterator result)
{
while (first != last)
*result++ = *first++;
return result;
}
使用這個泛型的copy()函數,我們可以拷貝各種各樣的序列,只要 它滿足了我們指定的需求。對外提供了迭代器的鏈表,比如std::list,也可以通 過我們的copy()函數來拷貝:
#include <list>
#include <vector>
#include <iostream>
int main()
{
const int N = 3;
std::vector<int> region1(N);
std::list<int> region2;
region2.push_back (1);
region2.push_back(0);
region2.push_back(3);
std::copy(region2.begin(), region2.end(), region1.begin ());
for (int i = 0; i < N; ++i)
std::cout << region1[i] << " ";
std::cout << std::endl;
}
2、何謂概念?
概念就是 需求的集合,這些需求可以包含有效表達式、相關類型、不變性,以及復雜度保 證。滿足概念中所有需求的類型,稱之為此概念的一個樣例。一個概念可以是對 別的概念的擴展,稱之為概念的細化。
有效表達式:任意的C++表達式。 若某類型是概念的一個樣例,那麼把該類型的一個對象代入此表達式中,該表達 式必能通過編譯。
相關類型:與樣例相關的一些類型,它們和樣例共同出 現在一個或多個有效表達式中。如果樣例類型是自定義類型,則相關類型可以由 類定義中所嵌套的一些typedef來訪問。相關類型經常也通過traits類來訪問。
不變性:對象的一些運行時特征,必須為真。所有用到這個對象的函數, 都必須保持這種特征。不變性通常以前置條件和後置條件的形式出現。
復 雜度保證:對概念中的有效表達式執行所需時間或資源所做的限制。
C++ 標准庫中所使用的概念在SGI STL網站上有詳細的文檔說明。
3、 Traits
traits類為訪問編譯時實體(類型、整數常量,或者地址)的相關信息 提供了一條途徑。比如,類模板std::iterator_traits<T>看起來是這個樣 子:
template <class Iterator>
struct iterator_traits {
typedef ... iterator_category;
typedef ... value_type;
typedef ... difference_type;
typedef ... pointer;
typedef ... reference;
};
該traits的value_type可以讓泛型代碼得知該迭代器所“指向”的是 何種類型的數據;而iterator_category對迭代器的能力進行分類,針對不同類型 的迭代器我們選擇與之適應的最高效的算法。
traits模板有一個很重要的 特點:它們是非侵入的(non-intrusive)。我們可以為任何類型提供相關信息, 不管它是內建的類型,還是第三方的庫中提供的類型。為了給某一特定類型指定 traits,一般采用部分特化或者全特化traits模板的方式。
欲更深入的了 解std::iterator_traits,可以參閱SGI提供的資料。 std::numeric_limits<T>也采用了traits技術,提供表示各內建數值類型 取值范圍的常量值。
4、標記分派
另外有一種技術經常與traits合用, 那就是標記分派。它依據類型的屬性,通過函數重載進行分派。一個很好的例子 就是std::advance()函數。這個函數的功能是將一個迭代器遞增n次,對於不同類 型的迭代器可以有各自優化的實現方法。如果是隨機訪問迭代器(可以以任意的 距離前後跳轉),advance()函數可以用i+=n來實現,既簡單又高效,只需要常量 時間。而其它的迭代器必須一步一步地遞增,需要線性的時間復雜度。如果是雙 向迭代器,n就可能為負值,因此必須判斷對迭代器到底是增還是減。
標 記分派和traits類的聯系很緊密。分派所依據的屬性(在這個例子中是 iterator_category)一般都是通過traits類來取得。主advance()函數從 iterator_traits中獲得對應於該iterator的iterator_category,然後調用重載 過的advance_dispach()函數。編譯器依據作為參數傳給advance_dispach()的 iterator_category,選擇合適的重載版本。標記只是一個極其簡單的類,它的唯 一任務就是為標記分派或者其它類似技術傳遞必要的信息。
namespace std {
struct input_iterator_tag { };
struct bidirectional_iterator_tag { };
struct random_access_iterator_tag { };
namespace detail {
template <class InputIterator, class Distance>
void advance_dispatch(InputIterator& i, Distance n, input_iterator_tag) {
while (n--) ++i;
}
template <class BidirectionalIterator, class Distance>
void advance_dispatch(BidirectionalIterator& i, Distance n,
bidirectional_iterator_tag) {
if (n >= 0)
while (n--) ++i;
else
while (n++) --i;
}
template <class RandomAccessIterator, class Distance>
void advance_dispatch(RandomAccessIterator& i, Distance n,
random_access_iterator_tag) {
i += n;
}
}
template <class InputIterator, class Distance>
void advance(InputIterator& i, Distance n) {
typename iterator_traits<InputIterator>::iterator_category category;
detail::advance_dispatch(i, n, category);
}
}
5、適配器
適配器是一種類模板,建立在其它類型之上,提供新的 接口或者行為。標准庫中就使用了適配器,比如std::reverse_iterator通過反轉 迭代器的遞增/遞減行為,適配了迭代器,還有std::stack,通過適配標准容器, 提供一個簡單的堆棧接口。
在這裡可以找到標准庫中所用適配器的深入闡 述。
6、類型生成器
類型生成器的工作是依據它的模板參數合成新的類 型[注]。類型生成器產生的新類型一般作為生成器中嵌套的typedef出現。用類型 生成器的主要目的是為了讓復雜的類型表達式顯得更簡單些。比如 boost::filter_iterator_generator:
template <class Predicate, class Iterator,
class Value = complicated default,
class Reference = complicated default,
class Pointer = complicated default,
class Category = complicated default,
class Distance = complicated default
>
struct filter_iterator_generator {
typedef iterator_adaptor<
Iterator,filter_iterator_policies<Predicate,Iterator>,
Value,Reference,Pointer,Category,Distance> type;
};
看起來可真夠復雜的。但現在生成一個合適的filter iterator可 就容易多了,只需要寫:
boost::filter_iterator_generator<my_predicate,my_base_iter ator>::type
就行了。
7、對象生成器
對象生成器是一種函 數模板,依據其參數產生新的對象。可以把它想象成泛型化的構造函數。有些情 況下,欲生成的對象的精確類型很難甚至根本無法表示出來,這時對象生成器可 就管用了。對象生成器的優點還在於它的返回值可以直接作為函數參數,而不像 構造函數那樣只有在定義變量時才會調用。Boost和標准庫中用到的對象生成器大 多都加了個前綴make_,比如std::make_pair(const T&, const U&)。
看看下面的例子:
struct widget {
void tweak (int);
};
std::vector<widget *> widget_ptrs;
通過把兩個標准的對象生成器bind2nd和mem_fun合用,我們可以很輕松地tweak所 有的widget:
void tweak_all_widgets1(int arg)
{
for_each(widget_ptrs.begin(), widget_ptrs.end(),
bind2nd (std::mem_fun(&widget::tweak), arg));
}
如果不用對象 生成器,上面的函數可能就得這樣來實現:
void tweak_all_widgets2(int arg)
{
for_each(struct_ptrs.begin (), struct_ptrs.end(),
std::binder2nd<std::mem_fun1_t<void, widget, int> >(
std::mem_fun1_t<void, widget, int>(&widget::tweak), arg));
}
表達式越復雜,就越需要縮短這些冗長的類型說明,對 象生成器的好處也就越能顯現。
8、策略類
策略類就是用來傳遞行為的模 板參數。標准庫中的std::allocator就是策略類,把內存管理的行為應用到標准 容器中。
Andrei Alexandrescu在他的文章中對策略類進入了全面而深入 的分析,他寫道:
“策略類精確地反映了設計時的抉擇。他們要麼 從其它類中繼承,要麼包含在其它類中。策略類在同樣的句法結構上提供了不同 的策略。使用策略的類把它用到的每一個策略都作為模板參數,這樣,用戶就可 以自由地選擇需要使用的策略。
策略類的強大在於它們能夠自由地組合在 一起。通過把策略類作為模板參數的辦法來組合多種策略,代碼量與使用的策略 數只成線性關系。”
Andrei認為策略類的強大源於其小粒度和正交 性。Boost在迭代適配器庫中的策略類運用可能淡化了這一賣點。在這個庫中,所 有已適配的迭代器的行為都放在一個策略類裡面。其實,Boost並不是開先河者。 std::char_traits就是一個策略類,它決定了std::basic_string的行為,盡管它 的名字叫traits而不叫policy。
注:因為C++缺少模板化的typedef,類型 生成器可以作為其替代方案。