c++11-17 模板核心知識(二)—— 類别範本

張雅宸發表於2020-11-08

類别範本宣告、實現與使用

宣告:

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

image

使用類别範本的部分成員函式

我們為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*>

image

預設模板引數

也與函式預設引數類似。比如我們為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更方便。

image

類别範本的引數推導 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)

(完)

朋友們可以關注下我的公眾號,獲得最及時的更新:

相關文章