c++11-17 模板核心知识(二)—— 类模板

朋友们可以关注下我的公众号,获得最及时的更新:

或者关注我的知乎账号 : https://www.zhihu.com/people/zhangyachen


类模板声明、实现与使用

声明:

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)

(完)

朋友们可以关注下我的公众号,获得最及时的更新:

posted @ 2020-11-08 23:23  张雅宸  阅读(658)  评论(0编辑  收藏  举报