c++11-17 模板核心知識(二)—— 類模板
阿新 • • 發佈:2020-11-09
- [類模板宣告、實現與使用](#類模板宣告實現與使用)
- [Class Instantiation](#class-instantiation)
- [使用類模板的部分成員函式](#使用類模板的部分成員函式)
- [Concept](#concept)
- [友元](#友元)
- [方式一](#方式一)
- [方式二](#方式二)
- [類模板的全特化](#類模板的全特化)
- [類模板的偏特化](#類模板的偏特化)
- [多模板引數的偏特化](#多模板引數的偏特化)
- [預設模板引數](#預設模板引數)
- [Type Aliases](#type-aliases)
- [new name for complete type](#new-name-for-complete-type)
- [alias template](#alias-template)
- [Alias Templates for Member Types](#alias-templates-for-member-types)
- [關鍵字typename](#關鍵字typename)
- [Using or Typedef](#using-or-typedef)
- [類模板的引數推導 Class Template Argument Deduction](#類模板的引數推導-class-template-argument-deduction)
- [Deduction Guides](#deduction-guides)
## 類模板宣告、實現與使用
宣告:
```c++
template
class Stack {
private:
std::vector 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();
}
};
```
實現:
```c++
template
void Stack::push(T const &elem) {
elems.push_back(elem); // append copy of passed elem
}
template
void Stack::pop() {
assert(!elems.empty());
elems.pop_back(); // remove last element
}
template
T const &Stack::top() const {
assert(!elems.empty());
return elems.back(); // return copy of last element
}
```
使用:
```c++
int main() {
Stack intStack; // stack of ints
Stack 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`簡寫為`Stack`,例如:
```c++
template
class Stack {
...
Stack (Stack const&); // copy constructor
Stack& operator= (Stack const&); // assignment operator
...
};
```
但是在類外,還是需要`Stack`:
```c++
template
bool operator== (Stack const& lhs, Stack const& rhs);
```
* 不可以將類模板宣告或定義在函式或者塊作用域內。通常類模板只能定義在global/namespace 作用域,或者是其它類的聲明裡面。
## Class Instantiation
instantiation的概念在函式模板中說過。在類模板中,類模板函式只有在被呼叫時才會被`instantiate`。在上面的例子中,`push()`和`top()`都會被`Stack`和`Stack`所`instantiate`,但是`pop()`只被`Stack`所`instantiate`。
![image](https://user-images.githubusercontent.com/14103319/98324443-392d1000-2027-11eb-81d6-c7cce206fcea.png)
## 使用類模板的部分成員函式
我們為Stack新提供`printOn()`函式,這需要`elem`支援`<<`操作:
```c++
template
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`的時候才會報錯:
```c++
Stack> 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`:
```c++
template
class C
{
static_assert(std::is_default_constructible::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.
## 友元
**首先需要明確一點:友元雖然看起來好像是該類的一個成員,但是友元不屬於這個類。這裡友元指的是友元函式和友元類。這點對於理解下面各種語法規則至關重要。**
### 方式一
```c++
template
class Stack {
...
void printOn(std::ostream &strm) const {
for (T const &elem : elems) {
strm << elem << ' '; // call << for each element
}
}
template
friend std::ostream &operator<<(std::ostream &, Stack const &);
};
template
std::ostream &operator<<(std::ostream &strm, Stack const &s) {
s.printOn(strm);
return strm;
}
int main() {
Stack s;
s.push("hello");
s.push("world");
std::cout << s;
return 0;
}
```
這裡在類裡宣告的友元函式使用的是與類模板不同的模板引數``,是因為友元函式的模板引數與類模板的模板引數不互相影響,**這可以理解為我們建立了一個新的函式模板。**
再舉一個友元類的例子:
```c++
template
class foo {
template
friend class bar;
};
```
**這裡也使用的是不同的模板引數。也就是:`bar`、`bar`、`bar`和其他任何型別的bar都是`foo`的友元。**
### 方式二
```c++
template
class Stack;
template
std::ostream& operator<< (std::ostream&, Stack const&);
template
class Stack {
...
friend std::ostream& operator<< (std::ostream&, Stack const&);
};
```
這裡提前聲明瞭Stack與`operator<<`,並且在類模板中,`operator<<`後面使用了``,沒有使用新的模板引數。**與第一種方式對比,這裡建立了一個特例化的非成員函式模板作為友元 (注意這個友元函式的宣告,是沒有``的 )。**
方式一中第二個友元類的例子用本方式寫是:
```c++
template
class bar;
template
struct foo {
friend class bar;
};
```
**對比的,這裡只有`bar`是`foo`的友元類。**
關於類模板友元規則有很多,知道有哪幾大類規則即可(Friend Classes of Class Templates、Friend Functions of Class Templates、Friend Templates),用到的時候再查也來得及。可以參考:《C++ Templates Second Edition》12.5小節。 (關注公眾號:紅宸笑。回覆:電子書 獲取pdf)
## 類模板的全特化
與函式模板類似,但是要注意的是,如果你想要全特化一個類模板,你必須全特化這個類模板的所有成員函式。
```c++
template <>
class Stack {
private:
std::deque 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::push(std::string const &elem) {
elems.push_back(elem); // append copy of passed elem
}
void Stack::pop() {
assert(!elems.empty());
elems.pop_back(); // remove last element
}
std::string const &Stack::top() const {
assert(!elems.empty());
return elems.back(); // return copy of last element
}
```
在類宣告的開始處,需要使用`template<>`並且表明類模板的全特化引數型別:
```c++
template<>
class Stack {
...
};
```
在成員函式中,需要將`T`替換成特化的引數型別:
```c++
void Stack::push (std::string const& elem) {
elems.push_back(elem); // append copy of passed elem
}
```
## 類模板的偏特化
類模板可以針對某一些特性場景進行部分特化,比如我們針對模板引數是指標進行偏特化:
```c++
// partial specialization of class Stack<> for pointers:
template
class Stack {
private:
std::vector 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
void Stack::push(T *elem) {
elems.push_back(elem); // append copy of passed elem
}
template
T *Stack::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
T *Stack::top() const {
assert(!elems.empty());
return elems.back(); // return copy of last element
}
```
注意類宣告與全特化的不同:
```c++
template
class Stack {
};
```
使用:
```c++
Stack ptrStack; // stack of pointers (special implementation)
ptrStack.push(new int{42});
```
### 多模板引數的偏特化
與函式模板過載類似,比較好理解。
原模板:
```c++
template
class MyClass {
...
};
```
過載:
```c++
// partial specialization: both template parameters have same type
template
class MyClass {
...
};
// partial specialization: second type is int
template
class MyClass {
...
};
// partial specialization: both template parameters are pointer types
template
class MyClass {
...
};
```
使用:
```c++
MyClass mif; // uses MyClass
MyClass mff; // uses MyClass
MyClass mfi; // uses MyClass
MyClass mp; // uses MyClass
```
同樣也會有過載衝突:
```c++
MyClass m; // ERROR: matches MyClass and MyClass
MyClass m; // ERROR: matches MyClass and MyClass
```
![image](https://user-images.githubusercontent.com/14103319/98329269-cfb2fe80-2032-11eb-9121-dc63f2bd0458.png)
## 預設模板引數
也與函式預設引數類似。比如我們為`Stack<>`增加一個預設引數,代表管理Stack元素的容器型別:
```c++
template >
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
void Stack::push(T const &elem) {
elems.push_back(elem); // append copy of passed elem
}
template
void Stack::pop() {
assert(!elems.empty());
elems.pop_back(); // remove last element
}
template
T const &Stack::top() const {
assert(!elems.empty());
return elems.back(); // return copy of last element
}
```
注意定義成員函式的模板引數變成了2個:
```c++
template
void Stack::push (T const& elem) {
elems.push_back(elem); // append copy of passed elem
}
```
使用:
```c++
// stack of ints:
Stack intStack;
// stack of doubles using a std::deque<> to manage the elements
Stack> dblStack;
```
## Type Aliases
### new name for complete type
兩種方式:typedef、using(c++11)
* typedef
```c++
typedef Stack IntStack;
void foo (IntStack const& s);
IntStack istack[10];
```
* using
```c++
using IntStack = Stack;
void foo (IntStack const& s);
IntStack istack[10];
```
### alias template
**using比typedef有一個很大的優勢是可以定義alias template:**
```c++
template
using DequeStack = Stack>; // stack of strings
int main() {
DequeStack ds;
return 0;
}
```
再強調一下,不可以將類模板宣告或定義在函式或者塊作用域內。通常類模板只能定義在global/namespace 作用域,或者是其它類的聲明裡面。
在之前函式模板文章中介紹過的`std::common_type_t`,實際上就是一個別名:
```c++
template using common_type_t = typename common_type<_Tp...>::type;
```
### Alias Templates for Member Types
* typedef:
```c++
struct C {
typedef ... iterator;
...
};
```
* using:
```c++
struct MyType {
using iterator = ...;
...
};
```
使用:
```c++
template
using MyTypeIterator = typename MyType::iterator; // typename必須有
MyTypeIterator pos;
```
#### 關鍵字typename
上面的註釋說明了:`typename MyType::iterator`裡的`typename`是必須的,因為這裡的typename代表後面緊跟的是一個定義在類內的型別,否則,`iterator`會被當成一個靜態變數或者列舉:
```c++
template class B {
public:
static int x; // 類內的靜態變數
using iterator = ...; // 類內定義的型別
};
template
int B::x = 20;
int main() {
std::cout << B::x; // 20
return 0;
}
```
### Using or Typedef
個人傾向使用using :
* using使用`=`,更符合看程式碼的習慣、更清晰:
```c++
typedef void (*FP)(int, const std::string&); // typedef
using FP = void (*)(int, const std::string&); // using
```
* 上面提到的,using定義`alias template`更方便。
![image](https://user-images.githubusercontent.com/14103319/98331118-d5aade80-2036-11eb-82ea-9de96afb0bef.png)
## 類模板的引數推導 Class Template Argument Deduction
或許你會覺得每次使用模板時都需要顯示的指明模板引數型別會多此一舉,如果類模板能像`auto`一樣自動推導模板型別就好了。在C++17中,這一想法變成了可能:如果建構函式能夠推匯出所有的模板引數,那麼我們就不需要顯示的指明模板引數型別。
```c++
Stack intStack1; // stack of strings
Stack intStack2 = intStack1; // OK in all versions
Stack intStack3 = intStack1; // OK since C++17
```
新增能推斷出類模型型別的建構函式:
```c++
template
class Stack {
private:
std::vector elems; // elements
public:
Stack () = default; //
Stack (T elem) : elems({std::move(elem)}) {}
...
};
```
使用:
```c++
Stack intStack = 80; // Stack deduced since C++17
```
之所以新增`Stack () = default; `是為了`Stack s;`這種預設構造不報錯。
### Deduction Guides
我們可以使用`Deduction Guides`來提供額外的模板引數推導規則,或者修正已有的模板引數推斷規則。
```c++
Stack(char const*) -> Stack;
Stack stringStack{"bottom"}; // OK: Stack deduced since C++17
```
更多規則和用法可以看:[Class template argument deduction (CTAD) (since C++17)](https://en.cppreference.com/w/cpp/language/class_template_argument_deduction)
(完)
朋友們可以關注下我的公眾號,獲得最及時的更新:
![](https://img2020.cnblogs.com/blog/1298604/202011/1298604-20201103132150036-885052