C++ Templates

第三章 类模板

预计阅读时间8 分钟 18 views

与函数相似,类也可以被一种或多种类型参数化。例如容器类,它们通常被用于管理某种特定的元素。只要使用类模板,就可以实现容器类,而不需要确定容器中元素的类型。

3.1 类模板 Stack 的实现

一个简单的类模板 Stack 实现如下:

#include <iostream>
#include <stdexcept>

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

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

template<typename T>
void Stack<T>::pop(){
    if(elems.empty()) throw std::out_of_range("Stack<>::pop: empty stack");
    elems.pop_back();
}

template<typename T>
T Stack<T>::top() const{
    if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty stack");
    return elems.back();
}
C++

上述的类模板 Stack<> 使用的是 C++ 标准库中的类模板 vector<> 来实现的,这样做的好处就是不需要亲自实现内存管理、拷贝构造函数和赋值运算符。

3.1.1 类模板的声明

类模板的声明和函数模板的声明基本相似,即在声明之前,先声明作为类型参数的标识符:

  • 使用 typename 关键字:
template<typename T>
class Stack{ };
C++
  • 使用 class 关键字:
template<class T>
class Stack{ };
C++

在类模板的内部, T 可以像其他任何类型一样,用于声明成员变量和成员函数,在下面的例子中:

  • T 被用于声明 std::vector 的元素类型
  • 声明 push() 是一个接受 const T& 为唯一实参的成员函数
  • 声明 top() 是返回类型 T的成员函数
template<typename T>
class Stack{
public:
    Stack();
    void push(T const&);
    void pop();
    T top() const;
private: std::vector<T> elems;
};
C++

这个类的类型是 Stack<T> ,其中 T 是模板参数。因此,当在声明中需要使用该类的类型时,就必须使用 Stack<T>, 例如要声明自己实现的拷贝构造函数和赋值运算符:

template<typename T>
class Stack{
    /* copy constructor */
    Stack(const Stack<T>&);
    /* copy asssignment */
    Stack<T>& operator=(const Stack<T>&);
}
C++

然而,当使用类名而不是类的类型时,就应该只用 Stack ,例如:指定类的名称、类的构造函数、析构函数。

3.1.2 成员函数的实现

为了定义类模板的成员函数,就需要指定该成员函数一个函数模板,而且还使用这个类模板的完整类型限定符。

  • 类型Stack<T>的成员函数push()的实现如下:
template<typename T>
void Stack<T>::push(const T& elem){
    elems.emplace_back(elem);
}
C++
  • 类型Stack<T>的成员函数pop()的实现如下:
template<typename T>
T Stack<T>::pop(){
    if(elems.empty()) throw std::out_of_range("Stack<>::pop: empty Stack");
    /* leave out Exception Security */
    T elem = elems.back();
    elems.pop_back();
    return elem;
}
C++
  • 类型Stack<T>的成员函数top()的实现如下:
template<typename T>
T Stack<T>::top const{
    if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty Stack");
    return elems.back();
}
C++

当然也可以直接将类模板的成员函数直接实现为内联函数,将其在类中实现:

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

3.2 类模板的 Stack 的使用

#include <iostream>
#include <stdexcept>

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

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

template<typename T>
void Stack<T>::pop(){
    if(elems.empty()) throw std::out_of_range("Stack<>::pop: empty stack");
    elems.pop_back();
}

template<typename T>
T Stack<T>::top() const{
    if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty stack");
    return elems.back();
}

int main(){
    try{
        Stack<int> intStack;
        Stack<std::string> stringStack;

        intStack.push(7);
        std::cout << intStack.top() << '\n';

        stringStack.push("hello");
        std::cout << stringStack.top() << '\n';
        stringStack.pop();
        stringStack.pop(); /* error */

    }catch(cosnt std::exception& ex){
        std::cerr << "Exception: " << ex.what() << '\n';
        return EXIT_FAILURE;
    }
}
C++

通过声明类型 Stack<int> ,在类模板内部就可以用 int 实例化 T ,因此, intStack 是一个创建自 Stack<int> 的对象, 它的元素存储于 'std::vector',且类型为 int,对于所有被调用的成员函数,都会实例化出基于 int 类型的函数,同理 Stack<std::string> 也如此。

  • 注:
    • 只有那些被调用的成员函数,才会产生这些函数的实例化代码。
    • 对于类模板,成员函数只有在被使用的时候才会被实例化。
    • 这样做的好处:
      • 节省空间和时间。
      • 对于那些“未能提供所有成员函数中所有操作的”类型,也可以使用该类型来实例化类模板,只要在模板内部不使用就可以。
      • 补充:什么是“未能提供某些操作的”成员函数:
          1. operator<
          2. operator==
          3. operator()
          4. ......
        • 如果在类模板中这些操作都未被实现,那么就无法使用(这些操作的默认实现无法应用在当前的模板类中)。

在上面的例子中,缺省构造函数、 push()top()都被实例化了一个 int 实例和 std::string 版本,而 pop() 仅被实例化了一个 std::string 版本。
此外,如果类模板中含有某些静态成员,那么用来实例化的每种类型,都会实例化这些静态成员。

可以像使用其他任何类型一样使用实例化后的类模板类型,只要它支持所调用的操作就可以:

void foo(const Stack<int>& s){
  Stack<int> istack[10];
  ...
}

typedef Stack<int> IntStack;
void foo(const IntStack& s){
  IntStack istack[10];
}
C++

使用 typedef 只是为 Stack<int> 取了一个“类型别名”,并没有定义一个新的类型。
因此,在定义了新的类型后:

typedef Stack<int> IntStack;
C++

IntStack 和 Stack<int> 仍是相同的类型,并可用于相互赋值。 模板实参可以是任何类型:

  • 浮点型指针: Stack<float*>
  • int类型的栈: Stack<Stack<int>>

唯一的要求就是:该类型必须提供被调用的所有操作,即所有操作符如==<>都需要被重载。

3.3 类模板的特化

和函数模板重载类似,可以用模板实参来特化类模板,通过特化类模板,可以优化基于某种特定类型的实现,或者解决某种特定类型在实例化模板时所出现的问题。
另外,如果要特化一个类模板,就还需要特化该类模板的所有成员函数,尽管也可以只特化某个成员函数,但这个做法并没有特化整个类,也就没有特化整个类模板。

为了特化一个类模板,需要在起始处声明一个 template<> ,再声明用来特化类模板的类型。这个类型被用作模板实参,且必须在类名的后面直接指定:

template<>
class Stack<std::string>{
  ...
};
...
C++

进行类模板的特化时,每个成员函数必须重新定义为普通函数,原来的模板函数中的每个 T 也相应地被进行特化的类型取代:

template<typename T>
class Stack{
public:
    /* declare functions */
    void push(T const&);
    void pop();
    T top() const;
    bool empty() const{ return elems.empty(); }

private:
    std::vector<T> elems;
};

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

template<typename T>
void Stack<T>::pop(){
    if(elems.empty()) throw std::out_of_range("Stack<>::pop(): empty stack");
    elems.pop_back();
}

template<typename T>
T Stack<T>::top() const{
    if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty Stack");
    return elems.back();
}

/* a specified template for std::stirng */
template<>
class Stack<std::string>{
public:
  void push(const std::string&);
  void pop();
  std::string top() const;
  bool empty() const { return elems.empty(); }
private: deque<std::string> elems;
}

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

void Stack<std::string>::pop(){
  if(elems.empty()) std::out_of_range("Stack<std::string>::pop(): empty Stack");
  elems.pop_back();
}

std::string Stack<std::string>::top const{
  if(elems.empty()) std::out_of_range("Stack<std::string>::top(): empty Stack");
  return elems.back();
}
C++

上面的例子说明了,特化的实现可以和基本类模板(primary template)的实现完全不同。

3.4 局部特化

类模板可以被局部特化,可以在特定的环境下指定类模板的特定实现,并且要求某些参数仍需用户自己定义,例如类模板:

template<typename T1, typename T2>
class MyClass{
  ...
};
C++

由上可以得到其他的几种局部特化:

  • 局部特化:两个模板参数具有相同的类型
template<typename T>
class MyClass<T, T>{
  ...
};
C++
  • 局部特化:第二个模板参数是其他类型,例如: int
template<typename T>
class MyClass<T, int>{
  ...
};
C++
  • 局部特化:两个模板参数都是指针类型
template<typename T>
class MyClass<T1*, T2*>{
  ...
};
C++
  • 特化后的类模板例子:
MyClass<int, float> mif;    // use MyClass<T1,T2>, two different types
MyClass<float, float> mff;  // use MyClass<T,T>, two same types
MyClass<float, int> mfi;    // use MyClass<T, int>, specified the second param with int
MyClass<int*, float*> mp;   // use MyClass<T1*, T2*>, two different types pointer
C++

如果多个局部特化同等程度地匹配某个声明,那么就称该声明具有二义性:

MyClass<int, int> m;    // error: as same as this matches MyClass<T, T> and MyClass<T, int>
MyClass<int*, int*> m;  // error: as same as this matches Myclass<T, T> and MyClass<T1*, T2*>
C++

解决第二种二义性,可以另外提供一个指向相同类型指针的特化:

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

3.5 缺省模板参数

对于类模板,还可以为模板参数定义缺省值,这些值被称为缺省模板实参,而且,它们还可以引用之前的模板参数。
例如,在类 Stack<> 中,可以把用于管理元素的容器定义为第 2 个模板参数,并且使用 std::vector<> 作为它的缺省值:

#include <vector>
#include <stdexcept>

template<typename T, typename CONT = std::vector<T>>
class Stack{
public:
  void push(const T&);
  void pop();
  T top() const;
  bool empty() const { return elems.empty(); }
private:
  // equivalent to
  // typedef
  // std::vector<int /* or other specific type */>
  // CONT
  CONT elems;
};

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

template<typename T, typename CONT>
void Stack<T, CONT>::pop(){
  if(elems.empty()) throw std::out_of_range("Stack<>::pop(): empty Stack");
  elems.pop_back();
}

template<typename T, typename CONT>
T Stack<T, CONT>::top() const{
  if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty Stack");
  return elems.back();
}
C++

在上面的代码中,类模板含有两个模板参数,因此每个成员函数的定义都必须具有这两个参数:

template<typename T, typename CONT>
void Stack<T, CONT>::push(const T& elem){
  elems.emplace_back(elem);
}
C++

如果只传递第一个类型实参给这个类模板,那么将会将会利用vector来管理stack的元素:

template<typename T,
         typename CONT =
              std::vector<T>>
class Stack{
  ...
private:
  CONT elems;
  ...
};
C++

当在程序中声明 Stack 对象的时候,还可以指定容器的类型:

#include <iostream>
#include <stdexcept>
#include <deque>
#include <cstdlib>
#include <vector>

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

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

template<typename T, typename CONT>
void Stack<T, CONT>::pop(){
  if(elems.empty()) throw std::out_of_range("Stack<>::pop(): empty Stack");
  elems.pop_back();
}

template<typename T, typename CONT>
T Stack<T, CONT>::top() const{
  if(elems.empty()) throw std::out_of_range("Stack<>::top(): empty Stack");
  return elems.back();
}

int main(){
  try{
    Stack<int> intStack;
    Stack<double, std::deque<dobule>> dblStack;

    intStack.push(7);
    std::cout << intStack.top() << '\n';
    intStack.pop();

    dblStack.push(42.42);
    std::cout << dblStack.top() << '\n';
    dblStack.pop();
    dblStack.pop();
  }catch(const std::exception& ex){
    std::cerr << "Exception: " << ex.what() << '\n';
    return EXIT_FAILURE;
  }
}
C++
  • 使用 Stack<double, std::deque<double>> 可以声明一个“元素类型为double, 并且使用std::deque<> 在内部管理元素”的栈。

3.6 小结

  • 类模板是具有如下性质的类:在类的实现中,可以有一个或多个类型还没有被指定。
  • 为了使用类模板,可以传入某个具体类型作为模板实参,然后编译器将会基于该类型来实例化类模板。
  • 对于类模板而言,只有那些被调用的成员函数才会被实例化。
  • 可以用某种特定类型特化类模板。
  • 可以用某种特定类型局部特化类模板。
  • 可以为类模板的参数定义缺省值,这些值还可以引用之前的模板参数。

Leave a Comment

Share this Doc

第三章 类模板

Or copy link

CONTENTS
It's late! Remember to rest.