类模板 Class Templates

本系列是对 C++ Templates The Complete Guide Second Edition 的学习和解读。本文介绍类模板。

和函数模板类似,类也可以实现部分类型的参数化。C++ 标准库的容器类就是类模板很好的例子。

类模板的实现

本文以数据结构中的栈(Stack)为例介绍类模板。

#include <vector>
#include <cassert>
template<typename T>
class Stack {
  private:
    std::vector<T> elems;
  public:
    void push(T const& elem);
    void pop();
    T const& top() const;
    bool empty() const {
      return elems.empty();
    }
};

template<typename T>
void Stack<T>::push (T const& elem)
{
  elems.push_back(elem);
}

template<typename T>
void Stack<T>::pop ()
{
  assert(!elems.empty());
  elems.pop_back();
}

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

本例中,Stack 的类模板实现借助了标准库的 vector<>,节省了内存管理、构造函数、析构函数等的实现,可以聚焦于类模板的语法。

类模板的申明和函数模板的申明类似。

template<typename T>
class Stack {
  ...
};

也可以使用关键字 class 代替 typename,但是为了避免混淆,建议优先使用 typename

template<class T>
class Stack {
  ...
};

在模板类内部,模板参数 T 可以像普通类型那样用于申明成员变量和成员函数。Stack 类的类型是 Stack<T>,因此,除了模板参数可以被推导的场景外,任何使用该类的类型的地方都必须使用 Stack<T>。但是,在类模板内部,可以省去类名后的模板参数。例如,你需要申明自己的拷贝构造函数和赋值操作:

emplate<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);

在类外定义成员函数时,必须要使用 template<Typename T> 指明这是一个模板。例如:

template<typename T>
void Stack<T>::push (T const& elem)
{
  elems.push_back(elem); // append copy of passed elem
}

类模板的使用

在 C++17 之前,使用类模板对象的时候,必须显示指定模板参数。

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<float*> floatPtrStack; // stack of float pointers
Stack<Stack<int>> intStackStack; // stack of stack of ints

但是,需要注意的是: 在 C++11 之前,两个右尖括号之间必须留有空白:

Stack<Stack<int> > intStackStack;  // OK with all C++ versions
Stack<Stack<int>> intStackStack;   // ERROR before C++11

类模板的成员函数只有在被调用的时候才实例化。 这可以节省时间和空间,也使得允许模板类的部分使用(partial usage)。

类模板的部分使用

前面也提到过,类模板的成员变量只在需要的时候(被调用)才会实例化,也即支持类模板的部分使用。例如:

template<typename T>
class Stack {
  ...
  void printOn() (std::ostream& strm) const {
    for (T const& elem : elems) {
      strm << elem << ’ ’; // call << for each element
    }
  }
};

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

因为类模板成员函数只有在调用的时候才实例化,因而 ps.printOn(std::cout); 语句会产生编译报错。

友元

一般使用重载 << 的方式实现类的打印功能。可以通过友元的方式重载 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;
  }
};

如果在类内声明,类外定义,情况稍有复杂。如下实现,则编译报错:

template<typename T> 
class Stack {
  ...
  friend std::ostream& operator<<(std::ostream&, const Stack<T>);
};

std::ostream& operator<<(std::ostream& os,
  const Stack<T>& stack) 
{
  stack.printOn(os);
  return os;
}

因为类外定义时, T 不可见。一般有两种解决方案。

  1. 声明一个新的模板函数,使用不同的模板参数。
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;
}
  1. 前置申明类模板和友元函数模板。
template<typename T> // operator<<中参数中要求Stack模板可见
class Stack;

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

// 随后就可以将其声明为友元
template<typename T> 
class Stack {// 加上<>是强制为模板函数,不加就是普通函数
   friend std::ostream& operator<< <T> (std::ostream&, const Stack<T>&);
   // friend std::ostream& operator<< <T> (std::ostream&, const Stack<T>&); is ok!
};

template<typename T>
std::ostream& operator<<(std::ostream& os, const Stack<T>& stack)
{
  stack.printOn(os);
  return os;
}

这种方式,需要在函数名 operator<< 后加 <T><> 表明是函数模板。

int main() {
  Stack<int> is; 
  is.push(3);
  is.push(4);
  std::cout << is << std::endl;
  Stack<std::pair<int,int>> ps; 
  ps.push({4, 5});
  ps.push({6, 7});
  std::cout << ps << std::endl; // ERROR
  return 0;
}

同样地,友元函数模板只有被调用时才实例化,元素没有定义 operator<< 时也可以使用这个类,只有调用 operator<< 时才会出错。

类模板特例化

类似于函数模板的重载,类模板可以使用特定的类型来特例化。类模板的特例化可以用于优化特定类型的实现,也可以用于修复特定类型实例化的错误行为。

一旦实例化了一个类模板,你需要实例化它所有的成员函数。虽然可以实例化类模板的一个成员函数,一旦你这样做了,你将不能再实例化这个类模板了。

实例化一个类模板,需要 template<> 打头。类外定义成员函数就可以像 ”普通“ 函数那样了。

#include <deque>
#include <string>
#include <cassert>
template<>
class Stack<std::string> {
  private:
    std::deque<std::string> elems;
  public:
    void push(std::string const&);
    void pop();
    std::string const& top() const;
    bool empty() const {
      return elems.empty();
    }
};

void Stack<std::string>::push (std::string const& elem)
{
  elems.push_back(elem);
}

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

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

类模板偏特化

类模板可以部分特例化,也即偏特化。偏特化介于特例化完全模板之间,可以针对某些类型特殊实现,又可以保留一部分模板参数交给用户定义。例如针对指针类型的 Stack 实现:

// partial specialization of class Stack<> for pointers:
template<typename T>
class Stack<T*> {
  private:
    std::vector<T*> elems;
  public:
    void push(T*);
    T* pop();
    T* top() const;
    bool empty() const {
      return elems.empty();
    }
};

template<typename T>
void Stack<T*>::push (T* elem)
{
  elems.push_back(elem);
}

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

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

该例为指针类型的 Stack 偏特化。偏特化可以提供不同的接口。例如 pop() 返回了指针,当指针用 new 创建时,用户可以使用 delete 清除。

模板偏特化也可能产生二义性。例如:

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*>

解决最后一个二义性的问题,可以提供两个类型相同指针的偏特化:

template<typename T>
class MyClass<T*,T*> {
  ...
};

类模板默认参数

和函数模板类似,类模板也可以指定默认模板参数。例如:

template<typename T, typename Cont = std::vector<T>>
class Stack {
 public: 
  void push(const T& x);
  void pop();
  const T& top() const;
  bool empty() const;
 private:
  Cont v;
};

template<typename T, typename Cont>
void Stack<T, Cont>::push(const T& x)
{
  v.emplace_back(x);
}

template<typename T, typename Cont>
void Stack<T, Cont>::pop()
{
  assert(!v.empty());
  v.pop_back();
}

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

template<typename T, typename Cont>
bool Stack<T, Cont>::empty() const
{
  return v.empty();
}

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

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

类型别名

Typedefs 和别名申明(Alias Declarations)

  1. 使用 typedef
typedef Stack<int> IntStack;  // typedef
void foo (IntStack const& s); // s is stack of ints
IntStack istack[10];          // istack is array of 10 stacks of ints
  1. 使用 using(C++11)
using IntStack = Stack<int>;  // alias declaration
void foo (IntStack const& s); // s is stack of ints
IntStack istack[10];          // istack is array of 10 stacks of ints

别名模板

别名申明(Alias declarations)还可以模板化,被称为别名模板(Alias Templates)。

template<typename T>
using DequeStack = Stack<T, std::deque<T>>;

DequeStack<int>Stack<int, std::deque<int>> 代表相同的类型。

成员类型的别名模板

template<typename T>
struct MyType {
  using iterator = typename std::vector<T>::iterator;
};

template<typename T>
using MyTypeIterator = typename MyType<T>::iterator;

MyTypeIterator<int> pos;

C++14 标准库的 type traits 就使用了上述方法定义了类型简称。

typename std::add_const<T>::type  // C++11
std::add_const_t<T> // C++14

namespace std { 
  template<typename T> using add_const_t = typename add_const<T>::type;
}

类模板参数推导

C++17 之前,你必须显示指定类模板的参数(除非有默认值),但是 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 {
 public:
  Stack(T const& elem) : elems({elem}) {} 
 private:
  std::vector<T> elems;
};

Stack intStack = 0; // Stack<int> deduced since C++17

原则上,你也可以使用一个字面值字符串初始化 Stack

Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17

但这里有点问题:用引用传递模板类型 T 的实参时,模板参数不会 decay,最终得到的类型是原始数组类型 Stack<char const[7]>,导致我们不能 push 一个不同大小的字符串到 Stack 中。

传值的话则不会有这种问题,模板实参会 decay,原始的数组类型会转换为指针。

template<typename T> 
class Stack {
 public:
  Stack(T elem) : elems({elem}) {}
 private:
  std::vector<T> elems;
};

Stack stringStack = "bottom"; // Stack<const char*> deduced since C++17

并且,这里最好使用 std::move 避免不必要的拷贝。

template<typename T> 
class Stack {
 public:
  Stack(T elem) : elems({std::move(elem)}) {}
 private:
  std::vector<T> elems;
};

Deduction Guides

为了禁止为容器类推断原始字符指针,可以为模板参数推导提供推导指南(Deduction Guides)。例如,当传入字符串字面值或 C 风格的字符串时,Stack 被实例化为 std::string 类型:

Stack(char const*) -> 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

因为 Stack 被实例化为:

class Stack {
 public:
  Stack(const std::string& elem) : elems({elem}) {}
 private:
  std::vector<std::string> elem;
};

符串字面值常量类型为 const char[7],而构造函数期望的是 std::string,无法拷贝初始化。还是需要如下初始化:

Stack stringStack{"bottom"};

下面都是调用了拷贝构造函数初始化 Stack

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

模板化聚合类

聚合类(Aggregate classes)(所有成员都是public、没有定义构造函数、没有类内初始值、没有基类、也没有虚函数)也可以模板化:

template<typename T>
struct ValueWithComment {
  T value;
  std::string comment;
};

ValueWithComment<int> vc;
vc.value = 42;
vc.comment = "initial value";

C++17 可以为聚合类指定模板参数推导指南。例如:

ValueWithComment(char const*, char const*)
  -> ValueWithComment<std::string>;
ValueWithComment vc2 = {"hello", "initial value"};

没有推导指南,聚合类无法初始化,因为聚合类没有构造函数。标准库 std::array<> 也是一个聚合类,C++ 标准库也是为它指定了模板参数推导指南。

以上,就是本文的全部内容,更多的模板知识,尽情期待!

参考

  • http://www.tmplbook.com
  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值