C++ 模板與泛型編程


《C++ Primer 4th》讀書筆記

所謂泛型編程就是以獨立於任何特定類型的方式編寫代碼。泛型編程與面向對象編程一樣,都依賴於某種形式的多態性。

面向對象編程中的多態性在運行時應用於存在繼承關系的類。我們能夠編寫使用這些類的代碼,忽略基類與派生類之間類型上的差異。

在泛型編程中,我們所編寫的類和函數能夠多態地用於跨越編譯時不相關的類型。一個類或一個函數可以用來操縱多種類型的對象。

面向對象編程所依賴的多態性稱為運行時多態性,泛型編程所依賴的多態性稱為編譯時多態性或參數式多態性。

 

模板是泛型編程的基礎。模板是創建類或函數的藍圖或公式。

 

函數模板

模板定義以關鍵字 template 開始,后接模板形參表,模板形參表是用尖括號括住的一個或多個模板形參的列表,形參之間以逗號分隔。模板形參表不能為空。

template <typename T>

int compare(const T &v1, const T &v2)

{

if (v1 < v2) return -1;

if (v2 < v1) return 1;

return 0;

}

 

模板形參可以是表示類型的類型形參,也可以是表示常量表達式的非類型形參。類型形參跟在關鍵字 class 或 typename 之后定義.在函數模板形參表中,關鍵字 typename 和 class 具有相同含義,可以互換使用,兩個關鍵字都可以在同一模板形參表中使用:

// ok: no distinction between typename and class in template parameter list

template <typename T, class U> calc (const T&, const U&);

 

模板形參表示可以在類或函數的定義中使用的類型或值。使用函數模板時,編譯器會推斷哪個(或哪些)模板實參綁定到模板形參。一旦編譯器確定了實際的模板實參,就稱它實例化了函數模板的一個實例。

實質上,編譯器將確定用什么類型代替每個類型形參,以及用什么值代替每個非類型形參。推導出實際模板實參后,編譯器使用實參代替相應的模板形參產生編譯該版本的函數。編譯器承擔了為我們使用的每種類型而編寫函數的單調工作。

int main ()

{

// T is int;

// compiler instantiates int compare(const int&, const int&)

cout << compare(1, 0) << endl;

// T is string;

// compiler instantiates int compare(const string&, const string&)

string s1 = "hi", s2 = "world";

cout << compare(s1, s2) << endl;

return 0;

}

 

 函數模板可以用與非模板函數一樣的方式聲明為 inline。說明符放在模板形參表之后、返回類型之前,不能放在關鍵字 template 之前。

// ok: inline specifier follows template parameter list

template <typename T> inline T min(const T&, const T&);

// error: incorrect placement of inline specifier

inline template <typename T> T min(const T&, const T&);

 

類模板

類模板也是模板,因此必須以關鍵字 template 開頭,后接模板形參表。Queue 模板接受一個名為 Type 的模板類型形參。

除了模板形參表外,類模板的定義看起來與任意其他類問相似。類模板可以定義數據成員、函數成員和類型成員,也可以使用訪問標號控制對成員的訪問,還可以定義構造函數和析構函數等等。在類和類成員的定義中,可以使用模板形參作為類型或值的占位符,在使用類時再提供那些類型或值。

template <class Type> class Queue {

public:

Queue (); // default constructor

Type &front (); // return element from head of Queue

const Type &front () const;

void push (const Type &); // add element to back of Queue

void pop(); // remove element from head of Queue

bool empty() const; // true if no elements in the Queue

private:

// ...

};

 

與調用函數模板形成對比,使用類模板時,必須為模板形參顯式指定實參:

Queue<int> qi; // Queue that holds ints

Queue< vector<double> > qc; // Queue that holds vectors of doubles

Queue<string> qs; // Queue that holds strings

 

除了定義數據成員或函數成員之外,類還可以定義類型成員。如果要在函數模板內部使用這樣的類型,必須告訴編譯器我們正在使用的名字指的是一個類型。必須顯式地這樣做,因為編譯器(以及程序的讀者)不能通過檢查得知,由類型形參定義的名字何時是一個類型何時是一個值。如果希望編譯器將 size_type 當作類型,則必須顯式告訴編譯器這樣做:

template <class Parm, class U>

Parm fcn(Parm* array, U value)

{

typename Parm::size_type * p; // ok: declares p to be a pointer

}

通過在成員名前加上關鍵字 typename 作為前綴,可以告訴編譯器將成員當作類型。

如果拿不准是否需要以 typename 指明一個名字是一個類型,那么指定它是個好主意。在類型之前指定 typename 沒有害處,因此,即使 typename 是不必要的,也沒有關系。

 

非類型模板形參

模板形參不必都是類型。模板非類型形參是模板定義內部的常量值,在需要常量表達式的時候,可使用非類型形參(例如,像這里所做的一樣)指定數組的長度。

// initialize elements of an array to zero

template <class T, size_t N> void array_init(T (&parm)[N])

{

for (size_t i = 0; i != N; ++i) {

parm[i] = 0;

}

}

int x[42];

double y[10];

array_init(x); // instantiates array_init(int(&)[42]

array_init(y); // instantiates array_init(double(&)[10]

 

類型等價性與非類型形參: 對模板的非類型形參而言,求值結果相同的表達式將認為是等價的。array_init 調用引用的是相同的實例—— array_init<int, 42>:

int x[42];

const int sz = 40;

int y[sz + 2];

array_init(x); // instantiates array_init(int(&)[42])

array_init(y); // equivalent instantiation

 

在函數模板內部完成的操作限制了可用於實例化該函數的類型。程序員的責任是,保證用作函數實參的類型實際上支持所用的任意操作,以及保證在模板使用哪些操作的環境中那些操作運行正常。

 

編寫獨立於類型的代碼的一般原則:編寫模板代碼時,對實參類型的要求盡可能少是很有益的。說明了編寫泛型代碼的兩個重要原則:

• 模板的形參是 const 引用。

• 函數體中的測試只用 < 比較。

通過將形參設為 const 引用,就可以允許使用不允許復制的類型。大多數類型(包括內置類型和我們已使用過的除 IO 類型之外的所有標准庫的類型)都允許復制。但是,也有不允許復制的類類型。將形參設為 const 引用,保證這種類型可以用於 compare 函數,而且,如果有比較大的對象調用 compare,則這個設計還可以使函數運行得更快。

 

實例化

模板是一個藍圖,它本身不是類或函數。編譯器用模板產生指定的類或函數的特定類型版本。產生模板的特定類型實例的過程稱為實例化。模板在使用時將進行實例化,類模板在引用實際模板類類型時實例化,函數模板在調用它或用它對函數指針進行初始化或賦值時實例化。

 

類模板的每次實例化都會產生一個獨立的類類型。為 int 類型實例化的 Queue 與任意其他 Queue 類型沒有關系,對其他Queue 類型成員也沒有特殊的訪問權。

 

從函數實參確定模板實參的類型和值的過程叫做模板實參推斷。

 

類型形參的實參的受限轉換

一般而論,不會轉換實參以匹配已有的實例化,相反,會產生新的實例。除了產生新的實例化之外,編譯器只會執行兩種轉換:

• const 轉換:接受 const 引用或 const 指針的函數可以分別用非 const對象的引用或指針來調用,無須產生新的實例化。如果函數接受非引用類型,形參類型實參都忽略 const,即,無論傳遞 const 或非 const 對象給接受非引用類型的函數,都使用相同的實例化。

• 數組或函數到指針的轉換:如果模板形參不是引用類型,則對數組或函數類型的實參應用常規指針轉換。數組實參將當作指向其第一個元素的指針,函數實參當作指向函數類型的指針。

template <typename T> T fobj(T, T); // arguments are copied

template <typename T>

T fref(const T&, const T&); // reference arguments

string s1("a value");

const string s2("another value");

fobj(s1, s2); // ok: calls f(string, string), const is ignored

fref(s1, s2); // ok: non const object s1 converted to const reference int a[10], b[42];

fobj(a, b); // ok: calls f(int*, int*)

fref(a, b); // error: array types don't match; arguments aren't converted to pointers

 

模板實參推斷與函數指針

可以使用函數模板對函數指針進行初始化或賦值,這樣做的時候,編譯器使用指針的類型實例化具有適當模板實參的模板版本。

template <typename T> int compare(const T&, const T&);

// pf1 points to the instantiation int compare (const int&, constint&)

int (*pf1) (const int&, const int&) = compare;

獲取函數模板實例化的地址的時候,上下文必須是這樣的:它允許為每個模板形參確定唯一的類型或值。如果不能從函數指針類型確定模板實參,就會出錯。

// overloaded versions of func; each take a different function pointer type

void func(int(*) (const string&, const string&));

void func(int(*) (const int&, const int&));

func(compare); // error: which instantiation of compare?

 

在返回類型中使用類型形參

指定返回類型的一種方式是引入第三個模板形參,它必須由調用者顯式指定:

// T1 cannot be deduced: it doesn't appear in the function parameterlist

template <class T1, class T2, class T3>

T1 sum(T2, T3);

// ok T1 explicitly specified; T2 and T3 inferred from argument types

long val3 = sum<long>(i, lng); // ok: calls long sum(int, long)

 

顯式模板實參從左至右對應模板形參相匹配,第一個模板實參與第一個模板形參匹配,第二個實參與第二個形參匹配,以此類推。

// poor design: Users must explicitly specify all three template parameters

template <class T1, class T2, class T3>

T3 alternative_sum(T2, T1);

// error: can't infer initial template parameters

long val3 = alternative_sum<long>(i, lng);

// ok: All three parameters explicitly specified

long val2 = alternative_sum<long, int, long>(i, lng);

 

模板編譯模型

編譯器實例化特定類型的模板的時候,編譯器必須能夠訪問定義模板的源代碼。當調用函數模板或類模板的成員函數的時候,編譯器需要函數定義,需要那些通常放在源文件中的代碼。標准 C++ 為編譯模板代碼定義了兩種模型。

在包含編譯模型中,編譯器必須看到用到的所有模板的定義。一般而言,可以通過在聲明函數模板或類模板的頭文件中添加一條 #include 指示使定義可用,該#include 引入了包含相關定義的源文件:

// header file utlities.h

#ifndef UTLITIES_H // header gaurd (Section 2.9.2, p. 69)

#define UTLITIES_H

template <class T> int compare(const T&, const T&);

// other declarations

#include "utilities.cc" // get the definitions for compare etc.

#endif

// implemenatation file utlities.cc

template <class T> int compare(const T &v1, const T &v2)

{

if (v1 < v2) return -1;

if (v2 < v1) return 1;

return 0;

}

// other definitions

 

在分別編譯模型中,編譯器會為我們跟蹤相關的模板定義。但是,我們必須讓編譯器知道要記住給定的模板定義,可以使用 export 關鍵字來做這件事。對類模板使用 export 更復雜一些。通常,類聲明必須放在頭文件中

// class template header goes in shared header file

template <class Type> class Queue { ... };

// Queue.ccimplementation file declares Queue as exported

export template <class Type> class Queue;

#include "Queue.h"

// Queue member definitions

導出類的成員將自動聲明為導出的。也可以將類模板的個別成員聲明為導出的,在這種情況下,關鍵字 export 不在類模板本身指定,而是只在被導出的特定成員定義上指定。

 

類模板的 static 成員

template <class T> class Foo {

public:

static std::size_t count() { return ctr; }

// other interface members

private:

static std::size_t ctr;

// other implementation members

};

每個實例化表示截然不同的類型,所以給定實例外星人所有對象都共享一個static 成員。因此,Foo<int> 類型的任意對象共享同一 static 成員 ctr,Foo<string> 類型的對象共享另一個不同的 ctr 成員。

 

通常,可以通過類類型的對象訪問類模板的 static 成員,或者通過使用作用域操作符直接訪問成員。當然,當試圖通過類使用 static 成員的時候,必須引用實際的實例化:

Foo<int> fi, fi2; // instantiates Foo<int> class

size_t ct = Foo<int>::count(); // instantiates Foo<int>::count

ct = fi.count(); // ok: uses Foo<int>::count

ct = fi2.count(); // ok: uses Foo<int>::count

ct = Foo::count(); // error: which template instantiation?

與任意其他成員函數一樣,static 成員函數只有在程序中使用時才進行實例化。

 

像使用任意其他 static 數據成員一樣,必須在類外部出現數據成員的定義。在類模板含有 static 成員的情況下,成員定義必須指出它是類模板的成員:

template <class T> size_t Foo<T>::ctr = 0; // define and initialize ctr

 

一個泛型句柄類

/* generic handle class: Provides pointerlike behavior. Although access through

* an unbound Handle is checked and throws a runtime_error exception.

* The object to which the Handle points is deleted when the last Handle goes away.

* Users should allocate new objects of type T and bind them to a Handle.

* Once an object is bound to a Handle,, the user must not delete that object.

*/

template <class T> class Handle {

public:

// unbound handle

Handle(T *p = 0): ptr(p), use(new size_t(1)) { }

// overloaded operators to support pointer behavior

T& operator*();

T* operator->();

const T& operator*() const;

const T* operator->() const;

// copy control: normal pointer behavior, but last Handle deletes the object

Handle(const Handle& h): ptr(h.ptr), use(h.use)

{ ++*use; }

Handle& operator=(const Handle&);

~Handle() { rem_ref(); }

private:

T* ptr; // shared object

size_t *use; // count of how many Handle spointto *ptr

void rem_ref()

{ if (--*use == 0) { delete ptr; delete use; } }

};

 

template <class T>

inline Handle<T>& Handle<T>::operator=(const Handle &rhs)

{

++*rhs.use; // protect against self-assignment

rem_ref(); // decrement use count and delete pointers if

needed

ptr = rhs.ptr;

use = rhs.use;

return *this;

}

 

template <class T> inline T& Handle<T>::operator*()

{

if (ptr) return *ptr;

throw std::runtime_error("dereference of unbound Handle");

}

template <class T> inline T* Handle<T>::operator->()

{

if (ptr) return ptr;

throw std::runtime_error("access through unbound Handle");

}

 

template <class T> inline const T* Handle<T>::operator->() const

{

           if (ptr) return ptr;

else throw std::logic_error("unbound Sales_item");

 }

 

template <class T> inline const T& Handle<T>:: const

{

           if (ptr) return *ptr;

else throw std::logic_error("unbound Sales_item");

}

 

class Sales_item {

public:

// default constructor: unbound handle

Sales_item(): h() { }

// copy item and attach handle to the copy

Sales_item(const Item_base &item): h(item.clone()) { }

// no copy control members: synthesized versions work

// member access operators: forward their work to the Handle class

const Item_base& operator*() const { return *h; }

const Item_base* operator->() const

{ return h.operator->(); }

private:

Handle<Item_base> h; // use-counted handle

};

 

 

模板特化

模板特化(template specialization)是這樣的一個定義,該定義中一個或多個模板形參的實際類型或實際值是指定的。特化的形式如下:

• 關鍵字 template 后面接一對空的尖括號(<>);

• 再接模板名和一對尖括號,尖括號中指定這個特化定義的模板形參;

• 函數形參表;

• 函數體。

template <typename T>

int compare(const T &v1, const T &v2)

{

if (v1 < v2) return -1;

if (v2 < v1) return 1;

return 0;

}

 

// special version of compare to handle C-style character strings

template <>

int compare<const char*>(const char* const &v1,

const char* const &v2)

{

return strcmp(v1, v2);

}

 

模板特化必須總是包含空模板形參說明符,即 template<>,而且,還必須包含函數形參表。如果可以從函數形參表推斷模板實參,則不必顯式指定模板實參:

// error: invalid specialization declarations

// missing template<>

int compare<const char*>(const char* const&,

const char* const&);

// error: function parameter list missing

template<> int compare<const char*>;

// ok: explicit template argument const char* deduced from parameter types

template<> int compare(const char* const&, const char* const&);

 

當定義非模板函數的時候,對實參應用常規轉換;當特化模板的時候,對實參類型不應用轉換。在模板特化版本的調用中,實參類型必須與特化版本函數的形參類型完全匹配,如果不完全匹配,編譯器將為實參從模板定義實例化一個實例。

 

與其他函數聲明一樣,應在一個頭文件中包含模板特化的聲明,然后使用該特化的每個源文件包含該頭文件。

 

普通作用域規則適用於特化

在能夠聲明或定義特化之前,它所特化的模板的聲明必須在作用域中。類似地,在調用模板的這個版本之前,特化的聲明必須在作用域中:

// define the general compare template

template <class T>

int compare(const T& t1, const T& t2) { /* ... */ }

int main() {

// uses the generic template definition

int i = compare("hello", "world");

// ...

}

// invalid program: explicit specialization after call

template<>

int compare<const char*>(const char* const& s1,

const char* const& s2)

{ /* ... */ }

 

這個程序有錯誤,因為在聲明特化之前,進行了可以與特化相匹配的一個調用。當編譯器看到一個函數調用時,它必須知道這個版本需要特化,否則,編譯器將可能從模板定義實例化該函數。

 

重載與函數模板

函數模板可以重載:可以定義有相同名字但形參數目或類型不同的多個函數模板,也可以定義與函數模板有相同名字的普通非模板函數。

 

如果重載函數中既有普通函數又有函數模板,確定函數調用的步驟如下:

1. 為這個函數名建立候選函數集合,包括:

a. 與被調用函數名字相同的任意普通函數。

b. 任意函數模板實例化,在其中,模板實參推斷發現了與調用中所用函數實參相匹配的模板實參。

2. 確定哪些普通函數是可行的(第 7.8.2 節)(如果有可行函數的話)。候選集合中的每個模板實例都 可行的,因為模板實參推斷保證函數可以被調用。

3. 如果需要轉換來進行調用,根據轉換的種類排列可靠函數,記住,調用模板函數實例所允許的轉換是有限的。

a. 如果只有一個函數可選,就調用這個函數。

b. 如果調用有二義性,從可行函數集合中去掉所有函數模板實例。

4. 重新排列去掉函數模板實例的可行函數。

• 如果只有一個函數可選,就調用這個函數。

• 否則,調用有二義性。

 

設計既包含函數模板又包含非模板函數的重載函數集合是困難的,因為可能會使函數的用戶感到奇怪,定義

函數模板特化幾乎總是比使用非模板版本更好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM