模板基础知识

阅读经典——《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》,第一时间获取最新发布文章。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容