c++11-17 模板核心知識(二)—— 類模板
- 2020 年 11 月 8 日
- 筆記
- 類模板聲明、實現與使用
- Class Instantiation
- 使用類模板的部分成員函數
- 友元
- 類模板的全特化
- 類模板的偏特化
- 默認模板參數
- Type Aliases
- 類模板的參數推導 Class Template Argument Deduction
類模板聲明、實現與使用
聲明:
template <typename T>
class Stack {
private:
std::vector<T> elems; // elements
public:
void push(T const &elem); // push element
void pop(); // pop element
T const &top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
實現:
template <typename T>
void Stack<T>::push(T const &elem) {
elems.push_back(elem); // append copy of passed elem
}
template <typename T>
void Stack<T>::pop() {
assert(!elems.empty());
elems.pop_back(); // remove last element
}
template <typename T>
T const &Stack<T>::top() const {
assert(!elems.empty());
return elems.back(); // return copy of last element
}
使用:
int main() {
Stack<int> intStack; // stack of ints
Stack<std::string> stringStack; // stack of strings
// manipulate int stack
intStack.push(7);
std::cout << intStack.top() << '\n';
// manipulate string stack
stringStack.push("hello");
std::cout << stringStack.top() << '\n';
stringStack.pop();
}
有兩點需要注意
- 在類聲明內的構造函數、拷貝構造函數、析構函數、賦值等用到類名字的地方,可以將
Stack<T>
簡寫為Stack
,例如:
template<typename T>
class Stack {
...
Stack (Stack const&); // copy constructor
Stack& operator= (Stack const&); // assignment operator
...
};
但是在類外,還是需要Stack<T>
:
template<typename T>
bool operator== (Stack<T> const& lhs, Stack<T> const& rhs);
- 不可以將類模板聲明或定義在函數或者塊作用域內。通常類模板只能定義在global/namespace 作用域,或者是其它類的聲明裡面。
Class Instantiation
instantiation的概念在函數模板中說過。在類模板中,類模板函數只有在被調用時才會被instantiate
。在上面的例子中,push()
和top()
都會被Stack<int>
和Stack<std::string>
所instantiate
,但是pop()
只被Stack<std::string>
所instantiate
。
使用類模板的部分成員函數
我們為Stack新提供printOn()
函數,這需要elem
支援<<
操作:
template<typename T>
class Stack {
...
void printOn() (std::ostream& strm) const {
for (T const& elem : elems) {
strm << elem << ' '; // call << for each element
}
}
};
根據上一小節關於類模板的instantiation
,只有使用到該函數時才會進行該函數的instantiation
。假如我們的模板參數是元素不支援<<
的std::pair< int, int>
,那麼仍然可以使用類模板的其他函數,只有調用printOn
的時候才會報錯:
Stack<std::pair< int, int>> ps; // note: std::pair<> has no operator<<
defined
ps.push({4, 5}); // OK
ps.push({6, 7}); // OK
std::cout << ps.top().first << 』\n』; // OK
std::cout << ps.top().second << 』\n』; // OK
ps.printOn(std::cout); // ERROR: operator<< not supported for element type
Concept
這就引出了一個問題,我們如何知道一個類模板和它的模板函數需要哪些操作?
在c++11中,我們有static_assert
:
template<typename T>
class C
{
static_assert(std::is_default_constructible<T>::value, "Class C requires default-constructible elements");
...
};
假如沒有static_assert,提供的模板參數不滿足std::is_default_constructible
,程式碼也編譯不過。但是編譯器產出的錯誤資訊會很長,包含整個模板instantiation
的資訊——從開始instantiation
直到引發錯誤的地方,讓人很難找出錯誤的真正原因。
所以使用static_assert是一個辦法。但是static_assert適用於做簡單的判斷,實際場景中我們的場景會更加複雜,例如判斷模板參數是否具有某個特定的成員函數,或者要求它們支援互相比較,這種情況下使用concept就比較合適。
concept是c++20中用來表明模板庫限制條件的一個特性,在後面會單獨說明concept,這裡為了文章篇幅先暫時只說一下為什麼要有concept.
友元
首先需要明確一點:友元雖然看起來好像是該類的一個成員,但是友元不屬於這個類。這裡友元指的是友元函數和友元類。這點對於理解下面各種語法規則至關重要。
方式一
template<typename T>
class Stack {
...
void printOn(std::ostream &strm) const {
for (T const &elem : elems) {
strm << elem << ' '; // call << for each element
}
}
template <typename U>
friend std::ostream &operator<<(std::ostream &, Stack<U> const &);
};
template <typename T>
std::ostream &operator<<(std::ostream &strm, Stack<T> const &s) {
s.printOn(strm);
return strm;
}
int main() {
Stack<std::string> s;
s.push("hello");
s.push("world");
std::cout << s;
return 0;
}
這裡在類里聲明的友元函數使用的是與類模板不同的模板參數<template typename U>
,是因為友元函數的模板參數與類模板的模板參數不互相影響,這可以理解為我們創建了一個新的函數模板。
再舉一個友元類的例子:
template<typename T>
class foo {
template<typename U>
friend class bar;
};
這裡也使用的是不同的模板參數。也就是:bar<char>
、bar<int>
、bar<float>
和其他任何類型的bar都是foo<char>
的友元。
方式二
template<typename T>
class Stack;
template<typename T>
std::ostream& operator<< (std::ostream&, Stack<T> const&);
template<typename T>
class Stack {
...
friend std::ostream& operator<< <T> (std::ostream&, Stack<T> const&);
};
這裡提前聲明了Stackoperator<<
,並且在類模板中,operator<<
後面使用了<T>
,沒有使用新的模板參數。與第一種方式對比,這裡創建了一個特例化的非成員函數模板作為友元 (注意這個友元函數的聲明,是沒有<T>
的 )。
方式一中第二個友元類的例子用本方式寫是:
template<typename T>
class bar;
template<typename T>
struct foo {
friend class bar<T>;
};
對比的,這裡只有bar<char>
是foo<char>
的友元類。
關於類模板友元規則有很多,知道有哪幾大類規則即可(Friend Classes of Class Templates、Friend Functions of Class Templates、Friend Templates),用到的時候再查也來得及。可以參考:《C++ Templates Second Edition》12.5小節。 (關注公眾號:紅宸笑。回復:電子書 獲取pdf)
類模板的全特化
與函數模板類似,但是要注意的是,如果你想要全特化一個類模板,你必須全特化這個類模板的所有成員函數。
template <>
class Stack<std::string> {
private:
std::deque<std::string> elems; // elements
public:
void push(std::string const &); // push element
void pop(); // pop element
std::string const &top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
void Stack<std::string>::push(std::string const &elem) {
elems.push_back(elem); // append copy of passed elem
}
void Stack<std::string>::pop() {
assert(!elems.empty());
elems.pop_back(); // remove last element
}
std::string const &Stack<std::string>::top() const {
assert(!elems.empty());
return elems.back(); // return copy of last element
}
在類聲明的開始處,需要使用template<>
並且表明類模板的全特化參數類型:
template<>
class Stack<std::string> {
...
};
在成員函數中,需要將T
替換成特化的參數類型:
void Stack<std::string>::push (std::string const& elem) {
elems.push_back(elem); // append copy of passed elem
}
類模板的偏特化
類模板可以針對某一些特性場景進行部分特化,比如我們針對模板參數是指針進行偏特化:
// partial specialization of class Stack<> for pointers:
template <typename T>
class Stack<T *> {
private:
std::vector<T *> elems; // elements
public:
void push(T *); // push element
T *pop(); // pop element
T *top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
template <typename T>
void Stack<T *>::push(T *elem) {
elems.push_back(elem); // append copy of passed elem
}
template <typename T>
T *Stack<T *>::pop() {
assert(!elems.empty());
T *p = elems.back();
elems.pop_back(); // remove last element
return p; // and return it (unlike in the general case)
}
template <typename T>
T *Stack<T *>::top() const {
assert(!elems.empty());
return elems.back(); // return copy of last element
}
注意類聲明與全特化的不同:
template<typename T>
class Stack<T*> {
};
使用:
Stack<int*> ptrStack; // stack of pointers (special implementation)
ptrStack.push(new int{42});
多模板參數的偏特化
與函數模板重載類似,比較好理解。
原模板:
template<typename T1, typename T2>
class MyClass {
...
};
重載:
// partial specialization: both template parameters have same type
template<typename T>
class MyClass<T,T> {
...
};
// partial specialization: second type is int
template<typename T>
class MyClass<T,int> {
...
};
// partial specialization: both template parameters are pointer types
template<typename T1, typename T2>
class MyClass<T1*,T2*> {
...
};
使用:
MyClass<int,float> mif; // uses MyClass<T1,T2>
MyClass<float,float> mff; // uses MyClass<T,T>
MyClass<float,int> mfi; // uses MyClass<T,int>
MyClass<int*,float*> mp; // uses MyClass<T1*,T2*>
同樣也會有重載衝突:
MyClass<int,int> m; // ERROR: matches MyClass<T,T> and MyClass<T,int>
MyClass<int*,int*> m; // ERROR: matches MyClass<T,T> and MyClass<T1*,T2*>
默認模板參數
也與函數默認參數類似。比如我們為Stack<>
增加一個默認參數,代表管理Stack元素的容器類型:
template <typename T, typename Cont = std::vector<T>>
class Stack {
private:
Cont elems; // elements
public:
void push(T const &elem); // push element
void pop(); // pop element
T const &top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
template <typename T, typename Cont>
void Stack<T, Cont>::push(T const &elem) {
elems.push_back(elem); // append copy of passed elem
}
template <typename T, typename Cont>
void Stack<T, Cont>::pop() {
assert(!elems.empty());
elems.pop_back(); // remove last element
}
template <typename T, typename Cont>
T const &Stack<T, Cont>::top() const {
assert(!elems.empty());
return elems.back(); // return copy of last element
}
注意定義成員函數的模板參數變成了2個:
template<typename T, typename Cont>
void Stack<T,Cont>::push (T const& elem) {
elems.push_back(elem); // append copy of passed elem
}
使用:
// stack of ints:
Stack<int> intStack;
// stack of doubles using a std::deque<> to manage the elements
Stack<double,std::deque<double>> dblStack;
Type Aliases
new name for complete type
兩種方式:typedef、using(c++11)
- typedef
typedef Stack<int> IntStack;
void foo (IntStack const& s);
IntStack istack[10];
- using
using IntStack = Stack<int>;
void foo (IntStack const& s);
IntStack istack[10];
alias template
using比typedef有一個很大的優勢是可以定義alias template:
template <typename T>
using DequeStack = Stack<T, std::deque<T>>; // stack of strings
int main() {
DequeStack<int> ds;
return 0;
}
再強調一下,不可以將類模板聲明或定義在函數或者塊作用域內。通常類模板只能定義在global/namespace 作用域,或者是其它類的聲明裡面。
在之前函數模板文章中介紹過的std::common_type_t
,實際上就是一個別名:
template <class ..._Tp> using common_type_t = typename common_type<_Tp...>::type;
Alias Templates for Member Types
- typedef:
struct C {
typedef ... iterator;
...
};
- using:
struct MyType {
using iterator = ...;
...
};
使用:
template<typename T>
using MyTypeIterator = typename MyType<T>::iterator; // typename必須有
MyTypeIterator<int> pos;
關鍵字typename
上面的注釋說明了:typename MyType<T>::iterator
里的typename
是必須的,因為這裡的typename代表後面緊跟的是一個定義在類內的類型,否則,iterator
會被當成一個靜態變數或者枚舉:
template <typename T> class B {
public:
static int x; // 類內的靜態變數
using iterator = ...; // 類內定義的類型
};
template <typename T>
int B<T>::x = 20;
int main() {
std::cout << B<int>::x; // 20
return 0;
}
Using or Typedef
個人傾向使用using :
- using使用
=
,更符合看程式碼的習慣、更清晰:
typedef void (*FP)(int, const std::string&); // typedef
using FP = void (*)(int, const std::string&); // using
- 上面提到的,using定義
alias template
更方便。
類模板的參數推導 Class Template Argument Deduction
或許你會覺得每次使用模板時都需要顯示的指明模板參數類型會多此一舉,如果類模板能像auto
一樣自動推導模板類型就好了。在C++17中,這一想法變成了可能:如果構造函數能夠推導出所有的模板參數,那麼我們就不需要顯示的指明模板參數類型。
Stack<int> intStack1; // stack of strings
Stack<int> intStack2 = intStack1; // OK in all versions
Stack intStack3 = intStack1; // OK since C++17
添加能推斷出類模型類型的構造函數:
template<typename T>
class Stack {
private:
std::vector<T> elems; // elements
public:
Stack () = default; //
Stack (T elem) : elems({std::move(elem)}) {}
...
};
使用:
Stack intStack = 80; // Stack<int> deduced since C++17
之所以添加Stack () = default;
是為了Stack<int> s;
這種默認構造不報錯。
Deduction Guides
我們可以使用Deduction Guides
來提供額外的模板參數推導規則,或者修正已有的模板參數推斷規則。
Stack(char const*) -> Stack<std::string>;
Stack stringStack{"bottom"}; // OK: Stack<std::string> deduced since C++17
更多規則和用法可以看:Class template argument deduction (CTAD) (since C++17)
(完)
朋友們可以關注下我的公眾號,獲得最及時的更新: