模板基础知识

阅读经典——《C++ Templates》01

  1. 函数模板
  1. 类模板
  2. 非类型模板参数
  3. 一些技巧
  4. 模板代码的组织结构

一、函数模板

定义

template <typename T>
inline T const& max (T const& a, T const& b)
{
  return a < b ? b : a;
}

使用

max(7, 1);
max(7.1, 1.2);
max("mathematics", "math");

编译时,函数模板根据实参来确定模板参数T的类型,针对每一种类型实例化出不同的函数。由于max中使用了比较运算符operator<,因此类型T必须支持该操作,否则编译器报错。

模板参数不支持自动类型转换,例如,下面的调用会出错。

max(7, 1.2);    //wrong

编译器无法根据实参决定T的类型,因为71.2是两种不同的类型,这里不允许int自动转换为double。有一种妥协方案,显式指定T的类型,这时int可以转换为double

max<double>(7, 1.2);

找错误

下面的程序隐藏着一个惊天Bug,请把它找出来。

template <typename T1, typename T2>
inline T1 const& max (T1 const& a, T2 const& b)
{
    return a < b ? b : a;
}
max(4, 4.2);

答案见文末。

二、类模板

声明和定义

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

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

使用

Stack<int> intStack;
Stack<std::string> stringStack;
intStack.push(7);
stringStack.push("hello");

编译时,类模板根据模板参数实例化出相应的类对象和成员变量,而成员函数并不一定实例化,只有那些被调用了的成员函数才会被实例化。显然这样做可以节省空间,而且,对于那些“未能支持所有成员函数中的所有操作”的类型,只要不调用那些不支持的成员函数,就仍然可以使用。听起来有些抽象,举个例子,假如Stack中也有max操作,那么如果使用自定义类型Person作为Stack的模板参数,而且Person没有重载operator<运算符,那么该stack对象就不能访问max方法,若访问则编译器会报错。

局部特化

可以指定类模板的特定实现,并且要求某些模板参数仍然必须由用户来定义。

例如类模板:

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

就可以有下面几种局部特化:

//两个模板参数具有相同的类型
template <typename T>
class MyClass<T, T> {
    ...
};
//第2个模板参数的类型是int
template <typename T>
class MyClass<T, int> {
    ...
};
//两个模板参数都是指针类型
template <typename T1, typename T2>
class MyClass<T1*, T2*> {
    ...
};

缺省模板实参

可以为模板参数定义缺省值。例如,在Stack<>类中把用于存放元素的容器类型定义为第2个模板参数,并使用std::vector<>作为缺省值。

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

三、非类型模板参数

模板参数并不一定是属于typename的类型,也可以是普通值,称为非类型模板参数。例如,我们可以把栈容量MAXSIZE作为Stack<>的一个非类型模板参数,并用它初始化数组大小。

template <typename T, int MAXSIZE>
class Stack {
  private:
    T elems[MAXSIZE];
  ...
};

使用方式如下:

Stack<int, 20> int20Stack;
Stack<int, 40> int40Stack;
Stack<std::string, 40> stringStack;

非类型模板参数只能是常整数(包括枚举)或指向外部链接对象的指针。(关于外部链接对象的概念请参考...)

四、一些技巧

关键字typename
typename最初用于指定模板内部的标识符是一个类型,例如:

template <typename T>
class MyClass {
    typename T::SubType* ptr;
    ...
};

如果不加typenameSubType会被认为是T的一个静态成员,而不会被认为是一个内部类型。

成员模板
如果类的成员函数也是独立的模板函数,则称之为成员模板。例如,给Stack<>类增加一个赋值操作符operator=成员模板函数。

//声明
template <typename T>
class Stack {
  ...
  public:
    ...
    template <typename T2>
    Stack<T>& operator= (Stack<T2> const&);
};
//定义
...
template <typename T>
  template <typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
    ...
}

模板的模板参数
当模板参数也是一个模板的时候,情况就变得复杂了。例如,Stack<T, CONT>中,模板参数CONT就是一个模板,我们需要传入std::vector<T>作为实参。但是这样做无法约束vector的模板参数TStack的第一个模板参数T一致,有可能出错。这种情况下使用模板的模板参数更合适。

template <typename T, template <typename ELEM> class CONT = std::deque>
class Stack {
  private:
    CONT<T> elems;
  ...
};

使用时不必传入容器类的模板参数,它会自动根据Stack类的模板参数决定。

Stack<int> intStack;  //使用缺省模板参数
Stack<float, std::vector> floatStack;  //使用vector<float>作为容器

模板的模板参数只能使用class作为关键字,因为只有类可以作为模板的模板参数。函数模板不支持模板的模板参数。

零初始化
未初始化的基本数据类型通常具有一个不确定(undefined)值。因此建议采用如下写法:

template <typename T>
void foo()
{
    T x;  //不建议这样写,如果T是基本数据类型,那么x本身是一个不确定的值
    T x = T();  //建议这样写,如果T是基本数据类型,那么x是0或者false
}

五、模板代码的组织结构

我们通常把声明写在.h文件中,把定义写在.cpp文件中。然而这种惯例被模板打破了。

如果把函数模板的声明和定义分别写在两个文件中,链接器将会报错,提示找不到函数的定义。这是因为,函数模板还没有实例化,也就是说,函数模板的定义所在的文件并没有被编译,因为编译器不知道应该使用哪个模板参数来实例化。因此,通常的做法是,把函数模板的声明和定义全部放在头文件中。

//myfirst2.h
#ifndef MYFIRST_H
#define MYFIRST_H

#include <iostream>
#include <typeinfo>

//模板声明
template <typename T>
void print_typeof(T const&);

//模板的实现/定义
template <typename T>
void print_typeof(T const& x)
{
    std::cout << typeid(x).name() << std::endl;
}

#endif

模板代码的这种组织结构称为包含模型。除此之外,还有显式实例化、分离模型等组织结构,但都不常用,特别是分离模型(使用export关键字导出模板)已经被c++标准委员会废除。

找错误答案

template <typename T1, typename T2>
inline T1 const& max (T1 const& a, T2 const& b)
{
    return a < b ? b : a;
}
max(4, 4.2);

T1intT2double,返回值类型为int。由于需要返回的数是double类型的b,因此需要一个从doubleint的转换,这次转换将会创建一个临时int型变量作为返回值。而由于返回类型为引用,导致该函数返回后将持有一个临时局部变量的引用,一旦临时变量被释放,继续使用这个引用将得到意想不到的结果,甚至引起程序崩溃。

解决方案是返回值不使用引用。这个问题很容易出现,与此类似的还有返回局部变量的指针,因此编程时需要多加注意。

关注作者文集《C++ Templates》,第一时间获取最新发布文章。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容