模板是C++中泛型编程的基础,一个模板就是创建一个类或者函数的蓝图或者说公式。

C++模板分为函数模板和类模板。C++根据调用模板时传入的具体类型来生成相应类型的具体函数或者类。

类模板则可以是整个类是个模板,类的某个成员函数是个模板,以及类本身和成员函数分别是不同的模板。

1.函数模板

函数模板以关键字template开始,后接尖括号括起来的模板参数列表,模板参数列表不允许是空的,也即模板参数至少有一个或多个,多个之间使用逗号分割。

模板参数表示的是函数中用到的类型或者是一个值。当我们使用模板时,根据提供的实参推断出实参的类型,该类型即被用于绑定到模板参数,这个过程被叫做模板的实例化,相应的,生成的版本叫做模板的实例。

模板参数表示的是类型,该类型可以用于制定函数的返回类型或者函数的参数类型,也可以用于函数体内变量声明定义等。

对于每个模板参数列表中的每个类型参数,其前面必须加上关键字typename或class。关键字typename和class之间没有区别。

除了定义模板的类型参数,还可以定义一种非类型的参数。非类型参数不表示一种类型,而是表示一个值。非类型参数也不使用关键字typename和class,而是使用具体的类型来指定。

非类型参数在模板实例化时,被用户或者编译器推断出的值所代替,该值必须是常量表达式。(常量表达式是指值不会改变,并且编译期间就能计算得出结果的表达式,字面值属于常量表达式,常量表达式初始化的const对象也是常量表达式)。

例如:

template <unsigned N, unsigned M>
int fun(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}

这里的N,M将会被我们调用fun时传入的实参的值替代:

fun("hi", "mom");

编译器会使用字面常量的大小来替代N,M,并在字符串字面值末尾插入一个空字符作为终止标记,最终实例化为:

int fun(const char (&p1)[], const char (&p2)[]);

非类型参数可以是整形,也可以是指针或者引用。条件是整形是常量表达式,而指针和引用必须是关联static对象。

模板可以声明为inline和constexpr的。

当编译器对代码进行编译时,在源码模板定义部分,编译器并没有实际去生成相应的模板代码,只有在使用模板实例化一个具体的版本时,编译器才生成相应的代码。

2.类模板

与函数模板不同,类模板不能为其模板类型参数进行推断。为什么不能为类模板推断类型参数?因为定义类对象的时候,有可能无法提供足够的类型来让编译器进行推断,比如 vector v;这里仅仅定义了一个vector对象,没有提供任何额外的信息让编译器来推断模板参数,所以类模板不为其模板类型参数进行推断,必须我们在实例化时显式指明。

类模板的名字不是一个类,也即不是自定义的一个class,而是一个生成class的说明模板。因此使用模板生成的类,必定是带模板类型实参的class,所以一个实例化的class必然有<>来指明类型参数。

类模板的成员函数可以在内部定义,这样是隐式内联。也可以在外部定义。类模板的成员函数是一个普通的成员,而不是模板。虽然在外部定义时,成员函数需要以关键字template开始,并且后接模板参数列表,但这不表示该成员函数是个模板。之所以需要关键字template和模板参数列表,是因为成员函数所属的类在实例化时,会具体绑定到一个特定的类型上,成员函数也需要相应被动地绑定到该类型。

类模板的成员函数只有被用到的时候才会进行实例化,如果没有被用到,就不会实例化,如同类中定义的成员函数一样,如果不会被用到,那么可以只声明而不定义它。

当我们在类模板作用域内进行模板成员的定义时,可以省略模板实参。

template <typename T>
class BlobPtr
{
public:
BlobPtr& operator++(); //无需说明具体类名BlobPtr<T>
BlobPtr& operator--(); //无需说明具体类名BlobPtr<T>
BlobPtr operator--(int); //无需说明具体类名BlobPtr<T> bool empty()
{
return data->empty();
} size_t size(); private:
std::shared_ptr<std::vector<T>> data;
}; template <typename T>
size_t BlobPtr<T>::size() //类作用域外,需说明具体类名BlobPtr<T>
{
return data->size();
}

当我们在类模板的外面定义成员函数时,必须以关键字template开始,后接类模板参数列表。

这里为什么类成员函数size( )前要加模板参数列表<T>?template<typyname T>不已经指明了类型了吗?

前面已经说过:类模板的成员函数是一个普通的成员,而不是模板。这里的template <typename T>是说明size( ) 所属的BlobPtr是个模板类,并且BlobPtr

本身也不是一个类名,是类模板名,真正的类名是BlobPtr<T>,说白了,其实是在具体指明类作用域。所以在类外定义成员函数时,成员函数的参数列表对模板参数是可以省略的。

因此在类模板外面定义普通非模板成员函数时,首先用template <typename T>说明是一个类模板,然后再具体说明该成员函数是哪个类中的成员。后面会看到,如果模板类的成员函数也是一个模板,那么就需要分别各自说明类的模板和函数的模板。

3.类模板和友元

如果一个类模板包含一个非模板友元,则友元可以访问该类模板的所有实例。

如果类和友元都是模板,则类实例可以对友元所有实例授权,也可以只授权给特定实例。

为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。

在C++11中,我们也可以模板类型参数声明成友元。例如:

template <typename Type>
class Bar
{
friend Type;
// ...
};

新标准也允许我们为类模板定义一个类型别名:

template <typename T> using twin = pair<T, T>;
twin<string, string> authors;    //authors是一个pair<string, string>

模板参数遵循普通的作用域规则。与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字,需要注意的是,在模板内不能重用模板参数名。例如:

typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a;  //类型A隐藏外部typedef double A
double B;   //错误,不允许重用模板类型参数
}

模板声明必须包含模板参数。与函数参数相同,声明中的模板参数的名字不必与定义中相同。

对于一个给定的模板的声明和定义必须有相同数量和种类的参数。

关于typename可以用做模板参数的关键字,也可以用来指示类型还是变量名。具体可以参考这篇文章:http://feihu.me/blog/2014/the-origin-and-usage-of-typename/

4.类模板的static成员

与普通类一样,模板类也可以拥有static成员,如下:

template <typename T>
class Foo
{
public:
static std::size_t count() { return ctr; }  //声明并定义
private:
static std::size_t ctr;    //声明,尚未定义
};

上面这段代码,Foo是一个类模板,它实例化后的类有一个count的静态成员函数和一个静态ctr数据成员。

类的static数据成员有且只有一个定义,类模板也是如此。因此,我们将需要在类模板外定义ctr数据成员,类模板外定义静态数据成员的格式是template关键字开始,后跟模板参数列表,如下:

template <typename T>
size_t Foo<T>::ctr = ;

上面代码中类名是带模板参数的,因为实例化后的静态数据成员是具体属于某一个类的,而一个具体类则是带模板实参的,仅有类名则只是一个类模板的名字。

在新标准中,我们还可以为函数模板和类模板提供默认模板实参,对于类模板,当使用默认实参时,只需使用空的尖括号<>来表示即可。

5.成员模板

一个普通类或者类模板可以包含一个模板的成员函数。这种成员被称为成员模板。成员模板不允许是虚函数。

对于类模板,其类和成员有各自独立的模板参数。

与类模板的普通成员函数不同,成员模板是函数模板。当我们在类模板外面定义实现成员模板时,要同时提供类模板和成员模板的参数列表。其中,类模板的参数列表在前,成员模板的在后。例如:

template <typename T>
class Blob
{
template <typename It> Blob(It b, It e);
// ...
}; template <typename T>     //类模板的参数列表
template <typename It>   //成员模板参数列表
Blob<T>::Blob(It b, It e):data(std::make_shared<std::vector<T>>(b, e))
{
//constructor
}

对于含成员模板的类模板,在实例化时,需要同时提供类模板实参和成员模板实参,对于类模板实参,则是显式提供,对于成员模板的实参,则是自动推断。

6.控制实例化

模板只有被用到时,编译器才会根据实参进行实例化实参相应的代码,在不同源文件中提供相同实参实例化同一模板时,将会在不同文件中重复生成相同的实例,每个源文件中都会有一个实例。大系统中,这会导致严重的额外开销。

C++11新标准中,可以使用显式实例化来避免这种额外开销。方法是使用实例化声明和实例化定义。

形如:

extern template declaration;     //实例化声明
template declaration; //实例化定义 下面是实际的例子
extern template class Blob<string>;              // 声明
template int compare(const int&, const int&); // 定义

上面的代码中,第一行是声明,第二行是定义。需要注意的是,模板的实例化控制的声明和定义是配套使用的。

当编译器遇到extern模板声明时,编译器就不再实例化,它会去程序的其他地方寻找实例,可以多次声明,但只能一次定义。

由于编译器会在使用模板时自动实例化,因此extern声明必须在任何使用当前模板前面声明。

当编译器遇到一个模板的实例化控制的定义时,编译器将会进行实例化,以生成代码。

类模板的实例化定义会实例化模板的所有成员,而不是用到哪个成员才实例化哪个成员。

7.

C++ Primer 5th 第16章 模板与泛型编程的更多相关文章

  1. [C++ Primer] : 第16章: 模板与泛型编程

    面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况, 不同之处在于: OOP能处理类型在程序运行之前都未知的情况, 而在泛型编程中, 在编译时就能获知类型了. 函数模板 模板是C++ ...

  2. 【c++ Prime 学习笔记】第16章 模板与泛型编程

    面向对象编程(OOP)和泛型编程(GP)都能处理在编写程序时类型未知的情况 OOP能处理运行时获取类型的情况 GP能处理编译期可获取类型的情况 标准库的容器.迭代器.算法都是泛型编程 编写泛型程序时独 ...

  3. C++ Primer 5th 第12章 动态内存

    练习12.1:在此代码的结尾,b1 和 b2 各包含多少个元素? StrBlob b1; { StrBlob b2 = {"a", "an", "th ...

  4. C++ Primer 5th 第6章 函数

    正如第一章所说:C++的函数是一个能够完成一个功能的模块或者说是一段命名了的代码块. 如下图所示,函数可以重载,是一段实现某些功能命名了的代码. 一个完整的函数的构成有四部分: 1.返回类型 2.函数 ...

  5. C++ Primer 5th 第3章 字符串、向量和数组

    *****代码在Debian g++ 5.40 / clang++ 3.8(C++11)下编写调试***** 本章主要是关于字符串.数组的内容,以及一些简单的容器知识. 1.using的声明 usin ...

  6. C++ Primer 5th 第1章 开始

    *****代码在Ubuntu g++ 5.31 / clang++ 3.8(C++11)下编写调试***** 每个C++程序必须有一个main( )函数,main( )函数的返回值也必须是int类型, ...

  7. C++ Primer 5th 第9章 顺序容器

    练习9.1:对于下面的程序任务,vector.deque和list哪种容器最为适合?解释你的选择的理由.如果没有哪一种容器优于其他容器,也请解释理由.(a) 读取固定数量的单词,将它们按字典序插入到容 ...

  8. C++ Primer 5th 第7章 类

    类的基本思想是数据抽象和封装,定义类就是定义一个抽象数据类型. 类中的所有成员必须在类中声明,也即默认定义在类中的成员全部为声明,除非显式的定义成员函数的函数体.成员函数是在类中声明的,定义可以在类内 ...

  9. C++ Primer 5th 第2章 变量和基本类型

    *****代码在Debian g++ 5.3.1 / clang++ 3.8(C++11)下编写调试***** 由于部分编译器对标准遵循的不同以及自身额外的扩展,本章书中的少数知识点与实际实现存在偏差 ...

随机推荐

  1. Data 语义学(2)

    四."继承"与 Data Member (1). 只有继承没有多态 先来看个例子 #include <iostream> using namespace std; cl ...

  2. 【JavaScript】

    右键禁用.防止文字选中 .返回选中的文本 JavaScript 原理 Javascript高性能动画与页面渲染 前端不为人知的一面--前端冷知识集锦 屏幕外去计算值,position:absolute ...

  3. Android实现OCR扫描识别数字图片之图片扫描识别

    [Android实例] Android实现OCR扫描识别数字图片之图片扫描识别 Android可以识别和扫描二维码,但是识别字符串呢? google提供了以下解决方案用的是原来HP的相关资料. 可以吧 ...

  4. 动态规划——E (LIS())最长上升子序列

    E - LIS Time Limit:1000MS     Memory Limit:65536KB     64bit IO Format:%I64d & %I64u Submit Stat ...

  5. 动态规划——C编辑最短距离

    C - 编辑距离 时间限制: 1000女士 内存限制: 65536KB 64位输入输出格式: %I64d & %I64u 提交 状态 描述 Let x and y be two strings ...

  6. Vagrant网络配置

    Vagrant中网络配置 一.基本配置 Vagrant offers multiple options for how you are able to connect your guest machi ...

  7. poj3258

    题目翻译 二分法(其实两个单词的意思分别是河,跳格子游戏,至于为啥翻译成二分法- -只能说英语博大精深啊) 奶牛每年举办一场有特色的跳格子游戏(很明显题目翻译错误)涉及到在河里从一块岩石跳到另一块岩石 ...

  8. the identity used to sign the executable is no longer valid.解决方法

    the identity used to sign the executable is no longer valid.解决方法 一.重新下载Provisioning Profile 1.到devel ...

  9. linux —— 学习笔记(环境变量的设置)

    目录 环境变量概要 与环境变量相关的文件 设置环境变量 注意以及相关 1.环境变量概要 环境变量,简单来说,是储存了环境信息的变量.它可以让你在不指明全部路径的情况下执行某脚本或某应用程序,比如在 l ...

  10. android里Toast的用法

    在活动中,可以通过findViewById()方法获取到在布局文件中定义的元素,这里我们传入R.id.button_1,来得到按钮的实例,这个值是刚才在first_layout.xml中通过andro ...