C++模板與泛型編程:類模板

定義模版

類模板

類模板是用來生成類的藍圖的。與函數模板的不同之處是,編譯器不能爲類模板推斷模板參數的類型。所以我們需要在模板名後的尖括號中提供額外信息——用來代替模板參數的模板實參列表。

定義類模板

​ 我們將實現一個名爲 Blob 的模板類,且該模板類會對元素的共享(且覈查過的)訪問能力。

​ 類似函數模板,我們的類以關鍵字 template 開始,後跟模板參數列表。在類模板(及其成員)的定義中,我們將模板參數當做替身,代替使用模板時用戶需要提供的類型或值:

template <typename T> class Blob {
public:
    typedef T value_type;
    // 爲什麼要使用 typename 在後面一篇博客有解釋。
    typedef typename std::vector<T>::size_type size_type;
    // 構造函數
    Blob();
    Blob(std::initializer_list<T> i1);
    // Blob 中的元素數目
    size_type size() const { return data -> size(); }
    bool empty() const { return data -> empty(); }
    // 添加和刪除元素
    void push_back(const T& t) { data -> push_back(t); }
    void push_back(T&& t) { data -> push_back(std::move(t)); }
    void pop_back();
    // 元素訪問
    T& back();
    T& operator[](size_type i);

private:
    std::shared_ptr<std::vector<T>> data;
    void check(size_type i,const std::string &msg) const;
};

我們的 Blob 模板有一個名爲 T 的模板類型參數,用來表示 Blob 保存的元素的類型。當用戶實例化 Blob 時,T 就會被替換爲特定的模板實參類型。

實例化類模板

​ 當我們使用類模板時,我們必須提供額外的信息。這些額外信息是顯式模板實參列表,它們被綁定到模板參數。編譯器使用這些模板實參來實例化出特定的類。

​ 例如:

Blob<int> id;		// 空 Blob<int>
Blob<int> ia2 = {0,1,2,3,4};	// 含 5 個元素的 Blob<int>

對於 Blob<int> 編譯器便會實例化出一個與下面定義等價的類:

template <> class Blob<int> {
public:
    typedef typename std::vector<int>::size_type size_type;
    // 構造函數
    Blob();
    Blob(std::initializer_list<int> i1);
    // ...
    int& back();
    int& operator[](size_type i);

private:
    std::shared_ptr<std::vector<int>> data;
    void check(size_type i,const std::string &msg) const;
};

實際上就是重寫 Blob 模板,將模板參數 T 每個實例替換爲給定的模板實參,在本例中就是 int。

在模板作用域中引用模板類型

​ 爲了閱讀模板類代碼,應該記住類模板的名字不是一個類型名。類模板用來實例化類型,而一個實例化的類型總是包含模板參數的。

​ 可能令人迷惑的是,一個類模板中的代碼如果使用了另一個模板,通常不將一個實際類型(或值)的名字用作其模板實參。相反,我們通常將模板自己的參數當做被使用模板的參數。如,我們的 data 定義如下:

std::shared_ptr<vector<T>> data;

它使用了 Blob 的類型參數來聲明 data 是一個 shared_ptr 實例。當我們實例化一個特定類型的 Blob,例如 Blob<string>時,data 會成爲:

shared_ptr<vector<string>> data;
類模板的成員函數

​ 我們既可以在類模板內部,也可以在類模板外部爲其定義函數,且定義在類模板內的成員函數被隱式聲明爲內聯函數。

​ 類模板的每個實例都有自己版本的成員函數。因而,定義在類外部的函數必須以關鍵字 template 開始,後接類模板參數列表。同時,定義時我們仍然需要說明成員屬於哪個類,所以 Blob 類外部定義成員的格式應該是下面這樣:

template<typename T>
ret-type Blob<T>::member-name(parm-list)
check 和 元素訪問成員

​ 首先定義 check 成員,它檢查一個給定的索引:

template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const {
    if(i >= data -> size())
        throw std::out_of_range(msg);
}

我們可以發現,除了聲明作用域的類名是使用了模板參數列表外,其他的幾乎和普通函數一樣。

​ 我們也可以寫出 back 函數和下標運算符:

template <typename T>
T& Blob<T>::operator[](size_type i) {
    check(i,"subscript out of range");
    return (*data)[i];
}

template <typename T>
T& Blob<T>::back() {
    check(0,"back on empty Blob");
    return data -> back();
}

模板返回的是 T&,指向用來實例化 Blob 的類型。

​ pop_back 函數:

template <typename T>
void Blob<T>::pop_back() {
    check(0,"pop_back on empty Blob");
    data -> pop_back();
}
Blob 構造函數

​ 同樣,構造函數在類外的定義和普通函數一樣,要以 template 關鍵字開始:

template <typename T>
Blob<T>::Blob():data(std::make_shared<std::vector<T>>()) { }

template <typename T>
Blob<T>::Blob(std::initializer_list<T> i1):data(std::make_shared<std::vector<T>>(i1)) { }
類模板成員的實例化

​ 默認情況下,一個類模板的成員函數只有當程序用到它時才進行實例化。例如,下面代碼:

// 實例化 Blob<int> 和接受 initializer_list<int>的構造函數
Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};
// 實例化 Blob<int>::size() const
for(size_t i = 0;i != squares.size();++ i)
    square[i] = i * i;		// 實例化 Blob<int>::operator[](size_t)

如果一個成員函數沒有被使用,則它不會被實例化。 成員函數只有在被用到時才進行實例化,這一特性使得即使某種類型不能完全符合模板操作的要求,我們仍然能用該類型實例化類。

在類代碼內簡化模板類名的使用

​ 當我們使用一個類模板類型時必須提供模板參數,但這一規則有一個例外。在類模板自己的作用域中,我們可以直接使用模板名而不提供模板實參

// 若試圖訪問一個不存在的元素,BlobPtr 將拋出異常
// 當然,我們的 BlobPtr 應該爲 Blob 的友元類。
// 聲明方式:template<typename> class BlobPtr;
template <typename T> class BlobPtr {
public:
    BlobPtr(): curr(0) { }
    BlobPtr(Blob<T> &a,size_t sz = 0):
        wptr(a.data), curr(sz) { }
    T& operator*() const {
        auto p = check(curr,"dereference past end");
        return (*p)[curr];
    }
    BlobPtr&operator++();		// 前置
    BlobPtr&operator--();
    BlobPtr&operator++(int);	// 後置
    BlobPtr&operator--(int);
private:
    std::size_t curr;       // 數組中的當前位置
    std::weak_ptr<std::vector<T>> wptr;
    // 若檢查成功,check 返回一個指向 vector 的 shared_ptr
    std::shared_ptr<std::vector<T>>
        check(std::size_t t,const std::string&msg) const;
};
template<typename T>
std::shared_ptr<std::vector<T>> BlobPtr<T>::check(std::size_t t,const std::string &msg) const {
    auto ret = wptr.lock();
    if(!ret) throw std::runtime_error("unbound StrBlobPtr");
    if(t >= ret -> size())
        throw std::out_of_range(msg);
    return ret;
}

我們可以發現,BlobPtr 的前置遞增和遞減返回的是 BlobPtr&,而不是 BlobPtr<T>&。當我們處於一個模板類的作用域中時,編譯器處理模板自身引用時就好像我們已經提供了與模板參數匹配的實參一樣。

在類模板外使用模板名

​ 當我們在類模板外定義其成員時,必須記住,我們不在類的作用域中,只要遇到類名才表示進入類的作用域:

// 前置遞增/遞減
template <typename T> BlobPtr<T>& BlobPtr<T>::operator++() {
    check(curr,"increment past end of BlobPtr");
    ++ curr;
    return *this;
}

template <typename T> BlobPtr<T>& BlobPtr<T>::operator--() {
    -- curr;        // 如果 curr 爲 0,curr 是 size_t 類型,它會變成一個極大數
    check(curr,"decrement past begin of BlobPtr");
    return *this;
}
類模板和友元

​ 當一個類包含一個友元聲明時,類與友元各自是否是模版是無關的。如果一個模板類包含一個非模板友元,則友元被授權可以訪問所有模板實例。如果友元自身是模板,類可以授權給所有友元模板實例,也可以只授權給特定實例。

一對一友好關係

​ 類模板與另一個模板間友好關係的最常見的形式是建立對應實例及其友元間的友好關係。例如,我們爲 Blob 定義 BlobPtr 友元,爲 Blob 定義 operator== 友元:

template<typename T> class BlobPtr;
template<typename T> class Blob;
template<typename T>
	bool operator==(const Blob<T>&,const Blob<T>&);
template<typename T> class Blob {
    // 每個 Blob 實例將訪問權限授予用相同類型實例化的 BlobPtr 與相等運算符
    friend class BlobPtr<T>;
    friend bool operator==<T>(const Blob<T>&,const Blob<T>&);
    // 其他的與之前相同
};

在上面代碼中,每個 Blob 實例將訪問權限授予用相同類型實例化的 BlobPtr 與相等運算符。如:

Blob<int> ia;	// BlobPtr<int> 和 operator==<int> 都是本對象的友元
通用和特定的模板友好關係

​ 一個類也可以將另一個模板的每個實例都聲明爲自己的友元,或者限定特定的實例爲友元:

// 前置聲明,在將模板的一個特定實例聲明爲友元時要用到
template <typename T> class Pal;
class C {		// C 是普通類
    friend class Pal<C>;		// 用類 C 實例化的 Pal 是 C 的一個友元
    // Pal2 的所有實例都是 C 的友元;這種無需前置聲明
    template <typename T> friend class Pal2;
};
template <typename T> class C2 {		// C2 本身是一個類模板
    // C2 的每個實例將相同實例化的 Pal 聲明爲友元
    friend class Pal<T>;
    // Pal2 的所有實例都是 C2 每個實例的友元,不需要前置聲明
    template <typename X> friend class Pal2;
    // Pal3 是一個非模板類,它是 C2 所有實例的友元
    friend class Pal3;		// 不需要 Pal3 的前置聲明
};
令模板自己的類型參數成爲友元

​ 在新標準中,我們可以將模板類型參數聲明爲友元:

template <typename Type> class Bar {
friend Type;	// 將訪問權限授予用來實例化 Bar 的類型
    // ...
};

此處我們將用來實例化 Bar 的類型聲明爲友元。即 Sales_data 將會成爲 Bar<Sales_data> 的友元。

​ 值得注意的是,雖然友元通常來說是一個類或函數,但我們完全可以用內置類型來實例化 Bar,這種與內置類型的友好關係是允許的。

模板類型別名

​ 類模板的一個實例定義了一個類類型,與任何其他類類型一樣,我們可以定義一個 typedef 來引用實例化的類:

typedef Blob<string> StrBlob;
StrBlob x;						// x 爲 Blob<string>

​ 由於模板不是一個類型,我們不能定義一個 typedef 引用一個模板。即,無法定義一個 typedef 引用 Blob<T>。但是新標準允許我們爲類模板定義一個類型別名:

template<typename T> using twin = pair<T,T>;
twin<string> authors;		// authors 是 pair<string,string>

​ 一個模板類型別名就是一族類的別名:

twin<int> win_loss;		// win_loss 是 pair<int,int>
twin<double> area;		// area 是 pair<double,double>

​ 當我們定義一個模板類型別名時,可以固定一個或多個模板參數:

template<typename T> using partNo = pair<T,unsigned>;
partNo<string> books;		// books 是 pair<string,unsigned>
partNo<double> s;	// s 是 pair<double,unsigned>
類模板的 static 成員

​ 類模板可以聲明 static 成員:

template<typename T> class Foo {
public:
    static std::size_t count() { return ctr; }
private:
    static std::size_t ctr;
};

Foo 是一個類模板,它有一個名爲 count 的 public static 成員函數和一個名爲 ctr 的 private static 數據成員。每個 Foo 的實例都有其自己的 static 成員實例。即,對任意的給定類型 X,都有一個 Foo<X>::ctr 和一個 Foo<X>::count 成員。所有 Foo<X> 類型的對象都共享相同的 ctr 和 count。例如:

Foo<int> fi,fi2,fi3;		// 這三個對象共享相同的 Foo<int>::ctr 和 Foo<int>::count

與任何其他 static 數據成員相同,模板類的每個 static 數據成員必須有且僅有一個定義。但是,類模板的每個實例都有一個獨有的 static 對象,所以我們將 static 數據成員也定義爲模版:

template<typename T>
size_t Foo<T>::ctr = 0;		// 定義並初始化 ctr

​ 這樣,當使用一個特定的模板實參實例化 Foo 時,將會爲該類類型實例化一個獨立的 ctr,並將其賦值爲 0。

​ 與非模板類的靜態成員類似,我們可以通過類類型對象來訪問一個類模板的 static 成員,也可以使用作用域運算符直接訪問成員。當然,爲了通過類來直接訪問 static 成員,我們必須引用一個特定的實例:

Foo<int> fi;			// 實例化 Foo<int> 類和 static 數據成員 ctr
auto ct = Foo<int>::count();	// 實例化 Foo<int>::count
ct = fi.count();				// 使用 Foo<int>::count
ct = Foo::count();				// 錯誤,使用哪個模板實例的 count

類似其他成員函數,一個 static 成員只有在使用時纔會實例化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章