类模板示例

  1. template <typename T>
  2. class Stack
  3. {
  4. public:
  5. Stack();
  6. Stack(const Stack<T> &); // T是同一类型的类模板才能拷贝
  7. Stack<T> &operator=(const Stack<T> &);
  8. void push(const T &);
  9. void pop();
  10. const T &top() const;
  11. bool empty() const;
  12. private:
  13. std::vector<T> v;
  14. };
  15. template <typename T>
  16. Stack<T>::Stack()
  17. {
  18. }
  19. template <typename T>
  20. Stack<T>::Stack(const Stack<T> &rhs) : v(rhs.v)
  21. {
  22. }
  23. template <typename T>
  24. Stack<T> &Stack<T>::operator=(const Stack<T> &rhs)
  25. {
  26. v = rhs.v;
  27. return *this;
  28. }
  29. template <typename T>
  30. void Stack<T>::push(const T &x)
  31. {
  32. v.emplace_back(x);
  33. }
  34. template <typename T>
  35. void Stack<T>::pop()
  36. {
  37. assert(!v.empty());
  38. v.pop_back();
  39. }
  40. template <typename T>
  41. const T &Stack<T>::top() const
  42. {
  43. assert(!v.empty());
  44. return v.back();
  45. }
  46. template <typename T>
  47. bool Stack<T>::empty() const
  48. {
  49. return v.empty();
  50. }
  51. int main()
  52. {
  53. using IntStack = Stack<int>; // typedef Stack<int> IntStack
  54. IntStack intStack; // Stack<int> intStack
  55. intStack.push(42);
  56. std::cout << intStack.top(); // 42
  57. Stack<std::string> stringStack;
  58. stringStack.push("hi");
  59. std::cout << stringStack.top(); // hi
  60. stringStack.pop();
  61. }
  • 模板实参可以是任何类型

    1. Stack<double*> doublePtrStack;
    2. Stack<Stack<int>> intStackStack;
  • 成员函数只有被调用到时才实例化

  • 如果类模板有static数据成员,每种实例化类型都会实例化static数据成员。static成员函数和数据成员只被同类型共享 ```cpp template class A { public: static std::size_t count(); private: static std::size_t n; };

template std::size_t A::n = 0;

A a; // 实例化A::n A b, c, d; // 实例化A::n,bcd共享A::count()和A::n std::size_t n = A::count(); // 实例化A::count() n = b.count(); // 使用A::count() n = A::count(); // 错误:必须指定模板参数,否则无法得知实例化版本

  1. <a name="dbc254ac"></a>
  2. ## 类模板的部分实例化
  3. - 由于成员函数只有被调用到时才实例化,模板实参只要提供必要的操作,而非所有需要的操作。如Stack提供一个printOn对每个元素调用`operator<<`,即使没有对元素定义`operator<<`也能使用这个类。只有调用printOn时才会产生错误,因为这时不能对这些元素实例化`operator<<`
  4. ```cpp
  5. template<typename T>
  6. class Stack {
  7. ...
  8. void printOn(std::ostream&) const;
  9. };
  10. template<typename T>
  11. void Stack<T>::printOn(std::ostream& os) const
  12. {
  13. for (const T& x : v) os << x << ' ';
  14. }
  15. Stack<std::pair<int, int>> s; // std::pair没有定义operator<<
  16. s.push({1, 2}); // OK
  17. s.push({3, 4}); // OK
  18. std::cout << s.top().first << s.top().second; // 34
  19. s.printOn(std::cout); // 错误:元素类型不支持operator<<

友元非成员函数

  • 与其使用printOn函数打印元素,不如重载operator<<,然而通常operator<<会实现为非成员函数。下面在类内定义友元,它是一个普通函数

    template<typename T> 
    class Stack {
    ...
    void printOn(std::ostream& os) const;
    friend std::ostream& operator<<(std::ostream& os, const Stack<T>& stack)
    {
      stack.printOn(os); 
      return os;
    }
    };
    
  • 如果在类外定义友元,类模板参数不可见,事情会复杂很多 ```cpp template class Stack { … friend std::ostream& operator<<(std::ostream&, const Stack); };

std::ostream& operator<<(std::ostream& os, const Stack& stack) // 错误:类模板参数T不可见 { stack.printOn(os); return os; }


- 有两个解决方案,一是**隐式声明一个新的函数模板,并使用不同的模板参数**
```cpp
template<typename T> 
class Stack {
  … 
  template<typename U> 
  friend std::ostream& operator<<(std::ostream&, const Stack<U>&);
};

// 类外定义
template<typename U>
std::ostream& operator<<(std::ostream& os, const Stack<U>& stack)
{
  stack.printOn(os);
  return os;
}
  • 二是将友元前置声明为模板,而友元参数中包含类模板,这样就必须先前置声明类模板 ```cpp template // operator<<中参数中要求Stack模板可见 class Stack;

template std::ostream& operator<<(std::ostream&, const Stack&);

// 随后就可以将其声明为友元 template class Stack { … friend std::ostream& operator<< (std::ostream&, const Stack&); };

// 类外定义 template std::ostream& operator<<(std::ostream& os, const Stack& stack) { stack.printOn(os); return os; }


- 同样,函数只有被调用到时才实例化,元素没有定义`operator<<`时也可以使用这个类,只有调用`operator<<`时才会出错
```cpp
Stack<std::pair<int, int>> s; // std::pair没有定义operator<<
s.push({1, 2}); // OK
s.push({3, 4}); // OK
std::cout << s.top().first << s.top().second; // 34
std::cout << s << '\n'; // 错误:元素类型不支持operator<<

特化与偏特化

特化:模板参数不指定,直接将类型T替换成具体的类型:

template<>
class Stack<std::string> {
 public:
  void push(const std::string&);
  void pop();
  std::string const& top() const;
  bool empty() const;
 private:
  std::deque<std::string> v;
};

void Stack<std::string>::push(const std::string& x)
{
  v.emplace_back(x);
}

void Stack<std::string>::pop()
{
  assert(!v.empty());
  v.pop_back();
}

const std::string& Stack<std::string>::top() const
{
  assert(!v.empty());
  return v.back();
}

bool Stack<std::string>::empty() const
{
  return v.empty();
}

偏特化:指定模板参数,不直接使用T类型,内部使用的类型T的特殊形式或其他组合形式:

// 针对指针类型的偏特化
template<typename T>
class Stack<T*> {
 public:
  void push(T*);
    T* pop();
  T* top() const;
    bool empty() const;
 private:
  std::vector<T*> v;
};

template<typename T>
void Stack<T*>::push(T* x)
{
  v.emplace_back(x);
}

template<typename T>
T* Stack<T*>::pop()
{
  assert(!v.empty());
    T* p = v.back();
    v.pop_back();
    return p;
}

template<typename T>
T* Stack<T*>::top() const
{
  assert(!v.empty());
    return v.back();
}

template<typename T>
bool Stack<T*>::empty() const
{
  return v.empty();
}
  • 特化可以提供一个轻微不同的实现,比如这里的pop返回存储的指针,所以当指针用new创建时,类模板的用户能delete

    Stack<int*> s;
    s.push(new int{42});
    std::cout << *s.top(); 
    delete s.pop();
    
  • 类模板也能特化多个模板参数之间的关系 ```cpp template class A {};

// 偏特化:两个模板参数有相同类型 template class A {};

// 偏特化:第二个模板参数类型为int template class A {};

// 偏特化:两个模板参数都是指针类型 template class A {};

A a; // A A b; // A A c; // A A d; // A


**注意**:多个偏特化匹配程度相同时,将产生二义性错误
```cpp
A<int, int> e; // 错误:同时匹配A<T, T>和A<T, int>
A<int*, int*> f; // 错误:同时匹配A<T, T>和A<T1*, T2*>

要解决第二个二义性错误,可以再提供一个两个相同指针类型的偏特化

template<typename T> 
class A<T*, T*>
{};

类模板默认实参

  • 类模板也可以指定默认的模板实参 ```cpp template> class Stack { public: void push(const T& x); void pop(); const T& top() const; bool empty() const; private: Cont v; };

template void Stack::push(const T& x) { v.emplace_back(x); }

template void Stack::pop() { assert(!v.empty()); v.pop_back(); }

template const T& Stack::top() const { assert(!v.empty()); return v.back(); }

template bool Stack::empty() const { return v.empty(); }

int main() { Stack intStack; intStack.push(1); std::cout << intStack.top(); // 1 intStack.pop();

Stack> doubleStack; doubleStack.push(3.14); std::cout << doubleStack.top(); // 3.14 dblStack.pop(); }


<a name="51c4a473"></a>
## 类型别名
```cpp
using IntStack = Stack<int>; // typedef Stack<int> IntStack;
void f(const IntStack&);
IntStack s[10]; // 元素为10个Stack<int>的数组
  • 别名模板在定义类模板成员的类型简称时十分有用 ```cpp template struct MyType { using iterator = typename std::vector::iterator; };

template using Iter = typename MyType::iterator;

// 对于下面这个使用 typename MyType::iterator it; // 可改写为 Iter it;


- C++14中用这种方法为所有的[type traits](https://zh.cppreference.com/w/cpp/header/type_traits)定义了简称
```cpp
// C++11中的
typename std::add_const<T>::type
// 在C++14中可以简写为
std::add_const_t<T>
// 标准库的定义中为
namespace std { 
template<typename T>
using add_const_t = typename add_const<T>::type;
}

类模板实参推断

  • C++17开始,如果构造函数能推断出所有模板参数(没有默认值),就不用显式指定模板实参

    Stack<int> s1;
    Stack<int> s2 = s1; // OK in all versions
    Stack s3 = s1; // OK since C++17
    
  • 提供一个传递初始化实参的构造函数,可支持单个元素类型的推断 ```cpp template class Stack { public: Stack() = default; Stack(const T& x) : v({x}) {} // 单元素初始化 private: std::vector v; };

Stack intStack = 0; // Stack deduced since C++17


- 原则上也可以传递字符串字面值常量,但这样会造成许多麻烦。用引用传递模板类型T的实参时,模板参数不会decay,最终得到的类型是原始数组类型
```cpp
Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17
  • 传值的话则不会有这种问题,模板实参会decay,原始数组类型会转换为指针 ```cpp template class Stack { public: Stack(T x) : v({x}) {} private: std::vector v; };

Stack stringStack = “bottom”; // Stack deduced since C++17


- 传值时最好使用[std::move](https://zh.cppreference.com/w/cpp/utility/move)以避免不必要的拷贝
```cpp
template<typename T> 
class Stack {
 public:
  Stack(T x) : v({std::move(x)}) {}
 private:
  std::vector<T> v;
};
  • 除了传值,还有一种方法是禁止为容器类推断原始字符指针,可以定义deduction guide来提供对现有模板实参额外的推断,这样传递的字符串字面值常量或C风格字符串都将实例化为std::string

    Stack(const char*) -> Stack<std::string>;
    Stack stringStack{"bottom"}; // OK: Stack<std::string> deduced since C++17
    
  • 但下面这种仍无法工作

    Stack stringStack = "bottom"; // Stack<std::string> deduced, but still not valid
    
  • 因为推断std::string实例化了一个Stack<std::string>如下

    class Stack {
    public:
    Stack(const std::string& x) : v({x}) {}
    private:
    std::vector<std::string> v;
    };
    
  • 字符串字面值常量类型为const char[7],而构造函数期望的是std::string,所以必须初始化如下

    Stack stringStack{"bottom"}; // Stack<std::string> deduced and valid
    
  • 注意,下列初始化都调用拷贝构造函数声明相同类型,而不是初始化一个元素是stringStack的stack

    Stack s1(stringStack); // Stack<std::string> deduced
    Stack s2{stringStack}; // Stack<std::string> deduced
    Stack s3 = {stringStack}; // Stack<std::string> deduced
    

    模板化聚合

  • 聚合类也能作为模板

    template<typename T> 
    struct A {
    T x;
    std::string s;
    };
    
  • 这样可以为了参数化值而定义一个聚合,它可以像其他类模板一样声明对象,同时当作一个聚合使用

    A<int> a;
    a.x = 42;
    a.s = "initial value";
    
  • C++17中可以为聚合类模板定义deduction guide ```cpp template struct A { T x; std::string s; };

A(const char, const char) -> A;

int main() { A a = { “hi”, “initial value” }; std::cout << a.x; // hi }


- 没有deduction guide,初始化就无法进行,因为A没有构造函数来推断。[std::array](https://zh.cppreference.com/w/cpp/container/array)也是一个聚合,元素类型和大小都是参数化的,C++17为其定义了一个deduction guide
```cpp
namespace std {
template<typename T, typename... U> array(T, U...)
  -> array<enable_if_t<(is_same_v<T, U> && ...), T>, (1 + sizeof...(U))>;
}

std::array a{ 1, 2, 3, 4 };
// 等价于
std::array<int, 4> a{ 1, 2, 3, 4 };