Skip to content

面向对象编程基础

本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org

ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book

第16章

模板与泛型编程

模板与泛型编程

面向对象编程OOP和泛型编程
- 都能处理在编写程序时不知道类型的情况
- 不同之处在于
    - OOP能处理类型在程序运行之前都未知的情况
    - 而在泛型编程中在编译时就能获知类型了

容器迭代器和算法都是泛型编程的例子
当我们编写一个泛型程序时是独立于任何特定类型来编写代码的
当使用一个泛型程序时我们提供类型或值程序实例可在其上运行
- 标准库为每个容器提供了单一的泛型的定义如vector
  我们可以使用这个泛型定义来定义很多类型的vector
  它们的差异就在于包含的元素类型不同

模板与泛型编程

模板是泛型编程的基础
我们不必了解模板是如何定义的就能使用它们实际上我们已经这样用了
在本章中我们将学习如何定义自己的模板

模板是C++中泛型编程的基础一个模板就是一个创建类或函数的蓝图或者说公式
当使用一个vector这样的泛型类型或者find这样的泛型函数时
- 我们提供足够的信息将蓝图转换为特定的类或函数这种转换发生在编译时
我们已经学习了如何使用模板在本章中我们将学习如何定义模板

定义模板

假定我们希望编写一个函数来比较两个值并指出第一个值是小于等于还是大于第二个值
在实际中我们可能想要定义多个函数每个函数比较一种给定类型的值
我们的初次尝试可能定义多个重载函数
// returns 0 if the values are equal, -1 if v1 is smaller, 1 if v2 is smaller
int compare(const string &v1, const string &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}
int compare(const double &v1, const double &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}

定义模板

这两个函数几乎是相同的唯一的差异是参数的类型函数体则完全一样

如果对每种希望比较的类型都不得不重复定义完全一样的函数体是非常烦琐且容易出错的
更麻烦的是在编写程序的时候我们就要确定可能要compare的所有类型
如果希望能在用户提供的类型上使用此函数这种策略就失效了

函数模板

我们可以定义一个通用的函数模板function template),
而不是为每个类型都定义一个新函数
一个函数模板就是一个公式可用来生成针对特定类型的函数版本
compare的模板版本可能像下面这样
template <typename T>
int compare(const T &v1, const T &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}
模板定义以关键字template开始后跟一个模板参数列表template parameterlist
这是一个逗号分隔的一个或多个模板参数template parameter的列表
用小于号<和大于号>包围起来

在模板定义中模板参数列表不能为空

函数模板

模板参数列表的作用很像函数参数列表
函数参数列表定义了若干特定类型的局部变量但并未指出如何初始化它们
在运行时调用者提供实参来初始化形参
类似的模板参数表示在类或函数定义中用到的类型或值
当使用模板时我们隐式地或显式地指定模板实参template argument),
- 将其绑定到模板参数上

我们的compare函数声明了一个名为T的类型参数
在compare中我们用名字T表示一个类型
而T表示的实际类型则在编译时根据compare的使用情况来确定

实例化函数模板

当我们调用一个函数模板时编译器通常用函数实参来为我们推断模板实参
当我们调用compare时编译器使用实参的类型来确定绑定到模板参数T的类型
例如在下面的调用中
cout << compare(1, 0) << endl;       // T is int
实参类型是int编译器会推断出模板实参为int并将它绑定到模板参数T

编译器用推断出的模板参数来为我们实例化instantiate一个特定版本的函数
当编译器实例化一个模板时它使用实际的模板实参代替对应的模板参数来创建出
模板的一个新实例”。例如给定下面的调用
// instantiates int compare(const int&, const int&)
cout << compare(1, 0) << endl;       // T is int
// instantiates int compare(const vector<int>&, const vector<int>&)
vector<int> vec1{1, 2, 3}, vec2{4, 5, 6};
cout << compare(vec1, vec2) << endl; // T is vector<int>
编译器会实例化出两个不同版本的compare

实例化函数模板

cout << compare(1, 0) << endl;       // T is int
cout << compare(vec1, vec2) << endl; // T is vector<int>
对于第一个调用编译器会编写并编译一个compare版本其中T被替换为int
int compare(const int &v1, const int &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}
对于第二个调用编译器会生成另一个compare版本其中T被替换为vector<int>
这些编译器生成的版本通常被称为模板的实例instantiation)。

模板类型参数

我们的compare函数有一个模板类型参数type parameter)。
一般来说我们可以将类型参数看作类型说明符就像内置类型或类类型说明符一样使用
特别是类型参数可以用来指定返回类型或函数的参数类型
以及在函数体内用于变量声明或类型转换
// ok: same type used for the return type and parameter
template <typename T> T foo(T* p){
    T tmp = *p; // tmp will have the type to which p points
    // ...
    return tmp;
}
类型参数前必须使用关键字class或typename
// error: must precede U with either typename or class
template <typename T, U> T calc(const T&, const U&);
在模板参数列表中这两个关键字的含义相同可以互换使用
一个模板参数列表中可以同时使用这两个关键字
template <typename T, class U> calc (const T&, const U&);
看起来用关键字typename来指定模板类型参数比用class更为直观
毕竟我们可以用内置非类类型作为模板类型实参
而且typename更清楚地指出随后的名字是一个类型名
但是typename是在模板已经广泛使用之后才引入C++语言的某些程序员仍然只用class

非类型模板参数

除了定义类型参数还可以在模板中定义非类型参数nontype parameter)。
一个非类型参数表示一个值而非一个类型
我们通过一个特定的类型名而非关键字class或typename来指定非类型参数

当一个模板被实例化时非类型参数被一个用户提供的或编译器推断出的值所代替
这些值必须是常量表达式从而允许编译器在编译时实例化模板

例如我们可以编写一个compare版本处理字符串字面常量
这种字面常量是const char的数组由于不能拷贝一个数组所以我们将自己的参数
定义为数组的引用由于我们希望能比较不同长度的字符串字面常量因此为模板定
义了两个非类型的参数
第一个模板参数表示第一个数组的长度第二个参数表示第二个数组的长度
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}

非类型模板参数

当我们调用这个版本的compare时compare("hi", "mom")
编译器会使用字面常量的大小来代替N和M从而实例化模板
记住编译器会在一个字符串字面常量的末尾插入一个空字符作为终结符
因此编译器会实例化出如下版本
int compare(const char (&p1)[3], const char (&p2)[4])

一个非类型参数可以是一个整型或者是一个指向对象或函数类型的指针或左值引用
绑定到非类型整型参数的实参必须是一个常量表达式
绑定到指针或引用非类型参数的实参必须具有静态的生存期
我们不能用一个普通非static局部变量或动态对象作为指针或引用非类型模板参数的实参
指针参数也可以用nullptr或一个值为0的常量表达式来实例化

在模板定义内模板非类型参数是一个常量值
在需要常量表达式的地方可以使用非类型参数
例如指定数组大小

非类型模板参数的模板实参必须是常量表达式

inline和constexpr的函数模板

函数模板可以声明为inline或constexpr的如同非模板函数一样
inline或constexpr说明符放在模板参数列表之后返回类型之前
// ok: inline specifier follows the template parameter list
template <typename T> inline T min(const T&, const T&);
// error: incorrect placement of the inline specifier
inline template <typename T> T min(const T&, const T&);

编写类型无关的代码

编写泛型代码的两个重要原则:· 
- 模板中的函数参数是const的引用。· 
- 函数体中的条件判断仅使用<比较运算
通过将函数参数设定为const的引用我们保证了函数可以用于不能拷贝的类型
大多数类型包括内置类型和我们已经用过的标准库类型除unique_ptr和IO类型之外),
都是允许拷贝的但是不允许拷贝的类类型也是存在的
通过将参数设定为const的引用保证了这些类型可以用我们的compare函数来处理
而且如果compare用于处理大对象这种设计策略还能使函数运行得更快

编写类型无关的代码

你可能认为既使用<运算符又使用>运算符来进行比较操作会更为自然
// expected comparison
if (v1 < v2) return -1;
if (v1 > v2) return 1;
return 0;
但是如果编写代码时只使用<运算符我们就降低了compare函数对要处理的类型的要求
这些类型必须支持<但不必同时支持>
实际上如果我们真的关心类型无关和可移植性可能需要用less来定义我们的函数
// version of compare that will be correct even if used on pointers; see § 14.8.2
template <typename T> int compare(const T &v1, const T &v2)
{
    if (less<T>()(v1, v2)) return -1;
    if (less<T>()(v2, v1)) return 1;
    return 0;
}
原始版本存在的问题是如果用户调用它比较两个指针且两个指针未指向相同的数组
则代码的行为是未定义的
less<T>的默认实现用的就是<所以这其实并未起到让这种比较有一个良好定义的作用)。
模板程序应该尽量减少对实参类型的要求

模板编译

当编译器遇到一个模板定义时它并不生成代码
只有当我们实例化出模板的一个特定版本时编译器才会生成代码
当我们使用而不是定义模板时编译器才生成代码
- 这一特性影响了我们如何组织代码以及错误何时被检测到

通常当我们调用一个函数时编译器只需要掌握函数的声明
类似的当我们使用一个类类型的对象时类定义必须是可用的
但成员函数的定义不必已经出现因此我们将类定义和函数声明放在头文件中
而普通函数和类的成员函数的定义放在源文件中

模板则不同为了生成一个实例化版本编译器需要掌握函数模板或类模板成员函数的定义
因此与非模板代码不同模板的头文件通常既包括声明也包括定义

函数模板和类模板成员函数的定义通常放在头文件中

关键概念:模板和头文件

模板包含两种名字:·
- 那些不依赖于模板参数的名字· 
- 那些依赖于模板参数的名字
当使用模板时所有不依赖于模板参数的名字都必须是可见的这是由模板的提供者来保证的
而且模板的提供者必须保证当模板被实例化时
- 模板的定义包括类模板的成员的定义也必须是可见的

用来实例化模板的所有函数类型以及与类型关联的运算符的声明都必须是可见的
这是由模板的用户来保证的

通过组织良好的程序结构恰当使用头文件这些要求都很容易满足模板的设计者
应该提供一个头文件包含模板定义以及在类模板或成员定义中用到的所有名字的声明
模板的用户必须包含模板的头文件以及用来实例化模板的任何类型的头文件

大多数编译错误在实例化期间报告

模板直到实例化时才会生成代码这一特性影响了我们何时才会获知模板内代码的编译错误
通常编译器会在三个阶段报告错误
- 第一个阶段是编译模板本身时
  在这个阶段编译器通常不会发现很多错误编译器可以检查语法错误
  例如忘记分号或者变量名拼错等但也就这么多了
- 第二个阶段是编译器遇到模板使用时在此阶段编译器仍然没有很多可检查的
  对于函数模板调用编译器通常会检查实参数目是否正确它还能检查参数类型是否匹配
  对于类模板编译器可以检查用户是否提供了正确数目的模板实参但也仅限于此了
- 第三个阶段是模板实例化时只有这个阶段才能发现类型相关的错误
  依赖于编译器如何管理实例化这类错误可能在链接时才报告
当我们编写模板时代码不能是针对特定类型的
但模板代码通常对其所使用的类型有一些假设
//例如,我们最初的compare函数中的代码就假定实参类型定义了<运算符。
if (v1 < v2) return -1;  // requires < on objects of type T
if (v2 < v1) return 1;   // requires < on objects of type T
return 0;                // returns int; not dependent on T
当编译器处理此模板时它不能验证if语句中的条件是否合法

大多数编译错误在实例化期间报告

如果传递给compare的实参定义了<运算符则代码就是正确的否则就是错误的例如
Sales_data data1, data2;
cout << compare(data1, data2);// error: no < on Sales_data
此调用实例化了compare的一个版本将T替换为Sales_data
if条件试图对Sales_data对象使用<运算符但Sales_data并未定义此运算符
此实例化生成了一个无法编译通过的函数版本
但是这样的错误直至编译器在类型Sales_data上实例化compare时才会被发现

保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。

练习

给出实例化的定义

当编译器实例化一个模版时
它使用实际的模版参数代替对应的模版参数来创建出模版的一个新实例”。

练习

编写并测试你自己版本的 compare 函数
template<typename T>
int compare(const T& lhs, const T& rhs)
{
    if (lhs < rhs) return -1;
    if (rhs < lhs) return 1;
    return 0;
}

练习

对两个 Sales_data 对象调用你的 compare 函数
观察编译器在实例化过程中如何处理错误

error: no match for 'operator<' 

练习

编写行为类似标准库 find 算法的模版
函数需要两个模版类型参数一个表示函数的迭代器参数另一个表示值的类型
使用你的函数在一个 vector<int> 和一个list<string>中查找给定值
template<typename Iterator, typename Value>
Iterator find(Iterator first, Iterator last, const Value& v)
{
    for ( ; first != last && *first != value; ++first);
    return first;
}

练习

为6.2.4节中的print函数编写模版版本
它接受一个数组的引用能处理任意大小任意元素类型的数组
template<typename Array>
void print(const Array& arr)
{
    for (const auto& elem : arr)
        std::cout << elem << std::endl;
}

练习

你认为接受一个数组实参的标准库函数 begin  end 是如何工作的
定义你自己版本的 begin  end
template<typename T, unsigned N>
T* begin(const T (&arr)[N])
{
    return arr;
}

template<typename T, unsigned N>
T* end(const T (&arr)[N])
{
    return arr + N;
}

练习

编写一个 constexpr 模版返回给定数组的大小
template<typename T, size_t N> constexpr
unsigned size(const T (&arr)[N])
{
    return N;
}

练习

在第97页的关键概念我们注意到
C++程序员喜欢使用 != 而不喜欢 < 解释这个习惯的原因

因为大多数类只定义了 != 操作而没有定义 < 操作
使用 != 可以降低对要处理的类型的要求

类模板

类模板class template是用来生成类的蓝图的
与函数模板的不同之处是编译器不能为类模板推断模板参数类型
如我们已经多次看到的为了使用类模板我们必须在模板名后的尖括号中提供额外信息
 用来代替模板参数的模板实参列表

定义类模板

作为一个例子我们将实现StrBlob的模板版本
我们将此模板命名为Blob意指它不再针对string
- 类似StrBlob我们的模板会提供对元素的共享且核查过的访问能力
- 与类不同我们的模板可以用于更多类型的元素

与标准库容器相同当使用Blob时用户需要指出元素类型
类似函数模板类模板以关键字template开始后跟模板参数列表
在类模板及其成员的定义中我们将模板参数当作替身
代替使用模板时用户需要提供的类型或值

定义类模板

template <typename T> class Blob {
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;
    // constructors
    Blob();
    Blob(std::initializer_list<T> il);
    // number of elements in the Blob
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    // add and remove elements
    void push_back(const T &t) {data->push_back(t);}
    // move version; see § 13.6.3 
    void push_back(T &&t) { data->push_back(std::move(t)); }
    void pop_back();
    // element access
    T& back();
    T& operator[](size_type i); // defined in § 14.5

定义类模板

private:
    std::shared_ptr<std::vector<T>> data;
    // throws msg if data[i] isn't valid
    void check(size_type i, const std::string &msg) const;
};
我们的Blob模板有一个名为T的模板类型参数用来表示Blob保存的元素的类型
例如我们将元素访问操作的返回类型定义为T&
当用户实例化Blob时T就会被替换为特定的模板实参类型
除了模板参数列表和使用T代替string之外此类模板的定义与12.1.1节中定义的
类版本及12.1.6节和第13章第14章中更新的版本是一样的

实例化类模板

我们已经多次见到当使用一个类模板时我们必须提供额外信息
我们现在知道这些额外信息是显式模板实参explicit template argument列表
它们被绑定到模板参数编译器使用这些模板实参来实例化出特定的类

例如为了用我们的Blob模板定义一个类型必须提供元素类型
Blob<int> ia;                // empty Blob<int>
Blob<int> ia2 = {0,1,2,3,4}; // Blob<int> with five elements
//ia和ia2使用相同的特定类型版本的Blob(即Blob<int>)。
从这两个定义编译器会实例化出一个与下面定义等价的类
template <> class Blob<int> {
    typedef typename std::vector<int>::size_type size_type;
    Blob();
    Blob(std::initializer_list<int> il);
    // ...
    int& operator[](size_type i);
private:
    std::shared_ptr<std::vector<int>> data;
    void check(size_type i, const std::string &msg) const;
};

实例化类模板

当编译器从我们的Blob模板实例化出一个类时它会重写Blob模板
将模板参数T的每个实例替换为给定的模板实参在本例中是int

对我们指定的每一种元素类型编译器都生成一个不同的类
// these definitions instantiate two distinct Blob types
Blob<string> names; // Blob that holds strings
Blob<double> prices;// different element type
这两个定义会实例化出两个不同的类
- names的定义创建了一个Blob类每个T都被替换为string
- prices的定义生成了另一个Blob类T被替换为double

一个类模板的每个实例都形成一个独立的类
类型Blob<string>与任何其他Blob类型都没有关联
也不会对任何其他Blob类型的成员有特殊访问权限

在模板作用域中引用模板类型

为了阅读模板类代码应该记住类模板的名字不是一个类型名
类模板用来实例化类型而一个实例化的类型总是包含模板参数的

可能令人迷惑的是一个类模板中的代码如果使用了另外一个模板
通常不将一个实际类型或值的名字用作其模板实参
相反的我们通常将模板自己的参数当作被使用模板的实参
例如我们的data成员使用了两个模板vector和shared_ptr
我们知道无论何时使用模板都必须提供模板实参
在本例中我们提供的模板实参就是Blob的模板参数因此data的定义如下
std::shared_ptr<std::vector<T>> data;
它使用了Blob的类型参数来声明data是一个shared_ptr的实例
此shared_ptr指向一个保存类型为T的对象的vector实例
当我们实例化一个特定类型的Blob例如Blob<string>data会成为
shared_ptr<vector<string>>
如果我们实例化Blob<int>则data会成为shared_ptr<vector<int>>依此类推

类模板的成员函数

与其他任何类相同我们既可以在类模板内部也可以在类模板外部为其定义成员函数
且定义在类模板内的成员函数被隐式声明为内联函数

类模板的成员函数本身是一个普通函数但是类模板的每个实例都有其自己版本的成员函数
因此类模板的成员函数具有和模板相同的模板参数
因而定义在类模板之外的成员函数就必须以关键字template开始后接类模板参数列表

与往常一样当我们在类外定义一个成员时必须说明成员属于哪个类
而且从一个模板生成的类的名字中必须包含其模板实参当我们定义一个成员函数时
模板实参与模板形参相同对于StrBlob的一个给定的成员函数
ret-type StrBlob::member-name(parm-list)
对应的Blob的成员应该是这样的
template <typename T>
ret-type Blob<T>::member-name(parm-list)

check和元素访问成员

我们首先定义check成员它检查一个给定的索引
template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
    if (i >= data->size())
        throw std::out_of_range(msg);
}
除了类名中的不同之处以及使用了模板参数列表外
此函数与原StrBlob类的check成员完全一样

check和元素访问成员

下标运算符和back函数用模板参数指出返回类型其他未变
template <typename T>
T& Blob<T>::back()
{
    check(0, "back on empty Blob");
    return data->back();
}
template <typename T>
T& Blob<T>::operator[](size_type i)
{
    // if i is too big, check will throw, preventing access to a nonexistent element
    check(i, "subscript out of range");
    return (*data)[i];
}
在原StrBlob类中这些运算符返回string&而模板版本则返回一个引用
指向用来实例化Blob的类型

check和元素访问成员

pop_back函数与原StrBlob的成员几乎相同
template <typename T> void Blob<T>::pop_back()
{
    check(0, "pop_back on empty Blob");
    data->pop_back();
}
在原StrBlob类中下标运算符和back成员都对const对象进行了重载
我们将这些成员及front成员的定义留作练习

Blob构造函数

与其他任何定义在类模板外的成员一样构造函数的定义要以模板参数开始
template <typename T>
Blob<T>::Blob(): data(std::make_shared<std::vector<T>>()) { }
这段代码在作用域Blob<T>中定义了名为Blob的成员函数类似StrBlob的默认构造函数
此构造函数分配一个空vector并将指向vector的指针保存在data中
如前所述我们将类模板自己的类型参数作为vector的模板实参来分配vector

Blob构造函数

类似的接受一个initializer_list参数的构造函数将其类型参数T
作为initializer_list参数的元素类型
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il):
              data(std::make_shared<std::vector<T>>(il)) { }
类似默认构造函数此构造函数分配一个新的vector
在本例中我们用参数il来初始化此vector为了使用这个构造函数
我们必须传递给它一个initializer_list其中的元素必须与Blob的元素类型兼容
Blob<string> articles = {"a", "an", "the"};
这条语句中构造函数的参数类型为initializer_list<string>
列表中的每个字符串字面常量隐式地转换为一个string

类模板成员函数的实例化

默认情况下一个类模板的成员函数只有当程序用到它时才进行实例化
例如下面代码
Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};
// instantiates Blob<int>::size() const
for (size_t i = 0; i != squares.size(); ++i)
    squares[i] = i*i; // instantiates Blob<int>::operator[](size_t)
实例化了Blob<int>类和它的三个成员函数
operator[]size和接受initializer_list<int>的构造函数

如果一个成员函数没有被使用则它不会被实例化成员函数只有在被用到时才进行实例化
这一特性使得即使某种类型不能完全符合模板操作的要求我们仍然能用该类型实例化类

默认情况下对于一个实例化了的类模板其成员只有在使用时才被实例化

在类代码内简化模板类名的使用

当我们使用一个类模板类型时必须提供模板实参但这一规则有一个例外
在类模板自己的作用域中我们可以直接使用模板名而不提供实参
// BlobPtr throws an exception on attempts to access a nonexistent element
template <typename T> class BlobPtr
public:
    BlobPtr(): curr(0) { }
    BlobPtr(Blob<T> &a, size_t sz = 0):
            wptr(a.data), curr(sz) { }
    T& operator*() const
    { auto p = check(curr, "dereference past end");
      return (*p)[curr];  // (*p) is the vector to which this object points
    }
    // increment and decrement
    BlobPtr& operator++();        // prefix operators
    BlobPtr& operator--();

在类代码内简化模板类名的使用

private:
    // check returns a shared_ptr to the vector if the check succeeds
    std::shared_ptr<std::vector<T>>
        check(std::size_t, const std::string&) const;
    // store a weak_ptr, which means the underlying vector might be destroyed
    std::weak_ptr<std::vector<T>> wptr;
    std::size_t curr;      // current position within the array
};
细心的读者可能已经注意到BlobPtr的前置递增和递减成员返回BlobPtr&
而不是BlobPtr<T>&当我们处于一个类模板的作用域中时
编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样
就好像我们这样编写代码一样
BlobPtr<T>& operator++();
BlobPtr<T>& operator--();

在类模板外使用类模板名

当我们在类模板外定义其成员时必须记住我们并不在类的作用域中
直到遇到类名才表示进入类的作用域
// postfix: increment/decrement the object but return the unchanged value
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
    // no check needed here; the call to prefix increment will do the check
    BlobPtr ret = *this;  // save the current value
    ++*this;    // advance one element; prefix ++ checks the increment
    return ret;  // return the saved state
}

在类模板外使用类模板名

由于返回类型位于类的作用域之外我们必须指出返回类型是一个实例化的BlobPtr
它所用类型与类实例化所用类型一致在函数体内我们已经进入类的作用域
因此在定义ret时无须重复模板实参
如果不提供模板实参则编译器将假定我们使用的类型与成员实例化所用类型一致
因此ret的定义与如下代码等价
BlobPtr<T> ret = *this;

在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。

类模板和友元

当一个类包含一个友元声明时类与友元各自是否是模板是相互无关的
如果一个类模板包含一个非模板友元则友元被授权可以访问所有模板实例
如果友元自身是模板类可以授权给所有友元模板实例也可以只授权给特定实例

一对一友好关系

类模板与另一个类或函数模板间友好关系的最常见的形式是
建立对应实例及其友元间的友好关系
例如我们的Blob类应该将BlobPtr类和一个模板版本的Blob相等运算符(练习中
为StrBlob定义的定义为友元

为了引用类或函数模板的一个特定实例我们必须首先声明模板自身
一个模板声明包括模板参数列表
// forward declarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob; // needed for parameters in operator==
template <typename T>
    bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
    // each instantiation of Blob grants access to the version of
    // BlobPtr and the equality operator instantiated with the same type
    friend class BlobPtr<T>;
    friend bool operator==<T>
           (const Blob<T>&, const Blob<T>&);
    // other members as in § 12.1.1
};

一对一友好关系

我们首先将BlobBlobPtr和operator==声明为模板
这些声明是operator==函数的参数声明以及Blob中的友元声明所需要的
友元的声明用Blob的模板形参作为它们自己的模板实参
因此友好关系被限定在用相同类型实例化的Blob与BlobPtr相等运算符之间
Blob<char> ca; // BlobPtr<char> and operator==<char> are friends
Blob<int> ia;  // BlobPtr<int> and operator==<int> are friends
BlobPtr<char>的成员可以访问ca或任何其他Blob<char>对象的非public部分
但ca对ia或任何其他Blob<int>对象或Blob的任何其他实例都没有特殊访问权限

通用和特定的模板友好关系

一个类也可以将另一个模板的每个实例都声明为自己的友元或者限定特定的实例为友元
// forward declaration necessary to befriend a specific instantiation of a template
template <typename T> class Pal;
class C {  //  C is an ordinary, nontemplate class
    friend class Pal<C>;  // Pal instantiated with class C is a friend to C
    // all instances of Pal2 are friends to C;
    // no forward declaration required when we befriend all instantiations
    template <typename T> friend class Pal2;
};
template <typename T> class C2 { // C2 is itself a class template
    // each instantiation of C2 has the same instance of Pal as a friend
    friend class Pal<T>;  // a template declaration for Pal must be inscope
    // all instances of Pal2 are friends of each instance of C2, prior declaration needed
    template <typename X> friend class Pal2;
    // Pal3 is a nontemplate class that is a friend of every instance of C2
    friend class Pal3;    // prior declaration for Pal3 not needed
};
为了让所有实例成为友元友元声明中必须使用与类模板本身不同的模板参数

令模板自己的类型参数成为友元

在新标准中我们可以将模板类型参数声明为友元
template <typename Type> class Bar {
friend Type; // grants access to the type used to instantiate Bar
    //  ...
};
此处我们将用来实例化Bar的类型声明为友元
因此对于某个类型名FooFoo将成为Bar<Foo>的友元
Sales_data将成为Bar<Sales_data>的友元依此类推

值得注意的是虽然友元通常来说应该是一个类或是一个函数
但我们完全可以用一个内置类型来实例化Bar
这种与内置类型的友好关系是允许的以便我们能用内置类型来实例化Bar这样的类

模板类型别名

类模板的一个实例定义了一个类类型与任何其他类类型一样
我们可以定义一个typedef来引用实例化的类
      typedef Blob<string> StrBlob;
这条typedef语句允许我们运行在12.1.1节中编写的代码
而使用的却是用 string 实例化的模板版本的 Blob
由于模板不是一个类型我们不能定义一个typedef引用一个模板
无法定义一个typedef引用Blob<T>

但是新标准允许我们为类模板定义一个类型别名
      template<typename T> using twin = pair<T, T>;
      twin<string> authors; // authors是一个pair<string, string>
在这段代码中我们将twin定义为成员类型相同的pair的别名
这样twin的用户只需指定一次类型

模板类型别名

一个模板类型别名是一族类的别名
      twin<int> win_loss;  // win_loss是一个pair<int, int>
      twin<double> area;   // area是一个pair<double, double>
就像使用类模板一样当我们使用twin时需要指出希望使用哪种特定类型的twin
当我们定义一个模板类型别名时可以固定一个或多个模板参数
      template <typename T> using partNo = pair<T, unsigned>;
      partNo<string> books; // books是一个pair<string, unsigned>
      partNo<Vehicle> cars; // cars是一个pair<Vehicle, unsigned>
      partNo<Student> kids; // kids是一个pair<Student, unsigned>
这段代码中我们将 partNo 定义为一族类型的别名
这族类型是 second 成员为unsigned的pair
partNo的用户需要指出pair的first成员的类型但不能指定second成员的类型

类模板的static成员

与任何其他类相同类模板可以声明static成员
template <typename T> class Foo {
public:
   static std::size_t count() { return ctr; }
   // other interface members
private:
   static std::size_t ctr;
   // other implementation members
};
Foo是一个类模板它有一个名为count的public static成员函数
和一个名为ctr的private static数据成员每个Foo的实例都有其自己的static成员实例
对任意给定类型 X都有一个 Foo<X>::ctr 和一个 Foo<X>::count成员
所有Foo<X>类型的对象共享相同的ctr对象和count函数例如
// instantiates static members Foo<string>::ctr and Foo<string>::count
Foo<string> fs;
// all three objects share the same Foo<int>::ctr and Foo<int>::count members
Foo<int> fi, fi2, fi3;

类模板的static成员

与任何其他static数据成员相同模板类的每个static数据成员必须有且仅有一个定义
但是类模板的每个实例都有一个独有的static对象
因此与定义模板的成员函数类似我们将static数据成员也定义为模板
template <typename T>
size_t Foo<T>::ctr = 0; // define and initialize ctr
与类模板的其他任何成员类似定义的开始部分是模板参数列表
随后是我们定义的成员的类型和名字
与往常一样成员名包括成员的类名对于从模板生成的类来说类名包括模板实参
因此当使用一个特定的模板实参类型实例化 Foo 
将会为该类类型实例化一个独立的ctr并将其初始化为0

类模板的static成员

与非模板类的静态成员相同我们可以通过类类型对象来访问一个类模板的static成员
也可以使用作用域运算符直接访问成员
当然为了通过类来直接访问static成员我们必须引用一个特定的实例
Foo<int> fi;                 // instantiates Foo<int> class
                             // and the static data member ctr
auto ct = Foo<int>::count(); // instantiates Foo<int>::count
ct = fi.count();             // uses Foo<int>::count
ct = Foo::count();           // error: which template instantiation?
类似任何其他成员函数一个static成员函数只有在使用时才会实例化

练习

什么是函数模版什么是类模版

一个函数模版就是一个公式可用来生成针对特定类型的函数版本
类模版是用来生成类的蓝图的
与函数模版的不同之处是编译器不能为类模版推断模版参数类型
如果我们已经多次看到为了使用类模版我们必须在模版名后的尖括号中提供额外信息

练习

当一个类模版被实例化时会发生什么

一个类模版的每个实例都形成一个独立的类

练习

下面 List 的定义是错误的应如何修改它
template <typename elemType> class ListItem;
template <typename elemType> class List {
public:
    List<elemType>();
    List<elemType>(const List<elemType> &);
    List<elemType>& operator=(const List<elemType> &);
    ~List();
    void insert(ListItem *ptr, elemType value);
private:
    ListItem *front, *end;
};

练习

模版需要模版参数应该修改为如下
template <typename elemType> class ListItem;  
template <typename elemType> class List{  
public:  
      List<elemType>();  
      List<elemType>(const List<elemType> &);  
      List<elemType>& operator=(const List<elemType> &);  
      ~List();  
      void insert(ListItem<elemType> *ptr, elemType value);  
private:  
      ListItem<elemType> *front, *end;  
};

练习

编写你自己版本的 Blob  BlobPtr 模版包含书中未定义的多个const成员
//Blob:
#include <memory>
#include <vector>

template<typename T> class Blob
{
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;

    // constructors
    Blob();
    Blob(std::initializer_list<T> il);

    // number of elements in the Blob
    size_type size() const { return data->size(); }
    bool      empty() const { return data->empty(); }

练习

    void push_back(const T& t) { data->push_back(t); }
    void push_back(T&& t) { data->push_back(std::move(t)); }
    void pop_back();

    // element access
    T& back();
    T& operator[](size_type i);

    const T& back() const;
    const T& operator [](size_type i) const;

private:
    std::shared_ptr<std::vector<T>> data;
    // throw msg if data[i] isn't valid
    void check(size_type i, const std::string &msg) const;
};

练习

// constructors
template<typename T>
Blob<T>::Blob() : data(std::make_shared<std::vector<T>>())
{}

template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) :
data(std::make_shared<std::vector<T>>(il))
{}

template<typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
    if (i >= data->size())
        throw std::out_of_range(msg);
}

练习

template<typename T>
T& Blob<T>::back()
{
    check(0, "back on empty Blob");
    return data->back();
}

template<typename T>
const T& Blob<T>::back() const
{
    check(0, "back on empty Blob");
    return data->back();
}

练习

template<typename T>
T& Blob<T>::operator [](size_type i)
{
    // if i is too big, check function will throw, preventing access to a nonexistent element
    check(i, "subscript out of range");
    return (*data)[i];
}


template<typename T>
const T& Blob<T>::operator [](size_type i) const
{
    // if i is too big, check function will throw, preventing access to a nonexistent element
    check(i, "subscript out of range");
    return (*data)[i];
}

template<typename T>
void Blob<T>::pop_back()
{
    check(0, "pop_back on empty Blob");
    data->pop_back();
}

练习

//BlobPtr:
#include "Blob.h"
#include <memory>
#include <vector>

template <typename> class BlobPtr;

template <typename T>
bool operator ==(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);

template <typename T>
bool operator < (const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);

练习

template<typename T> class BlobPtr
{
    friend bool operator ==<T>
    (const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);

    friend bool operator < <T>
        (const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);

public:
    BlobPtr() : curr(0) {}
    BlobPtr(Blob<T>& a, std::size_t sz = 0) :
        wptr(a.data), curr(sz)
    {}

    T& operator*() const
    {
        auto p = check(curr, "dereference past end");
        return (*p)[curr];
    }

练习

    // prefix
    BlobPtr& operator++();
    BlobPtr& operator--();

    // postfix
    BlobPtr operator ++(int);
    BlobPtr operator --(int);

private:
    // returns  a shared_ptr to the vector if the check succeeds
    std::shared_ptr<std::vector<T>>
        check(std::size_t, const std::string&) const;

    std::weak_ptr<std::vector<T>> wptr;
    std::size_t curr;

};

练习

// prefix ++
template<typename T>
BlobPtr<T>& BlobPtr<T>::operator ++()
{
    // if curr already points past the end of the container, can't increment it
    check(curr, "increment past end of StrBlob");
    ++curr;
    return *this;
}

// prefix --
template<typename T>
BlobPtr<T>& BlobPtr<T>::operator --()
{
    --curr;
    check(curr, "decrement past begin of BlobPtr");

    return *this;
}

练习

// postfix ++
template<typename T>
BlobPtr<T> BlobPtr<T>::operator ++(int)
{
    BlobPtr ret = *this;
    ++*this;

    return ret;
}

// postfix --
template<typename T>
BlobPtr<T> BlobPtr<T>::operator --(int)
{
    BlobPtr ret = *this;
    --*this;

    return ret;
}

练习

template<typename T> bool operator==(const BlobPtr<T> &lhs, const BlobPtr<T> &rhs)
{
    if (lhs.wptr.lock() != rhs.wptr.lock())
    {
        throw runtime_error("ptrs to different Blobs!");
    }
    return lhs.i == rhs.i;
}

template<typename T> bool operator< (const BlobPtr<T> &lhs, const BlobPtr<T> &rhs)
{
    if (lhs.wptr.lock() != rhs.wptr.lock())
    {
        throw runtime_error("ptrs to different Blobs!");
    }
    return lhs.i < rhs.i;
}

练习

解释你为 BlobPtr 的相等和关系运算符选择哪种类型的友好关系

这里需要与类型一一对应所以就选择一对一友好关系

练习

编写 Screen 类模版用非类型参数定义 Screen 的高和宽
//Screen
#include <string>
#include <iostream>

template<unsigned H, unsigned W>
class Screen
{
public:
    typedef std::string::size_type pos;
    Screen() = default; // needed because Screen has another constructor
    // cursor initialized to 0 by its in-class initializer
    Screen(char c) :contents(H * W, c) {}
    char get() const              // get the character at the cursor
    {
        return contents[cursor];
    }       // implicitly inline
    Screen &move(pos r, pos c);      // can be made inline later

练习

    friend std::ostream & operator<< (std::ostream &os, const Screen<H, W> & c)
    {
        unsigned int i, j;
        for (i = 0; i<c.height; i++)
        {
            os << c.contents.substr(0, W) << std::endl;
        }
        return os;
    }

    friend std::istream & operator>> (std::istream &is, Screen &  c)
    {
        char a;
        is >> a;
        std::string temp(H*W, a);
        c.contents = temp;
        return is;
    }

练习

private:
    pos cursor = 0;
    pos height = H, width = W;
    std::string contents;
};

template<unsigned H, unsigned W>
inline Screen<H, W>& Screen<H, W>::move(pos r, pos c)
{
    pos row = r * width;
    cursor = row + c;
    return *this;
}

练习

为你的 Screen 模版实现输入和输出运算符
Screen 类需要哪些友元如果需要的话来令输入和输出运算符正确工作
解释每个友元声明如果有的话为什么是必要的

类的 operator<<  operator>> 应该是类的友元

练习

 StrVec 类重写为模版命名为 Vec
//Vec:
#include <memory>
template<typename T>
class Vec
{
public:
    Vec() :element(nullptr), first_free(nullptr), cap(nullptr) {}
    Vec(std::initializer_list<T> l);
    Vec(const Vec& v);

    Vec& operator =(const Vec& rhs);

    ~Vec();

    // memmbers
    void push_back(const T& t);

练习

    std::size_t size() const { return first_free - element; }
    std::size_t capacity()const { return cap - element; }

    T* begin() const { return element; }
    T* end()   const { return first_free; }

    void reserve(std::size_t n);

    void resize(std::size_t n);
    void resize(std::size_t n, const T& t);

private:
    // data members
    T* element;
    T* first_free;
    T* cap;

    std::allocator<T> alloc;

练习

    // utillities
    void reallocate();
    void chk_n_alloc() { if (size() == capacity()) reallocate(); }
    void free();

    void wy_alloc_n_move(std::size_t n);

    std::pair<T*, T*> alloc_n_copy(T* b, T* e);
};

练习

// copy constructor
template<typename T>
Vec<T>::Vec(const Vec &v)
{
    std::pair<T*, T*> newData = alloc_n_copy(v.begin(), v.end());
    element = newData.first;
    first_free = cap = newData.second;
}

练习

// constructor that takes initializer_list<T>
template<typename T>
Vec<T>::Vec(std::initializer_list<T> l)
{
    // allocate memory as large as l.size()
    T* const newData = alloc.allocate(l.size());

    // copy elements from l to the address allocated
    T* p = newData;
    for (const auto &t : l)
        alloc.construct(p++, t);

    // build data structure
    element = newData;
    first_free = cap = element + l.size();
}

练习

// operator =
template<typename T>
Vec<T>& Vec<T>::operator =(const Vec& rhs)
{
    // allocate and copy first to protect against self_assignment
    std::pair<T*, T*> newData = alloc_n_copy(rhs.begin(), rhs.end());

    // destroy and deallocate
    free();

    // update data structure
    element = newData.first;
    first_free = cap = newData.second;

    return *this;
}

练习

// destructor
template<typename T>
Vec<T>::~Vec()
{
    free();
}

template<typename T>
void Vec<T>::push_back(const T &t)
{
    chk_n_alloc();
    alloc.construct(first_free++, t);
}

练习

template<typename T>
void Vec<T>::reserve(std::size_t n)
{
    // if n too small, just return without doing anything
    if (n <= capacity()) return;

    // allocate new memory and move data from old address to the new one
    wy_alloc_n_move(n);
}

template<typename T>
void Vec<T>::resize(std::size_t n)
{
    resize(n, T());
}

练习

template<typename T>
void Vec<T>::resize(std::size_t n, const T &t)
{
    if (n < size())
    {
        // destroy the range [element+n, first_free) using destructor
        for (auto p = element + n; p != first_free;)
            alloc.destroy(p++);
        // update first_free to point to the new address
        first_free = element + n;
    }
    else if (n > size())
    {
        for (auto i = size(); i != n; ++i)
            push_back(t);
    }
}

练习

template<typename T>
std::pair<T*, T*>
Vec<T>::alloc_n_copy(T *b, T *e)
{
    // calculate the size needed and allocate space accordingly
    T* data = alloc.allocate(e - b);
    return{ data, std::uninitialized_copy(b, e, data) };
    //            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // which copies the range[first, last) to the space to which
    // the starting address data is pointing.
    // This function returns a pointer to one past the last element
}

练习

template<typename T>
void Vec<T>::free()
{
    // if not nullptr
    if (element)
    {
        // destroy it in reverse order.
        for (auto p = first_free; p != element;)
            alloc.destroy(--p);
        alloc.deallocate(element, capacity());
    }
}

练习

template<typename T>
void Vec<T>::wy_alloc_n_move(std::size_t n)
{
    // allocate as required.
    std::size_t newCapacity = n;
    T* newData = alloc.allocate(newCapacity);

    // move the data from old place to the new one
    T* dest = newData;
    T* old = element;
    for (std::size_t i = 0; i != size(); ++i)
        alloc.construct(dest++, std::move(*old++));
    free();
    // update data structure
    element = newData;
    first_free = dest;
    cap = element + newCapacity;
}

练习

template<typename T>
void Vec<T>::reallocate()
{
    // calculate the new capacity required
    std::size_t newCapacity = size() ? 2 * size() : 1;

    // allocate and move old data to the new space
    wy_alloc_n_move(newCapacity);
}

模板参数

类似函数参数的名字一个模板参数的名字也没有什么内在含义
我们通常将类型参数命名为T但实际上我们可以使用任何名字
template <typename Foo> Foo calc(const Foo& a, const Foo& b)
{
    Foo tmp = a; // tmp has the same type as the parameters and return type
    // ...
    return tmp;  // return type and parameters have the same type
}

模板参数与作用域

模板参数遵循普通的作用域规则
一个模板参数名的可用范围是在其声明之后至模板声明或定义结束之前
与任何其他名字一样模板参数会隐藏外层作用域中声明的相同名字
但是与大多数其他上下文不同在模板内不能重用模板参数名
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
    A tmp = a; // tmp has same type as the template parameter A, not double
    double B;  // error: redeclares template parameter B
}
//正常的名字隐藏规则决定了A的typedef被类型参数A隐藏。因此,tmp不是一个double,
//其类型是使用f时绑定到类型参数A的类型。
//由于我们不能重用模板参数名,声明名字为B的变量是错误的。
由于参数名不能重用所以一个模板参数名在一个特定模板参数列表中只能出现一次
// error: illegal reuse of template parameter name V
template <typename V, typename V> // ...

模板声明

模板声明必须包含模板参数
// declares but does not define compare and Blob
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
与函数参数相同声明中的模板参数的名字不必与定义中相同
// all three uses of calc refer to the same function template
template <typename T> T calc(const T&, const T&); // declaration
template <typename U> U calc(const U&, const U&); // declaration
// definition of the template
template <typename Type>
Type calc(const Type& a, const Type& b) { /* . . . */ }
当然一个给定模板的每个声明和定义必须有相同数量和种类类型或非类型的参数

一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置
出现于任何使用这些模板的代码之前

使用类的类型成员

回忆一下我们用作用域运算符(::)来访问static成员和类型成员
在普通非模板代码中编译器掌握类的定义
- 因此它知道通过作用域运算符访问的名字是类型还是static成员
- 例如string::size_type编译器有string的定义从而知道size_type是一个类型

但对于模板代码就存在困难
- 例如假定T是一个模板类型参数当编译器遇到类似T::mem这样的代码时
  它不会知道mem是一个类型成员还是一个static数据成员直至实例化时才会知道
  但是为了处理模板编译器必须知道名字是否表示一个类型
  例如假定T是一个类型参数的名字当编译器遇到如下形式的语句时
T::size_type * p;
它需要知道
- 我们是正在定义一个名为p的变量?
- 还是将一个名为size_type的static数据成员与名为p的变量相乘?

使用类的类型成员

默认情况下C++语言假定通过作用域运算符访问的名字不是类型
如果我们希望使用一个模板类型参数的类型成员就必须显式告诉编译器该名字是一个类型
我们通过使用关键字typename来实现这一点
template <typename T>
typename T::value_type top(const T& c)
{
    if (!c.empty())
        return c.back();
    else
        return typename T::value_type();
}
//我们的top函数期待一个容器类型的实参,
//它使用typename指明其返回类型并在c中没有元素时生成一个值初始化的元素返回给调用者

当我们希望通知编译器一个名字表示类型时必须使用关键字typename而不能使用class

默认模板实参

就像能为函数参数提供默认实参一样也可以提供默认模板实参default template argument)。
在新标准中可以为函数和类模板提供默认实参
而更早的C++标准只允许为类模板提供默认实参
//- 例如,我们重写compare,默认使用标准库的less函数对象模板
// compare has a default template argument, less<T>
// and a default function argument, F()
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
    if (f(v1, v2)) return -1;
    if (f(v2, v1)) return 1;
    return 0;
}
//在这段代码中,我们为模板添加了第二个类型参数,名为F,表示可调用对象的类型;
//并定义了一个新的函数参数f,绑定到一个可调用对象上。
//我们为此模板参数提供了默认实参,并为其对应的函数参数也提供了默认实参。
//默认模板实参指出compare将使用标准库的less函数对象类,
//它是使用与compare一样的类型参数实例化的。
//默认函数实参指出f将是类型F的一个默认初始化的对象。

默认模板实参

//当用户调用这个版本的compare时,可以提供自己的比较操作,但这并不是必需的:
bool i = compare(0, 42); // uses less; i is -1
// result depends on the isbns in item1 and item2
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn);
//第一个调用使用默认函数实参,即,类型less<T>的一个默认初始化对象。
//在此调用中,T为int,因此可调用对象的类型为less<int>。
//compare的这个实例化版本将使用less<int>进行比较操作。

//在第二个调用中,我们传递给compare三个实参:compareIsbn和两个Sales_data类
//型的对象。当传递给compare三个实参时,第三个实参的类型必须是一个可调用对象,
//该可调用对象的返回类型必须能转换为bool值,且接受的实参类型必须与compare的
//前两个实参的类型兼容。与往常一样,模板参数的类型从它们对应的函数实参推断
//而来。在此调用中,T的类型被推断为Sales_data,F被推断为compareIsbn的类型。

与函数默认实参一样对于一个模板参数
只有当它右侧的所有参数都有默认实参时它才可以有默认实参

模板默认实参与类模板

无论何时使用一个类模板我们都必须在模板名之后接上尖括号
尖括号指出类必须从一个模板实例化而来
特别是如果一个类模板为其所有模板参数都提供了默认实参
且我们希望使用这些默认实参就必须在模板名之后跟一个空尖括号对
template <class T = int> 
class Numbers {   // by default T is int
public:
    Numbers(T v = 0): val(v) { }
    // various operations on numbers
private:
    T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; // empty <> says we want the default type
//此例中我们实例化了两个Numbers版本:average_precision是用int代替T实例化得到的
//lots_of_precision是用long double代替T实例化而得到的。

练习

声明为 typename 的类型参数和声明为 class 的类型参数有什么不同如果有的话)?
什么时候必须使用typename

没有什么不同当我们希望通知编译器一个名字表示类型时必须使用关键字 typename
而不能使用 class

练习

解释下面每个函数模版声明并指出它们是否非法更正你发现的每个错误


(a) template <typename T, U, typename V> void f1(T, U, V);
(b) template <typename T> T f2(int &T);
(c) inline template <typename T> T foo(T, unsigned int *);
(d) template <typename T> f4(T, T);
(e) typedef char Ctype;
    template <typename Ctype> Ctype f5(Ctype a);

(a) 非法应该为 template <typename T, typename U, typename V> void f1(T, U, V);
(b) 非法应该为 template <typename T> T f2(int &t);
(c) 非法应该为 template <typename T> inline T foo(T, unsigned int*);
(d) 非法应该为 template <typename T> T f4(T, T);
(e) 非法Ctype 被隐藏了

练习

编写函数接受一个容器的引用打印容器中的元素
使用容器的 size_type  size成员来控制打印元素的循环

template<typename Container>
void print(const Container& c)
{
    for (typename Container::size_type i = 0; i != c.size(); ++i)
        std::cout << c[i] << " ";
}

练习

重写上一题的函数使用begin  end 返回的迭代器来控制循环


template<typename Container>
void print(const Container& c)
{
    for (auto it = c.begin(); it != c.end(); ++it)
        std::cout << *it << " ";
}

成员模板

一个类无论是普通类还是类模板可以包含本身是模板的成员函数
这种成员被称为成员模板member template)。成员模板不能是虚函数

普通(非模板)类的成员模板

作为普通类包含成员模板的例子我们定义一个类
类似unique_ptr所使用的默认删除器类型
类似默认删除器我们的类将包含一个重载的函数调用运算符
它接受一个指针并对此指针执行delete
与默认删除器不同我们的类还将在删除器被执行时打印一条信息
由于希望删除器适用于任何类型所以我们将调用运算符定义为一个模板
class DebugDelete {
public:
    DebugDelete(std::ostream &s = std::cerr): os(s) { }
    // as with any function template, the type of T is deduced by the compiler
    template <typename T> void operator()(T *p) const
      { os << "deleting unique_ptr" << std::endl; delete p;
}
private:
    std::ostream &os;
};

普通(非模板)类的成员模板

与任何其他模板相同成员模板也是以模板参数列表开始的
每个DebugDelete对象都有一个ostream成员用于写入数据
还包含一个自身是模板的成员函数我们可以用这个类代替delete
double* p = new double;
DebugDelete d;    // an object that can act like a delete expression
d(p); // calls DebugDelete::operator()(double*), which deletes p
int* ip = new int;
// calls operator()(int*) on a temporary DebugDelete object
DebugDelete()(ip);
由于调用一个DebugDelete对象会delete其给定的指针
我们也可以将DebugDelete用作unique_ptr的删除器
为了重载unique_ptr的删除器我们在尖括号内给出删除器类型
并提供一个这种类型的对象给unique_ptr的构造函数
// destroying the the object to which p points
// instantiates DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// destroying the the object to which sp points
// instantiates DebugDelete::operator()<string>(string*)
unique_ptr<string,DebugDelete> sp(new string,DebugDelete());

普通(非模板)类的成员模板

在本例中我们声明p的删除器的类型为DebugDelete
并在p的构造函数中提供了该类型的一个未命名对象

unique_ptr的析构函数会调用DebugDelete的调用运算符
因此无论何时unique_ptr的析构函数实例化时DebugDelete的调用运算符都会实例化
因此上述定义会这样实例化.
// sample instantiations for member templates of DebugDelete
void DebugDelete::operator()(int *p) const { delete p; }
void DebugDelete::operator()(string *p) const { delete p; }

类模板的成员模板

对于类模板我们也可以为其定义成员模板
在此情况下类和成员各自有自己的独立的模板参数
例如我们将为Blob类定义一个构造函数它接受两个迭代器表示要拷贝的元素范围
由于我们希望支持不同类型序列的迭代器因此将构造函数定义为模板
template <typename T> class Blob {
    template <typename It> Blob(It b, It e);
    // ...
};
此构造函数有自己的模板类型参数It作为它的两个函数参数的类型

与类模板的普通函数成员不同成员模板是函数模板
当我们在类模板外定义一个成员模板时必须同时为类模板和成员模板提供模板参数列表
类模板的参数列表在前后跟成员自己的模板参数列表
template <typename T>     // type parameter for the class
template <typename It>    // type parameter for the constructor
    Blob<T>::Blob(It b, It e):
              data(std::make_shared<std::vector<T>>(b, e)) {
}
在此例中我们定义了一个类模板的成员类模板有一个模板类型参数命名为T
而成员自身是一个函数模板它有一个名为It的类型参数

实例化与成员模板

为了实例化一个类模板的成员模板我们必须同时提供类和函数模板的实参
我们在哪个对象上调用成员模板编译器就根据该对象的类型来推断类模板参数的实参
与普通函数模板相同编译器通常根据传递给成员模板的函数实参来推断它的模板实参

实例化与成员模板

int ia[] = {0,1,2,3,4,5,6,7,8,9};
vector<long> vi = {0,1,2,3,4,5,6,7,8,9};
list<const char*> w = {"now", "is", "the", "time"};
// instantiates the Blob<int> class
// and the Blob<int> constructor that has two int* parameters
Blob<int> a1(begin(ia), end(ia));
// instantiates the Blob<int> constructor that has
// two vector<long>::iterator parameters
Blob<int> a2(vi.begin(), vi.end());
// instantiates the Blob<string> class and the Blob<string>
// constructor that has two (list<const char*>::iterator parameters
blob<string> a3(w.begin(), w.end());
当我们定义a1时显式地指出编译器应该实例化一个int版本的blob
构造函数自己的类型参数则通过beginia和endia的类型来推断结果为int*。
因此a1的定义实例化了如下版本
blob<int>::blob(int*, int*);
a2的定义使用了已经实例化了的blob<int>
并用vector<short>::iterator替换it来实例化构造函数
a3的定义显式地实例化了一个string版本的blob
隐式地实例化了该类的成员模板构造函数其模板参数被绑定到list<const char>

练习

编写你自己的 debugdelete 版本

//debugdelete
#include <iostream>

class debugdelete
{
public:
    debugdelete(std::ostream& s = std::cerr) : os(s) {}
    template<typename t>
    void operator() (t* p) const
    {
        os << "deleting unique_ptr" << std::endl;
        delete p;
    }

private:
    std::ostream& os;
};

练习

修改12.3节中你的 textquery 程序
 shared_ptr 成员使用 debugdelete 作为它们的删除器

对shared_ptr成员file的初始化进行修改创建一个debugdelete 对象作为第二个参数
textquery::textquery(ifstream &is):
    file(new vector<string>,debugdelete("shared_ptr"))

练习

预测在你的查询主程序中何时会执行调用运算符
如果你的预测和实际不符确认你理解了原因

当shared_ptr 的引用计数变为0需要释放资源时才会调用删除器进行资源释放

练习

为你的 blob 模版添加一个构造函数它接受两个迭代器
template<typename t>    //for class
template<typename it>   //for this member
blob<t>::blob(it b, it e) :
    data(std::make_shared<std::vector<t>>(b, e))
{ }

控制实例化

当模板被使用时才会进行实例化这一特性意味着相同的实例可能出现在多个对象文件中
当两个或多个独立编译的源文件使用了相同的模板并提供了相同的模板参数时
- 每个文件中就都会有该模板的一个实例
在大系统中在多个文件中实例化相同模板的额外开销可能非常严重
在新标准中我们可以通过显式实例化explicit instantiation来避免这种开销
一个显式实例化有如下形式
extern template declaration; // instantiation declaration
template declaration;        // instantiation definition
declaration是一个类或函数声明其中所有模板参数已被替换为模板实参例如
// instantion declaration and definition
extern template class blob<string>;             // declaration
template int compare(const int&, const int&);   // definition
当编译器遇到extern模板声明时它不会在本文件中生成实例化代码
将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明定义
对于一个给定的实例化版本可能有多个extern声明但必须只有一个定义

控制实例化

由于编译器在使用一个模板时自动对其实例化
因此extern声明必须出现在任何使用此实例化版本的代码之前
// application.cc
// these template types must be instantiated elsewhere in the program
extern template class blob<string>;
extern template int compare(const int&, const int&);
blob<string> sa1, sa2; // instantiation will appear elsewhere
// blob<int> and its initializer_list constructor instantiated in this file
blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
blob<int> a2(a1);  // copy constructor instantiated in this file
int i = compare(a1[0], a2[0]); // instantiation will appear elsewhere

控制实例化

//文件application.o将包含blob<int>的实例及其接受initializer_list参数的
//构造函数和拷贝构造函数的实例。而compare<int>函数和blob<string>类将不在
//本文件中进行实例化。这些模板的定义必须出现在程序的其他文件中:
// templatebuild.cc
// instantiation file must provide a (nonextern) definition for every
// type and function that other files declare as extern
template int compare(const int&, const int&);
template class blob<string>; // instantiates all members of the class template
//当编译器遇到一个实例化定义(与声明相对)时,它为其生成代码。因此,文件
//templatebuild.o将会包含compare的int实例化版本的定义和blob<string>类的定义。
//当我们编译此应用程序时,必须将templatebuild.o和application.o链接到一起。

对每个实例化声明在程序中某个位置必须有其显式的实例化定义

实例化定义会实例化所有成员

一个类模板的实例化定义会实例化该模板的所有成员包括内联的成员函数
当编译器遇到一个实例化定义时它不了解程序使用哪些成员函数
因此与处理类模板的普通实例化不同编译器会实例化该类的所有成员
即使我们不使用某个成员它也会被实例化
因此我们用来显式实例化一个类模板的类型必须能用于模板的所有成员

在一个类模板的实例化定义中所用类型必须能用于模板的所有成员函数

练习

解释下面这些声明的含义

extern template class vector<string>;
template class vector<sales_data>;


前者是模版声明后者是实例化定义

练习

假设 nodefault 是一个没有默认构造函数的类
我们可以显式实例化 vector<nodefualt>如果不可以解释为什么


不可以
std::vector<nodefault> vec(10);
会使用 nodefault 的默认构造函数 nodefault 没有默认构造函数因此是不可以的

练习

对下面每条带标签的语句解释发生了什么样的实例化如果有的话)。
如果一个模版被实例化解释为什么;如果未实例化解释为什么没有
template <typename t> class stack {    };
void f1(stack<char>);         //(a)
class exercise {
    stack<double> &rds;        //(b)
    stack<int> si;            //(c)
};
int main() {
    stack<char> *sc;        //(d)
    f1(*sc);                //(e)
    int iobj = sizeof(stack<string>);    //(f)
}

(a)(b)(c)(f) 都发生了实例化(d)(e) 没有实例化

效率与灵活性

对模板设计者所面对的设计选择标准库智能指针类型给出了一个很好的展示
shared_ptr和unique_ptr之间的明显不同是它们管理所保存的指针的策略
- 前者给予我们共享指针所有权的能力后者则独占指针
- 这一差异对两个类的功能来说是至关重要的

这两个类的另一个差异是它们允许用户重载默认删除器的方式
我们可以很容易地重载一个shared_ptr的删除器
只要在创建或reset指针时传递给它一个可调用对象即可
与之相反删除器的类型是一个unique_ptr对象的类型的一部分
用户必须在定义unique_ptr时以显式模板实参的形式提供删除器的类型
因此对于unique_ptr的用户来说提供自己的删除器就更为复杂

如何处理删除器的差异实际上就是这两个类功能的差异
但是如我们将要看到的这一实现策略上的差异可能对性能有重要影响

在运行时绑定删除器

虽然我们不知道标准库类型是如何实现的但可以推断出
shared_ptr必须能直接访问其删除器
删除器必须保存为一个指针或一个封装了指针的类

我们可以确定shared_ptr不是将删除器直接保存为一个成员
因为删除器的类型直到运行时才会知道
实际上在一个shared_ptr的生存期中我们可以随时改变其删除器的类型
我们可以使用一种类型的删除器构造一个shared_ptr
随后使用reset赋予此shared_ptr另一种类型的删除器
通常类成员的类型在运行时是不能改变的因此不能直接保存删除器

为了考察删除器是如何正确工作的让我们假定shared_ptr将它管理的指针保存在
一个成员p中且删除器是通过一个名为del的成员来访问的
则shared_ptr的析构函数必须包含类似下面这样的语句
// value of del known only at run time; call through a pointer
del ? del(p) : delete p; // del(p) requires run-time jump to del's location
由于删除器是间接保存的调用delp需要一次运行时的跳转操作
转到del中保存的地址来执行对应的代码

在编译时绑定删除器

现在让我们来考察unique_ptr可能的工作方式
在这个类中删除器的类型是类类型的一部分
unique_ptr有两个模板参数一个表示它所管理的指针另一个表示删除器的类型
由于删除器的类型是unique_ptr类型的一部分
因此删除器成员的类型在编译时是知道的从而删除器可以直接保存在unique_ptr对象中

unique_ptr的析构函数与shared_ptr的析构函数类似
也是对其保存的指针调用用户提供的删除器或执行delete
// del bound at compile time; direct call to the deleter is instantiated
del(p);   // no run-time overhead
del的类型或者是默认删除器类型或者是用户提供的类型
到底是哪种情况没有关系应该执行的代码在编译时肯定会知道
实际上如果删除器是类似DebugDelete之类的东西这个调用甚至可能被编译为内联形式

通过在编译时绑定删除器unique_ptr避免了间接调用删除器的运行时开销
通过在运行时绑定删除器shared_ptr使用户重载删除器更为方便

练习

编写你自己版本的 shared_ptr  unique_ptr

见book/ch16/ex16.28

练习

修改你的 Blob 用你自己的 shared_ptr 代替标准库中的版本
见book/ch16/ex16.29

练习

重新运行你的一些程序验证你的 shared_ptr 类和修改后的 Blob 
注意实现 weak_ptr 类型超出了本书范围
因此你不能将BlobPtr类与你修改后的Blob一起使用。)

参考之前

练习

如果我们将 DebugDelete  unique_ptr 一起使用
解释编译器将删除器处理为内联形式的可能方式


编译器可能会去掉DebugDelete类对象和相关函数替换为对应的输出语句和释放操作

模板实参推断

对于函数模板编译器利用调用中的函数实参来确定其模板参数
从函数实参来确定模板实参的过程被称为模板实参推断template argumentdeduction)。
在模板实参推断过程中编译器使用函数调用中的实参类型来寻找模板实参
用这些模板实参生成的函数版本与给定的函数调用最为匹配

类型转换与模板类型参数

与非模板函数一样我们在一次调用中传递给函数模板的实参被用来初始化函数的形参
如果一个函数形参的类型使用了模板类型参数那么它采用特殊的初始化规则
只有很有限的几种类型转换会自动地应用于这些实参
编译器通常不是对实参进行类型转换而是生成一个新的模板实例

顶层const无论是在形参中还是在实参中都会被忽略
在其他类型转换中能在调用中应用于函数模板的包括如下两项
- const转换可以将一个非const对象的引用或指针
  传递给一个const的引用或指针形参
- 数组或函数指针转换如果函数形参不是引用类型
  则可以对数组或函数类型的实参应用正常的指针转换
  一个数组实参可以转换为一个指向其首元素的指针
  类似的一个函数实参可以转换为一个该函数类型的指针

其他类型转换如算术转换派生类向基类的转换以及用户定义的转换
都不能应用于函数模板

类型转换与模板类型参数

//作为一个例子,考虑对函数fobj和fref的调用。
//fobj函数拷贝它的参数,而fref的参数是引用类型:
template <typename T> T fobj(T, T); // arguments are copied 
template <typename T> T fref(const T&, const T&); // references

string s1("a value");
const string s2("another value");
fobj(s1, s2); // calls fobj(string, string); const is ignored 
fref(s1, s2); // calls fref(const string&, const string&)
// uses premissible conversion to const on s1 
//在第一对调用中,我们传递了一个string和一个const string。
//虽然这些类型不严格匹配,但两个调用都是合法的。在fobj调用中,实参被拷贝,
//因此原对象是否是const没有关系。在fref调用中,参数类型是const的引用。
//对于一个引用参数来说,转换为const是允许的,因此这个调用也是合法的。

类型转换与模板类型参数

//作为一个例子,考虑对函数fobj和fref的调用。
//fobj函数拷贝它的参数,而fref的参数是引用类型:
template <typename T> T fobj(T, T); // arguments are copied 
template <typename T> T fref(const T&, const T&); // references

int a[10], b[42];
fobj(a, b); // calls f(int*, int*)
fref(a, b); // error: array types don't match
//在下一对调用中,我们传递了数组实参,两个数组大小不同,因此是不同类型。
//在fobj调用中,数组大小不同无关紧要。两个数组都被转换为指针。
//fobj中的模板类型为int*。但是,fref调用是不合法的。如果形参是一个引用,
//则数组不会转换为指针。a和b的类型是不匹配的,因此调用是错误的。

将实参传递给带模板类型的函数形参时
能够自动应用的类型转换只有const转换及数组或函数到指针的转换

使用相同模板参数类型的函数形参

一个模板类型参数可以用作多个函数形参的类型
由于只允许有限的几种类型转换因此传递给这些形参的实参必须具有相同的类型
如果推断出的类型不匹配则调用就是错误的
- 例如我们的compare函数接受两个const T&参数其实参必须是相同类型
long lng;
compare(lng, 1024); // error: cannot instantiate compare(long, int)
//此调用是错误的,因为传递给compare的实参类型不同。
//从第一个函数实参推断出的模板实参为long,从第二个实参推断出的模板实参为int。
//这些类型不匹配,因此模板实参推断失败。
如果希望允许对函数实参进行正常的类型转换我们可以将函数模板定义为两个类型参数
// argument types can differ but must be compatible template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2)
{
if (v2 < v1) return 1;
if (v1 < v2) return -1; return 0;
}
现在用户可以提供不同类型的实参了
long lng;
flexibleCompare(lng, 1024); // ok: calls flexibleCompare(long, int)
当然必须定义了能比较这些类型的值的<运算符

正常类型转换应用于普通函数实参

函数模板可以有用普通类型定义的参数不涉及模板类型参数的类型
这种函数实参不进行特殊处理它们正常转换为对应形参的类型
例如考虑下面的模板
template <typename T> ostream &print(ostream &os, const T &obj)
{ return os << obj;}
//第一个函数参数是一个已知类型ostream&。第二个参数obj则是模板参数类型。
//由于os的类型是固定的,因此调用print时,传递给它的实参会进行正常的类型转换:
print(cout, 42); // instantiates print(ostream&, int)
ofstream f("output");
print(f, 10); // uses print(ostream&, int); converts f to ostream&
//在第一个调用中,第一个实参的类型严格匹配第一个参数的类型。
//此调用会实例化接受一个ostream&和一个int的print版本。
//在第二个调用中,第一个实参是一个ofstream,它可以转换为ostream&。
//由于此参数的类型不依赖于模板参数,因此编译器会将f隐式转换为ostream&。
如果函数参数类型不是模板参数则对实参进行正常的类型转换

练习

在模版实参推断过程中发生了什么

在模版实参推断过程中编译器使用函数调用中的实参类型来寻找模版实参
用这些模版实参生成的函数版本与给定的函数调用最为匹配

练习

指出在模版实参推断过程中允许对函数实参进行的两种类型转换

- const 转换
  可以将一个非 const 对象的引用或指针
  传递给一个 const 的引用或指针形参

- 数组或函数指针转换
  如果函数形参不是引用类型则可以对数组或函数类型的
  实参应用正常的指针转换一个数组实参可以转换为一个指向其首元素的指针
  类似的一个函数实参可以转换为一个该函数类型的指针

练习

对下面的代码解释每个调用是否合法
如果合法T 的类型是什么如果不合法为什么
template <class T> int compare(const T&, const T&);
(a) compare("hi", "world");
(b) compare("bye", "dad");

(a) 不合法compare(const char [3], const char [6]), 两个实参类型不一致
(b) 合法compare(const char [4], const char [4]).

练习

下面调用中哪些是错误的如果有的话)?
如果调用合法T 的类型是什么如果调用不合法问题何在
template <typename T> T calc(T, int);
tempalte <typename T> T fcn(T, T);
double d; float f; char c;
(a) calc(c, 'c'); 
(b) calc(d, f);
(c) fcn(c, 'c');
(d) fcn(d, f);


(a) 合法类型为char
(b) 合法类型为double
(c) 合法类型为char
(d) 不合法这里无法确定T的类型是float还是double

练习

进行下面的调用会发生什么
template <typename T> f1(T, T);
template <typename T1, typename T2) f2(T1, T2);
int i = 0, j = 42, *p1 = &i, *p2 = &j;
const int *cp1 = &i, *cp2 = &j;
(a) f1(p1, p2);
(b) f2(p1, p2);
(c) f1(cp1, cp2);
(d) f2(cp1, cp2);
(e) f1(p1, cp1);
(f) f2(p1, cp1);

(a) f1(int*, int*);
(b) f2(int*, int*);
(c) f1(const int*, const int*);
(d) f2(const int*, const int*);
(e) f1(int*, const int*); 这个使用就不合法
(f) f2(int*, const int*);

函数模板显式实参

在某些情况下编译器无法推断出模板实参的类型
其他一些情况下我们希望允许用户控制模板实例化
当函数返回类型与参数列表中任何类型都不相同时这两种情况最常出现

指定显式模板实参

作为一个允许用户指定使用类型的例子
我们将定义一个名为sum的函数模板它接受两个不同类型的参数
我们希望允许用户指定结果的类型这样用户就可以选择合适的精度
我们可以定义表示返回类型的第三个模板参数从而允许用户控制返回类型
// T1 cannot be deduced: it doesn't appear in the function parameter list 
template <typename T1, typename T2, typename T3> 
T1 sum(T2, T3);
在本例中没有任何函数实参的类型可用来推断T1的类型每次调用sum时
调用者都必须为T1提供一个显式模板实参explicit template argument)。
我们提供显式模板实参的方式与定义类模板实例的方式相同
显式模板实参在尖括号中给出位于函数名之后实参列表之前
// T1 is explicitly specified; T2 and T3 are inferred from the argument types
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
此调用显式指定T1的类型而T2和T3的类型则由编译器从i和lng的类型推断出来

指定显式模板实参

显式模板实参按由左至右的顺序与对应的模板参数匹配
- 第一个模板实参与第一个模板参数匹配第二个实参与第二个参数匹配依此类推
只有尾部最右参数的显式模板实参才可以忽略而且前提是它们可以从函数参数推断出来
如果我们的sum函数按照如下形式编写
// poor design: users must explicitly specify all three template parameters 
template <typename T1, typename T2, typename T3> 
T3 alternative_sum(T2, T1);
则我们总是必须为所有三个形参指定实参
// error: can't infer initial template parameters
auto val3 = alternative_sum<long long>(i, lng);
// ok: all three parameters are explicitly specified
auto val2 = alternative_sum<long long, int, long>(i, lng);

正常类型转换应用于显式指定的实参

对于用普通类型定义的函数参数允许进行正常的类型转换
出于同样的原因对于模板类型参数已经显式指定了的函数实参也进行正常的类型转换
long lng;
compare(lng, 1024); // error: template parameters don't match 
compare<long>(lng, 1024); // ok: instantiates compare(long, long) 
compare<int>(lng, 1024); // ok: instantiates compare(int, int)
//如我们所见,第一个调用是错误的,因为传递给compare的实参必须具有相同的类型。
//如果我们显式指定模板类型参数,就可以进行正常类型转换了。
//因此,调用compare<long>等价于调用一个接受两个const long&参数的函数。
//int类型的参数被自动转化为long。
//在第三个调用中,T被显式指定为int,因此lng被转换为int。

练习

标准库 max 函数有两个参数它返回实参中的较大者此函数有一个模版类型参数
你能在调用 max 时传递给它一个 int 和一个 double 
如果可以如何做如果不可以为什么


可以提供显式的模版实参
int a = 1;
double b = 2;
std::max<double>(a, b);

练习

当我们调用 make_shared 必须提供一个显示模版实参
解释为什么需要显式模版实参以及它是如果使用的

如果不显示提供模版实参那么 make_shared 无法推断要分配多大内存空间

练习

对16.1.1 中的原始版本的 compare 函数
使用一个显式模版实参使得可以向函数传递两个字符串字面量

compare<std::string>("hello", "world")  

尾置返回类型与类型转换

当我们希望用户确定返回类型时用显式模板实参表示模板函数的返回类型是很有效的
但在其他情况下要求显式指定模板实参会给用户增添额外负担而且不会带来什么好处
我们可能希望编写一个函数接受表示序列的一对迭代器和返回序列中一个元素的引用
template <typename It> 
??? &fcn(It beg, It end)
{
// process the range
return *beg; // return a reference to an element from the range 
}
我们并不知道返回结果的准确类型但知道所需类型是所处理的序列的元素类型
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = { "hi", "bye" };
auto &i = fcn(vi.begin(), vi.end()); // fcn should return int& 
auto &s = fcn(ca.begin(), ca.end()); // fcn should return string&

尾置返回类型与类型转换

此例中我们知道函数应该返回beg
而且知道我们可以用decltype(*beg来获取此表达式的类型
但是在编译器遇到函数的参数列表之前beg都是不存在的
为了定义此函数我们必须使用尾置返回类型
由于尾置返回出现在参数列表之后它可以使用函数的参数
// a trailing return lets us declare the return type after the parameter list is seen
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
// process the range
return *beg; // return a reference to an element from the range 
}
//此例中我们通知编译器fcn的返回类型与解引用beg参数的结果类型相同。
//解引用运算符返回一个左值,
//因此通过decltype推断的类型为beg表示的元素的类型的引用。
//因此,如果对一个string序列调用fcn,返回类型将是string&。
//如果是int序列,则返回类型是int&。

进行类型转换的标准库模板类

有时我们无法直接获得所需要的类型
- 例如我们可能希望编写一个类似fcn的函数但返回一个元素的值而非引用
在编写这个函数的过程中我们面临一个问题
- 对于传递的参数的类型我们几乎一无所知
  在此函数中我们知道唯一可以使用的操作是迭代器操作
  而所有迭代器操作都不会生成元素只能生成元素的引用
为了获得元素类型我们可以使用标准库的类型转换type transformation模板
  这些模板定义在头文件type_traits中
  这个头文件中的类通常用于所谓的模板元程序设计
  但是类型转换模板在普通编程中也很有用表16.1列出了这些模板
  后续看到它们是如何实现的

进行类型转换的标准库模板类

在本例中我们可以使用remove_reference来获得元素类型
remove_reference模板有一个模板类型参数和一个名为type的public类型成员
如果我们用一个引用类型实例化remove_reference则type将表示被引用的类型
例如如果我们实例化remove_reference<int&>则type成员将是int
类似的如果我们实例化remove_reference<string&>则type成员将是string
依此类推更一般的给定一个迭代器beg
remove_reference<decltype(*beg)>::type
将获得beg引用的元素的类型decltype(*beg返回元素类型的引用类型
remove_reference::type脱去引用剩下元素类型本身

进行类型转换的标准库模板类

组合使用remove_reference尾置返回及decltype就可以在函数中返回元素值的拷贝
// must use typename to use a type member of a template parameter; see § 16.1.3
template <typename It>
auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
// process the range
return *beg; // return a copy of an element from the range 
}
注意type是一个类的成员而该类依赖于一个模板参数
因此我们必须在返回类型的声明中使用typename来告知编译器type表示一个类型

进行类型转换的标准库模板类

表16.1中描述的每个类型转换模板的工作方式都与remove_reference类似
每个模板都有一个名为type的public成员表示一个类型
此类型与模板自身的模板类型参数相关其关系如模板名所示
如果不可能或者不必要转换模板参数则type成员就是模板参数类型本身
例如如果T是一个指针类型则remove_pointer<T>::type是T指向的类型
如果T不是一个指针则无须进行任何转换从而type具有与T相同的类型

练习

下面的函数是否合法如果不合法为什么
如果合法对可以传递的实参类型有什么限制如果有的话)?返回类型是什么
template <typename It>
auto fcn3(It beg, It end) -> decltype(*beg + 0)
{
    //处理序列
    return *beg;
}

合法该类型需要支持 + 操作

练习

编写一个新的 sum 版本它返回类型保证足够大足以容纳加法结果
template<typename T>
auto sum(T lhs, T rhs) -> decltype( lhs + rhs)
{
    return lhs + rhs;
}

函数指针和实参推断

当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时
编译器使用指针的类型来推断模板实参
//例如,假定我们有一个函数指针,它指向的函数返回int,接受两个参数,
//每个参数都是指向const int的引用。我们可以使用该指针指向compare的一个实例:
template <typename T> int compare(const T&, const T&);
// pf1 points to the instantiation int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
//pf1中参数的类型决定了T的模板实参的类型。
//在本例中,T的模板实参类型为int。指针pf1指向compare的int版本实例。
如果不能从函数指针类型确定模板实参则产生错误
// overloaded versions of func; each takes a different function pointer type
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // error: which instantiation of compare?
//这段代码的问题在于,通过func的参数类型无法确定模板实参的唯一类型。
//对func的调用既可以实例化接受int的compare版本,也可以实例化接受string的版本。
//由于不能确定func的实参的唯一实例化版本,此调用将编译失败。

函数指针和实参推断

我们可以通过使用显式模板实参来消除func调用的歧义
// ok: explicitly specify which version of compare to instantiate
func(compare<int>);  // passing compare(const int&, const int&)
//此表达式调用的func版本接受一个函数指针,该指针指向的函数接受两个constint&参数。
当参数是一个函数模板实例的地址时程序上下文必须满足
对每个模板参数能唯一确定其类型或值

模板实参推断和引用

为了理解如何从函数调用进行类型推断考虑下面的例子
template <typename T> void f(T &p);
其中函数参数p是一个模板类型参数T的引用非常重要的是记住两点
编译器会应用正常的引用绑定规则const是底层的不是顶层的

从左值引用函数参数推断类型

当一个函数参数是模板类型参数的一个普通左值引用时形如T&),
绑定规则告诉我们只能传递给它一个左值一个变量或一个返回引用类型的表达式)。
实参可以是const类型也可以不是如果实参是const的则T将被推断为const类型
template <typename T> void f1(T&);  // argument must be an lvalue
// calls to f1 use the referred-to type of the argument as the template parameter type
f1(i);   //  i is an int; template parameter T is int
f1(ci);  //  ci is a const int; template parameter T is const int
f1(5);   //  error: argument to a & parameter must be an lvalue
如果一个函数参数的类型是const T&
正常的绑定规则告诉我们可以传递给它任何类型的实参
- 一个对象const或非const)、一个临时对象或是一个字面常量值
当函数参数本身是const时T的类型推断的结果不会是一个const类型
const已经是函数参数类型的一部分因此它不会也是模板参数类型的一部分
template <typename T> void f2(const T&); // can take an rvalue
// parameter in f2 is const &; const in the argument is irrelevant
// in each of these three calls, f2's function parameter is inferred as const int&
f2(i);  // i is an int; template parameter T is int
f2(ci); // ci is a const int, but template parameter T is int
f2(5);  // a const & parameter can be bound to an rvalue; T is int

从右值引用函数参数推断类型

当一个函数参数是一个右值引用形如T&&
正常绑定规则告诉我们可以传递给它一个右值
当我们这样做时类型推断过程类似普通左值引用函数参数的推断过程
推断出的T的类型是该右值实参的类型
template <typename T> void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int

引用折叠和右值引用参数

假定i是一个int对象我们可能认为像f3i这样的调用是不合法的
毕竟i是一个左值而通常我们不能将一个右值引用绑定到一个左值上
但是C++语言在正常绑定规则之外定义了两个例外规则允许这种绑定
- 这两个例外规则是move这种标准库设施正确工作的基础

第一个例外规则影响右值引用参数的推断如何进行
当我们将一个左值如i传递给函数的右值引用参数且此右值引用
指向模板类型参数如T&&编译器推断模板类型参数为实参的左值引用类型
因此当我们调用f3i编译器推断T的类型为int&而非int

T被推断为int&看起来好像意味着f3的函数参数应该是一个类型int&的右值引用
通常我们不能直接定义一个引用的引用
但是通过类型别名或通过模板类型参数间接定义是可以的

引用折叠和右值引用参数

在这种情况下我们可以使用第二个例外绑定规则
如果我们间接创建一个引用的引用则这些引用形成了折叠”。
- 在所有情况下除了一个例外),引用会折叠成一个普通的左值引用类型
  在新标准中折叠规则扩展到右值引用
- 只在一种特殊情况下引用会折叠成右值引用右值引用的右值引用
对于一个给定类型X
- X&&X&&&和X&&&都折叠成类型X&· 
- 类型X&&&&折叠成X&&
引用折叠只能应用于间接创建的引用的引用如类型别名或模板参数

如果将引用折叠规则和右值引用的特殊类型推断规则组合在一起
则意味着我们可以对一个左值调用f3当我们将一个左值传递给
f3的右值引用函数参数时编译器推断T为一个左值引用类型
f3(i);  // argument is an lvalue; template parameter T is int&
f3(ci); // argument is an lvalue; template parameter T is const int&

引用折叠和右值引用参数

当一个模板参数T被推断为引用类型时
折叠规则告诉我们函数参数T&&折叠为一个左值引用类型
例如f3i的实例化结果可能像下面这样
// invalid code, for illustration purposes only
void f3<int&>(int& &&); // when T is int&, function parameter is int& &&
//f3的函数参数是T&&且T是int&,因此T&&是int&&&,会折叠成int&。
//因此,即使f3的函数参数形式是一个右值引用(即,T&&),
//此调用也会用一个左值引用类型(即,int&)实例化f3:
void f3<int&>(int&); // when T is int&, function parameter collapses to int&

引用折叠和右值引用参数

这两个规则导致了两个重要结果
- 如果一个函数参数是一个指向模板类型参数的右值引用T&&),
  则它可以被绑定到一个左值
- 如果实参是一个左值则推断出的模板实参类型将是一个左值引用
  且函数参数将被实例化为一个普通左值引用参数T&

这两个规则暗示我们可以将任意类型的实参传递给T&&类型的函数参数对于这种
类型的参数,(显然可以传递给它右值而如我们刚刚看到的也可以传递给它左值

如果一个函数参数是指向模板参数类型的右值引用T&&),
则可以传递给它任意类型的实参
如果将一个左值传递给这样的参数则函数参数被实例化为一个普通的左值引用T&)。

编写接受右值引用参数的模板函数

模板参数可以推断为一个引用类型这一特性对模板内的代码可能有令人惊讶的影响
template <typename T> void f3(T&& val)
{
    T t = val;  // copy or binding a reference?
    t = fcn(t); // does the assignment change only t or val and t?
    if (val == t) { /* ... */ } // always true if T is a reference type
}
当我们对一个右值调用f3时例如字面常量42T为int
在此情况下局部变量t的类型为int且通过拷贝参数val的值被初始化
当我们对t赋值时参数val保持不变

另一方面当我们对一个左值i调用f3时则T为int&
当我们定义并初始化局部变量t时赋予它类型int&
因此对t的初始化将其绑定到val
当我们对t赋值时也同时改变了val的值
在f3的这个实例化版本中if判断永远得到true

当代码中涉及的类型可能是普通非引用类型也可能是引用类型时
编写正确的代码就变得异常困难虽然remove_reference这样的类型转换类可能会有帮助)

编写接受右值引用参数的模板函数

在实际中右值引用通常用于两种情况
- 模板转发其实参或模板被重载
使用右值引用的函数模板通常使用在13.6.3节中看到的方式来进行重载
template <typename T> void f(T&&);      // binds to nonconst rvalues
template <typename T> void f(const T&); // lvalues and const rvalues
与非模板函数一样
第一个版本将绑定到可修改的右值而第二个版本将绑定到左值或const右值

练习

对下面每个调用确定 T  val 的类型
template <typename T> void g(T&& val);
int i = 0; const int ci = i;
(a) g(i);
(b) g(ci);
(c) g(i * ci);

(a)T,val: int&
(b)T,val: const int&
(c)T:int    
   val: int&&

练习

使用上一题定义的函数如果我们调用g(i = ci),g 的模版参数将是什么

T : int & 
val : int &
i = ci 返回的是左值因此 g 的模版参数是 int&

练习

使用与第一题中相同的三个调用如果 g 的函数参数声明为 T而不是T&&),
确定T的类型如果g的函数参数是 const T&


template<typename T> void g(T val);
(a) T : int val : int 
(b) T : int val : int
(c) T : int val : int
template<typename T> void g(const T & val);
(a) T : int val : const int &
(b) T : int val : const int &
(c) T : int val : const int &

练习

如果下面的模版如果我们对一个像42这样的字面常量调用g
解释会发生什么如果我们对一个int 类型的变量调用g 
template <typename T> void g(T&& val) { vector<T> v; }

当使用字面常量T为intval为int && 类型

当使用int变量T将为int&,val折叠为int&.
vector管理动态空间需要维护指针而不能定义指向int&的指针
编译的时候将会报错因为没有办法对这种类型进行内存分配无法创建vector<int&>

理解std::move

标准库move函数是使用右值引用的模板的一个很好的例子
幸运的是我们不必理解move所使用的模板机制也可以直接使用它
但是研究move是如何工作的可以帮助我们巩固对模板的理解和使用

虽然不能直接将一个右值引用绑定到一个左值上
但可以用move获得一个绑定到左值上的右值引用
由于move本质上可以接受任何类型的实参因此我们不会惊讶于它是一个函数模板

std::move是如何定义的

标准库是这样定义move的
// for the use of typename in the return type and the cast see § 16.1.3 
// remove_reference is covered in § 16.2.3 
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
   // static_cast covered in § 4.11.3 
    return static_cast<typename remove_reference<T>::type&&>(t);
}
这段代码很短但其中有些微妙之处
首先move的函数参数T&&是一个指向模板类型参数的右值引用
通过引用折叠此参数可以与任何类型的实参匹配
特别是我们既可以传递给move一个左值也可以传递给它一个右值
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1);  // ok: but after the assigment s1 has indeterminate value

std::move是如何工作的

string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1);  // ok: but after the assigment s1 has indeterminate value
在第一个赋值中传递给move的实参是string的构造函数的右值结果——string"bye!"
当向一个右值引用函数参数传递一个右值时由实参推断出的类型为被引用的类型
因此在std::movestring"bye!"))
- 推断出的T的类型为string。· 
- 因此remove_reference用string进行实例化。· 
- remove_reference<string>的type成员是string。· 
- move的返回类型是string&&。· 
- move的函数参数t的类型为string&&
因此这个调用实例化move<string>即函数
string&& move(string &&t)
函数体返回static_cast<string&&>t)。
t的类型已经是string&&于是类型转换什么都不做
因此此调用的结果就是它所接受的右值引用

std::move是如何工作的

string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1);  // ok: but after the assigment s1 has indeterminate value
现在考虑第二个赋值它调用了std::move()。
在此调用中传递给move的实参是一个左值这样:· 
- 推断出的T的类型为string&string的引用而非普通string)。· 
- 因此remove_reference用string&进行实例化。· 
- remove_reference<string&>的type成员是string。· 
- move的返回类型仍是string&&。· 
- move的函数参数t实例化为string&&&会折叠为string&
因此这个调用实例化move<string&>
这正是我们所寻求的——我们希望将一个右值引用绑定到一个左值
这个实例的函数体返回static_cast<string&&>t)。
在此情况下t的类型为string&cast将其转换为string&&

从一个左值static_cast到一个右值引用是允许的

通常情况下static_cast只能用于其他合法的类型转换
但是这里又有一条针对右值引用的特许规则
- 虽然不能隐式地将一个左值转换为右值引用
  但我们可以用static_cast显式地将一个左值转换为一个右值引用

对于操作右值引用的代码来说将一个右值引用绑定到一个左值的特性允许它们截断左值
有时候例如在我们的StrVec类的reallocate函数中我们知道截断一个左值是安全的
- 一方面通过允许进行这样的转换C++语言认可了这种用法
- 但另一方面通过强制使用static_castC++语言试图阻止我们意外地进行这种转换

虽然我们可以直接编写这种类型转换代码但使用标准库move函数是容易得多的方式
而且统一使用std::move使得我们在程序中查找潜在的截断左值的代码变得很容易

练习

解释下面的循环它来自13.5节中的 StrVec::reallocate:

for (size_t i = 0; i != size(); ++i)
    alloc.construct(dest++, std::move(*elem++));

elem是string * 类型*elem++是令elem指向下一个位置
同时返回当前位置对象的左值引用
然后用std::move函数令左值类型转换为右值引用再传参给alloc.construct函数
令其利用这个右值移动到新的内存中

转发

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数
在此情况下我们需要保持被转发实参的所有性质包括实参类型是否是const的
以及实参是左值还是右值

作为一个例子我们将编写一个函数它接受一个可调用表达式和两个额外实参
我们的函数将调用给定的可调用对象将两个额外参数逆序传递给它
下面是我们的翻转函数的初步模样
// template that takes a callable and two parameters
// and calls the given callable with the parameters ''flipped''
// flip1 is an incomplete implementation: top-level const and references are lost
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
    f(t2, t1);
}
这个函数一般情况下工作得很好
但当我们希望用它调用一个接受引用参数的函数时就会出现问题

转发

void f(int v1, int &v2) // note v2 is a reference
{
    cout << v1 << " " << ++v2 << endl;
}
在这段代码中f改变了绑定到v2的实参的值
但是如果我们通过flip1调用ff所做的改变就不会影响实参
f(42, i);        // f changes its argument i
flip1(f, j, 42); // f called through flip1 leaves j unchanged
问题在于j被传递给flip1的参数t1
此参数是一个普通的非引用的类型int而非int&因此这个flip1调用会实例化为
void flip1(void(*fcn)(int, int&), int t1, int t2);
j的值被拷贝到t1中f中的引用参数被绑定到t1而非j从而其改变不会影响j

定义能保持类型信息的函数参数

为了通过翻转函数传递一个引用我们需要重写函数
使其参数能保持给定实参的左值性”。
更进一步可以想到我们也希望保持参数的const属性

通过将一个函数参数定义为一个指向模板类型参数的右值引用
我们可以保持其对应实参的所有类型信息
而使用引用参数无论是左值还是右值使得我们可以保持const属性
因为在引用类型中的const是底层的
如果我们将函数参数定义为T1&&和T2&&
通过引用折叠就可以保持翻转实参的左值/右值属性

定义能保持类型信息的函数参数

template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
    f(t2, t1);
}
与较早的版本一样如果我们调用flip2fj42),将传递给参数t1一个左值j
但是在flip2中推断出的T1的类型为int&这意味着t1的类型会折叠为int&
由于是引用类型t1被绑定到j上当flip2调用f时f中的引用参数v2被绑定到t1
也就是被绑定到j当f递增v2时它也同时改变了j的值

如果一个函数参数是指向模板类型参数的右值引用如T&&),
它对应的实参的const属性和左值/右值属性将得到保持

定义能保持类型信息的函数参数

这个版本的flip2解决了一半问题它对于接受一个左值引用的函数工作得很好
但不能用于接受右值引用参数的函数例如
void g(int &&i, int& j)
{
    cout << i << " " << j << endl;
}
如果我们试图通过flip2调用g则参数t2将被传递给g的右值引用参数
即使我们传递一个右值给flip2
flip2(g, i, 42); // error: can't initialize int&& from an lvalue
传递给g的将是flip2中名为t2的参数函数参数与其他任何变量一样都是左值表达式
因此flip2中对g的调用将传递给g的右值引用参数一个左值

在调用中使用std::forward保持类型信息

我们可以使用一个名为forward的新标准库设施来传递flip2的参数
它能保持原始实参的类型类似moveforward定义在头文件utility中
与move不同forward必须通过显式模板实参来调用
forward返回该显式实参类型的右值引用forward<T>的返回类型是T&&

通常情况下我们使用forward传递那些定义为模板类型参数的右值引用的函数参数
通过其返回类型上的引用折叠forward可以保持给定实参的左值/右值属性
template <typename Type> intermediary(Type &&arg)
{
    finalFcn(std::forward<Type>(arg));
    // ...
}
本例中我们使用Type作为forward的显式模板实参类型它是从arg推断出来的
由于arg是一个模板类型参数的右值引用Type将表示传递给arg的实参的所有类型信息
- 如果实参是一个右值则Type是一个普通非引用类型forward<Type>将返回Type&&
- 如果实参是一个左值则通过引用折叠Type本身是一个左值引用类型
  在此情况下返回类型是一个指向左值引用类型的右值引用
  再次对forward<Type>的返回类型进行引用折叠将返回一个左值引用类型

在调用中使用std::forward保持类型信息

当用于一个指向模板参数类型的右值引用函数参数T&&
forward会保持实参类型的所有细节

使用forward我们可以再次重写翻转函数
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}
如果我们调用flipgi42),i将以int&类型传递给g42将以int&&类型传递给g

与std::move相同对std::forward不使用using声明是一个好主意

练习

编写你自己版本的翻转函数通过调用接受左值和右值引用参数的函数来测试它

template<typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2)
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

重载与模板

函数模板可以被另一个模板或一个普通非模板函数重载
与往常一样名字相同的函数必须具有不同数量或类型的参数
如果涉及函数模板则函数匹配规则会在以下几方面受到影响
- 对于一个调用其候选函数包括所有模板实参推断成功的函数模板实例。· 
- 候选的函数模板总是可行的因为模板实参推断会排除任何不可行的模板。· 
- 与往常一样可行函数模板与非模板按类型转换如果对此调用需要的话来排序
  当然可以用于函数模板调用的类型转换是非常有限的。· 
- 与往常一样如果恰有一个函数提供比任何其他函数都更好的匹配则选择此函数
  但是如果有多个函数提供同样好的匹配
  - 如果同样好的函数中只有一个是非模板函数则选择此函数
  - 如果同样好的函数中没有非模板函数而有多个函数模板
    且其中一个模板比其他模板更特例化则选择此模板
  - 否则此调用有歧义

正确定义一组重载的函数模板
需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解

编写重载模板

作为一个例子我们将构造一组函数它们在调试中可能很有用
我们将这些调试函数命名为debug_rep每个函数都返回一个给定对象的string表示
我们首先编写此函数的最通用版本将它定义为一个模板接受一个const对象的引用
// print any type we don't otherwise handle
template <typename T> string debug_rep(const T &t)
{
    ostringstream ret; // see § 8.3 
    ret << t; // uses T's output operator to print a representation of t
    return ret.str(); // return a copy of the string to which ret is bound
}
此函数可以用来生成一个对象对应的string表示
该对象可以是任意具备输出运算符的类型

编写重载模板

接下来我们将定义打印指针的debug_rep版本
// print pointers as their pointer value, 
//followed by the object to which the pointer points
// NB: this function will not work properly with char*; see § 16.3 (p. 698)
template <typename T> string debug_rep(T *p)
{
    ostringstream ret;
    ret << "pointer: " << p;         // print the pointer's own value
    if (p)
        ret << " " << debug_rep(*p); // print the value to which p points
    else
        ret << " null pointer";      // or indicate that the p is null
    return ret.str(); // return a copy of the string to which ret is bound
}
此版本生成一个string包含指针本身的值和调用debug_rep获得的指针指向的值
注意此函数不能用于打印字符指针因为IO库为char值定义了一个<<版本
<<版本假定指针表示一个空字符结尾的字符数组并打印数组的内容而非地址值

编写重载模板

我们可以这样使用这些函数
string s("hi");
cout << debug_rep(s) << endl;
对于这个调用只有第一个版本的debug_rep是可行的
第二个debug_rep版本要求一个指针参数但在此调用中我们传递的是一个非指针对象
因此编译器无法从一个非指针实参实例化一个期望指针类型参数的函数模板
因此实参推断失败由于只有一个可行函数所以此函数被调用
如果我们用一个指针调用debug_rep
cout << debug_rep(&s) << endl;
两个函数都生成可行的实例:· 
- debug_repconst string&),
  由第一个版本的debug_rep实例化而来T被绑定到string*。· 
- debug_repstring*),
  由第二个版本的debug_rep实例化而来T被绑定到string

第二个版本的debug_rep的实例是此调用的精确匹配
第一个版本的实例需要进行普通指针到const指针的转换
正常函数匹配规则告诉我们应该选择第二个模板实际上编译器确实选择了这个版本

多个可行模板

作为另外一个例子考虑下面的调用
const string *sp = &s;
cout << debug_rep(sp) << endl;
此例中的两个模板都是可行的而且两个都是精确匹配:· 
- debug_repconst string&),
  由第一个版本的debug_rep实例化而来T被绑定到string*。· 
- debug_repconst string*),
  由第二个版本的debug_rep实例化而来T被绑定到const string

在此情况下正常函数匹配规则无法区分这两个函数
我们可能觉得这个调用将是有歧义的但是根据重载函数模板的特殊规则
此调用被解析为debug_repT*),更特例化的版本

设计这条规则的原因是没有它将无法对一个const的指针调用指针版本的debug_rep
问题在于模板debug_repconst T&本质上可以用于任何类型包括指针类型
此模板比debug_repT*)更通用后者只能用于指针类型
没有这条规则传递const的指针的调用永远是有歧义的

当有多个重载模板对一个调用提供同样好的匹配时应选择最特例化的版本

非模板和模板重载

作为下一个例子
我们将定义一个普通非模板版本的debug_rep来打印双引号包围的string
// print strings inside double quotes
string debug_rep(const string &s)
{
    return '"' + s + '"';
}
现在当我们对一个string调用debug_rep时
string s("hi");
cout << debug_rep(s) << endl;
有两个同样好的可行函数:· 
- debug_rep<string>const string&),第一个模板T被绑定到string*。· 
- debug_repconst string&),普通非模板函数
在本例中两个函数具有相同的参数列表因此显然两者提供同样好的匹配
但是编译器会选择非模板版本当存在多个同样好的函数模板时
编译器选择最特例化的版本出于相同的原因一个非模板函数比一个函数模板更好

对于一个调用
如果一个非函数模板与一个函数模板提供同样好的匹配则选择非模板版本

重载模板和类型转换

还有一种情况我们到目前为止尚未讨论C风格字符串指针和字符串字面常量
现在有了一个接受string的debug_rep版本我们可能期望一个传递字符串的调用会
匹配这个版本但是考虑这个调用
cout << debug_rep("hi world!") << endl; // calls debug_rep(T*)
本例中所有三个debug_rep版本都是可行的:· 
- debug_repconst T&),T被绑定到char[10]。· 
- debug_repT*),T被绑定到const char。· 
- debug_repconst string&),要求从const char到string的类型转换
对给定实参来说两个模板都提供精确匹配
- 第二个模板需要进行一次许可的数组到指针的转换
- 而对于函数匹配来说这种转换被认为是精确匹配非模板版本是可行的
  但需要进行一次用户定义的类型转换因此它没有精确匹配那么好
所以两个模板成为可能调用的函数T版本更加特例化编译器会选择它

重载模板和类型转换

如果我们希望将字符指针按string处理可以定义另外两个非模板重载版本
// convert the character pointers to string and call the string version of debug_rep
string debug_rep(char *p)
{
    return debug_rep(string(p));
}
string debug_rep(const char *p)
{
    return debug_rep(string(p));
}

缺少声明可能导致程序行为异常

值得注意的是为了使char版本的debug_rep正确工作在定义此版本时
debug_repconst string&的声明必须在作用域中
否则就可能调用错误的debug_rep版本
template <typename T> string debug_rep(const T &t);
template <typename T> string debug_rep(T *p);
// the following declaration must be in scope
// for the definition of debug_rep(char*) to do the right thing
string debug_rep(const string &);
string debug_rep(char *p)
{
    // if the declaration for the version that takes a const string& is not in scope
    // the return will call debug_rep(const T&) with T instantiated to string
    return debug_rep(string(p));
}
通常如果使用了一个忘记声明的函数代码将编译失败
但对于重载函数模板的函数而言则不是这样
如果编译器可以从模板实例化出与调用匹配的版本则缺少的声明就不重要了
在本例中如果忘记了声明接受string参数的debug_rep版本
编译器会默默地实例化接受const T&的模板版本

在定义任何函数之前记得声明所有重载的函数版本
这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本

练习

编写你自己版本的 debug_rep 函数

template<typename T> std::string debug_rep(const T& t)
{
    std::ostringstream ret;
    ret << t;
    return ret.str();
}

template<typename T> std::string debug_rep(T* p)
{
    std::ostringstream ret;
    ret << "pointer: " << p;
    if(p)
        ret << " " << debug_rep(*p);
    else
        ret << " null pointer";
    return ret.str();
}

练习

解释下面每个调用会发生什么
template <typename T> void f(T);
template <typename T> void f(const T*);
template <typename T> void g(T);
template <typename T> void g(T*);
int i = 42, *p = &i;
const int ci = 0, *p2 = &ci;
g(42); g(p); g(ci); g(p2);
f(42); f(p); f(ci); f(p2);

    g(42);        //g(T) ,T:int
    g(p);         //g(T*),T:int
    g(ci);        //g(T) ,T:int  
    g(p2);        //g(T*),T:const int    
    f(42);        //f(T) ,T:int
    f(p);         //f(T) ,T:int*
    f(ci);        //f(T) ,T:int 
    f(p2);        //f(const T*),T:int

练习

定义上一个练习中的函数令它们打印一条身份信息
运行该练习中的代码如果函数调用的行为与你预期不符确定你理解了原因
#include<iostream>
#include<string>
template <typename T>
void f(T t)
{
    std::cout << "void f(T t)" << std::endl;
}

template <typename T>
void f(const T * t)
{
    std::cout << "void f(const T * t)" << std::endl;
}

练习

template <typename T>
void g(T t)
{
    std::cout << "void g(T t)" << std::endl;
}

template <typename T>
void g(T * t)
{
    std::cout << "void g(T * t)" << std::endl;
}

int main()
{
    int i = 42, *p = &i;
    const int ci = 0, *p2 = &ci;
    g(42); g(p); g(ci); g(p2);
    f(42); f(p); f(ci); f(p2);
    return 0;
}

练习

输出:
void g(T t)
void g(T * t)
void g(T t)
void g(T * t)
void f(T t)
void f(T t)
void f(T t)
void f(const T * t)

可变参数模板

一个可变参数模板variadic template就是一个
接受可变数目参数的模板函数或模板类
可变数目的参数被称为参数包parameter packet)。存在两种参数包
- 模板参数包template parameter packet),表示零个或多个模板参数
- 函数参数包function parameter packet),表示零个或多个函数参数

可变参数模板

我们用一个省略号来指出一个模板参数或函数参数表示一个包在一个
模板参数列表中class或typename指出接下来的参数表示零个或多个类型的列表
一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表
在函数参数列表中
如果一个参数的类型是一个模板参数包则此参数也是一个函数参数包例如
// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
声明了foo是一个可变参数函数模板
它有一个名为T的类型参数和一个名为Args的模板参数包
这个包表示零个或多个额外的类型参数
foo的函数参数列表包含一个const &类型的参数指向T的类型
还包含一个名为rest的函数参数包此包表示零个或多个函数参数

可变参数模板

与往常一样编译器从函数的实参推断模板参数类型
对于一个可变参数模板编译器还会推断包中参数的数目例如给定下面的调用
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);    // three parameters in the pack
foo(s, 42, "hi");    // two parameters in the pack
foo(d, s);           // one parameter in the pack
foo("hi");           // empty pack
编译器会为foo实例化出四个不同的版本
void foo(const int&, const string&, const int&, double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
在每个实例中T的类型都是从第一个实参的类型推断出来的
剩下的实参如果有的话提供函数额外实参的数目和类型

sizeof…运算符

当我们需要知道包中有多少元素时可以使用sizeof运算符
类似sizeofsizeof也返回一个常量表达式而且不会对其实参求值
template<typename ... Args> void g(Args ... args) {
    cout << sizeof...(Args) << endl;  // number of type parameters
    cout << sizeof...(args) << endl;  // number of function parameters
}

练习

调用本节中的每个 foo确定 sizeof...(Args)  sizeof...(rest)分别返回什么
#include <iostream>
using namespace std;

template <typename T, typename ... Args>
void foo(const T &t, const Args& ... rest){
    cout << "sizeof...(Args): " << sizeof...(Args) << endl;
    cout << "sizeof...(rest): " << sizeof...(rest) << endl;
};

void test_param_packet(){
    int i = 0;
    double d = 3.14;
    string s = "how now brown cow";

    foo(i, s, 42, d);
    foo(s, 42, "hi");
    foo(d, s);
    foo("hi");
}

练习

int main(){
    test_param_packet();
    return 0;
}


结果
sizeof...(Args): 3
sizeof...(rest): 3
sizeof...(Args): 2
sizeof...(rest): 2
sizeof...(Args): 1
sizeof...(rest): 1
sizeof...(Args): 0
sizeof...(rest): 0

练习

编写一个程序验证上一题的答案

参考上题

编写可变参数函数模板

我们可以使用一个initializer_list来定义一个可接受可变数目实参的函数
但是所有实参必须具有相同的类型或它们的类型可以转换为同一个公共类型)。
当我们既不知道想要处理的实参的数目也不知道它们的类型时可变参数函数是很有用的
作为一个例子我们将定义一个函数它类似较早的error_msg函数
差别仅在于新函数实参的类型也是可变的
我们首先定义一个名为print的函数它在一个给定流上打印给定实参列表的内容

编写可变参数函数模板

可变参数函数通常是递归的
第一步调用处理包中的第一个实参然后用剩余实参调用自身
我们的print函数也是这样的模式
每次递归调用将第二个实参打印到第一个实参表示的流中
为了终止递归我们还需要定义一个非可变参数的print函数
它接受一个流和一个对象
// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template<typename T>
ostream &print(ostream &os, const T &t)
{
    return os << t; // no separator after the last element in the pack
}
// this version of print will be called for all but the last element in the pack
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
    os << t << ", ";           // print the first argument
    return print(os, rest...); // recursive call; print the other arguments
}

编写可变参数函数模板

第一个版本的print负责终止递归并打印初始调用中的最后一个实参
第二个版本的print是可变参数版本它打印绑定到t的实参
并调用自身来打印函数参数包中的剩余值

这段程序的关键部分是可变参数函数中对print的调用
return print(os, rest...); // recursive call; print the other arguments
我们的可变参数版本的print函数接受三个参数
一个ostream&一个const T&和一个参数包而此调用只传递了两个实参
其结果是rest中的第一个实参被绑定到t剩余实参形成下一个print调用的参数包
因此在每个调用中包中的第一个实参被移除成为绑定到t的实参给定
print(cout, i, s, 42);  // two parameters in the pack
递归会执行如下
调用t                                       rest...
print(cout, i, s, 42)           i           s,42
print(cout, s, 42)              s           42
print(cout, 42)调用非可变参数版本print
前两个调用只能与可变参数版本的print匹配非可变参数版本是不可行的
因为这两个调用分别传递四个和三个实参而非可变参数print只接受两个实参

编写可变参数函数模板

对于最后一次递归调用printcout42),两个print版本都是可行的
这个调用传递两个实参第一个实参的类型为ostream&
因此可变参数版本的print可以实例化为只接受两个参数
一个是ostream&参数另一个是const T&参数

对于最后一个调用两个函数提供同样好的匹配
但是非可变参数模板比可变参数模板更特例化因此编译器选择非可变参数版本

当定义可变参数版本的print时非可变参数版本的声明必须在作用域中
否则可变参数版本会无限递归

练习

编写你自己版本的 print 函数并打印一个两个及五个实参来测试它
要打印的每个实参都应有不同的类型 
#include<iostream>
#include<string>

template <typename T>
std::ostream & print(std::ostream & os, const T & t)
{
    return os << t;
} 

template <typename T, typename ... Args>
std::ostream & print(std::ostream & os, const T & t, const Args & ... rest)
{
    os << t << ", ";
    return print(os, rest...);
}

练习

int main()
{
    print(std::cout, 1) << std::endl;
    print(std::cout, 1, "123") << std::endl;
    print(std::cout, 1, "123", 3.123, std::string("qwert"), -5) << std::endl;
    return 0;
}

练习

如果我们对一个没 << 运算符的类型调用 print会发生什么

无法通过编译

练习

如果我们的可变参数版本 print 的定义之后声明非可变参数版本
解释可变参数的版本会如何执行

无限递归,最终最后一步
error: no matching function for call to 'print(std::ostream&)'

包扩展

对于一个参数包除了获取其大小外我们能对它做的唯一的事情就是扩展expand
当扩展一个包时我们还要提供用于每个扩展元素的模式pattern)。
扩展一个包就是将它分解为构成的元素对每个元素应用模式获得扩展后的列表
我们通过在模式右边放一个省略号(…)来触发扩展操作
例如我们的print函数包含两个扩展
template <typename T, typename... Args>
ostream &
print(ostream &os, const T &t, const Args&... rest)// expand Args
{
    os << t << ", ";
    return print(os, rest...);                     // expand rest
}
第一个扩展操作扩展模板参数包为print生成函数参数列表
第二个扩展操作出现在对print的调用中此模式为print调用生成实参列表

包扩展

对Args的扩展中编译器将模式const Arg&应用到模板参数包Args中的每个元素
因此此模式的扩展结果是一个逗号分隔的零个或多个类型的列表
每个类型都形如const type&例如
print(cout, i, s, 42);  // two parameters in the pack
最后两个实参的类型和模式一起确定了尾置参数的类型此调用被实例化为
ostream&
print(ostream&, const int&, const string&, const int&);
第二个扩展发生在对print的递归调用中
在此情况下模式是函数参数包的名字即rest)。
此模式扩展出一个由包中元素组成的逗号分隔的列表因此这个调用等价于
print(os, s, 42);

理解包扩展

print中的函数参数包扩展仅仅将包扩展为其构成元素C++语言还允许更复杂的扩展模式
我们可以编写第二个可变参数函数对其每个实参调用debug_rep
然后调用print打印结果string
// call debug_rep on each argument in the call to print
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
    // print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an)
    return print(os, debug_rep(rest)...);
}
这个print调用使用了模式debug_regrest)。
此模式表示我们希望对函数参数包rest中的每个元素调用debug_rep
扩展结果将是一个逗号分隔的debug_rep调用列表下面调用
errormsg(cerr,fcnname, code.num(),otherdata,"other",item);
就好像我们这样编写代码一样
print(cerr, debug_rep(fcnname), debug_rep(code.num()),
            debug_rep(otherdata), debug_rep("otherdata"),
            debug_rep(item));

理解包扩展

与之相对下面的模式会编译失败
// passes the pack to debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // error: no matching function to call
这段代码的问题是我们在debug_rep调用中扩展了rest它等价于
print(cerr, debug_rep(fcnname, code.num(),
                      otherdata, "otherdata", item));
在这个扩展中我们试图用一个五个实参的列表来调用debug_rep
但并不存在与此调用匹配的debug_rep版本debug_rep函数不是可变参数的
而且没有哪个debug_rep版本接受五个参数

扩展中的模式会独立地应用于包中的每个元素

练习

编写并测试可变参数版本的 errormsg

template<typename... args>
std::ostream& errormsg(std::ostream& os, const args... rest)
{
    return print(os, debug_rep(rest)...);
}

练习

比较你的可变参数版本的 errormsg 和6.2.6节中的 error_msg函数
两种方法的优点和缺点各是什么

可变参数版本有更好的灵活性

转发参数包

在新标准下我们可以组合使用可变参数模板与forward机制来编写函数
实现将其实参不变地传递给其他函数
作为例子我们将为strvec类添加一个emplace_back成员
标准库容器的emplace_back成员是一个可变参数成员模板
它用其实参在容器管理的内存空间中直接构造一个元素

我们为strvec设计的emplace_back版本也应该是可变参数的
因为string有多个构造函数参数各不相同
由于我们希望能使用string的移动构造函数
因此还需要保持传递给emplace_back的实参的所有类型信息

转发参数包

如我们所见保持类型信息是一个两阶段的过程
首先为了保持实参中的类型信息必须将emplace_back的函数参数定义为
模板类型参数的右值引用
class strvec {
public:
    template <class... args> void emplace_back(args&&...);
    // remaining members as in § 13.5 
};
模板参数包扩展中的模式是&&
意味着每个函数参数将是一个指向其对应实参的右值引用

转发参数包

其次当emplace_back将这些实参传递给construct时
我们必须使用forward来保持实参的原始类型
template <class... args>
inline
void strvec::emplace_back(args&&... args)
{
    chk_n_alloc(); // reallocates the strvec if necessary
    alloc.construct(first_free++, std::forward<args>(args)...);
}

转发参数包

emplace_back的函数体调用了chk_n_alloc来确保有足够的空间容纳一个新元素
然后调用了construct在first_free指向的位置中创建了一个元素
construct调用中的扩展为
std::forward<args>(args)...
它既扩展了模板参数包args也扩展了函数参数包args此模式生成如下形式的元素
std::forward<T_i>(t_i)
其中T_i表示模板参数包中第i个元素的类型t_i表示函数参数包中第i个元素
例如假定svec是一个strvec如果我们调用
svec.emplace_back(10, 'c'); // adds cccccccccc as a new last element
construct调用中的模式会扩展出
std::forward<int>(10), std::forward<char>(c)
通过在此调用中使用forward我们保证如果用一个右值调用emplace_back
则construct也会得到一个右值例如在下面的调用中
svec.emplace_back(s1 + s2); // uses the move constructor
传递给emplace_back的实参是一个右值它将以如下形式传递给construct
std::forward<string>(string("the end"))
forward<string>的结果类型是string&&因此construct将得到一个右值引用实参
construct会继续将此实参传递给string的移动构造函数来创建新元素

建议:转发和可变参数模板

可变参数函数通常将它们的参数转发给其他函数
这种函数通常具有与我们的emplace_back函数一样的形式
// fun has zero or more parameters each of which is
// an rvalue reference to a template parameter type
template<typename... args>
void fun(args&&... args) // expands args as a list of rvalue references
{
    // the argument to work expands both args and args
    work(std::forward<args>(args)...);
}
这里我们希望将fun的所有实参转发给另一个名为work的函数
假定由它完成函数的实际工作类似emplace_back中对construct的调用
work调用中的扩展既扩展了模板参数包也扩展了函数参数包

由于fun的参数是右值引用因此我们可以传递给它任意类型的实参
由于我们使用std::forward传递这些实参
因此它们的所有类型信息在调用work时都会得到保持

练习

为你的 strvec 类及你为16.1.2节练习中编写的 vec 类添加 emplace_back 函数

template<typename t>        //for the class  template
template<typename... args>  //for the member template
inline void
vec<t>::emplace_back(args&&...args)
{
    chk_n_alloc();
    alloc.construct(first_free++, std::forward<args>(args)...);
}

练习

假定 s 是一个 string解释调用 svec.emplace_back(s)会发生什么

s是一个左值经过包扩展以如下形式传给construct
std::forward<string>(s)
forward<string>结果类型是string&,因此construct将得到一个左值引用实参
它继续将此参数传递给string的拷贝构造函数来创建新元素

练习

解释 make_shared 是如何工作的

make_shared是一个可变参数模板的函数它的第一个模板类型是需要手动指定的
表示了构造的类型后面的是可变参数模板函数形参是可变参数模板类型
函数内容是new一个对象用std::forward保持参数类型
然后用指针来构造一个shared_ptr并返回

练习

定义你自己版本的 make_shared

template <typename t, typename ... args>
auto make_shared(args&&... args) -> std::shared_ptr<t>
{
    return std::shared_ptr<t>(new t(std::forward<args>(args)...));
}

模板特例化

编写单一模板使之对任何可能的模板实参都是最适合的都能实例化
这并不总是能办到在某些情况下通用模板的定义对特定类型是不适合的
通用定义可能编译失败或做得不正确其他时候
我们也可以利用某些特定知识来编写更高效的代码而不是从通用模板实例化
当我们不能或不希望使用模板版本时可以定义类或函数模板的一个特例化版本

模板特例化

我们的compare函数是一个很好的例子
它展示了函数模板的通用定义不适合一个特定类型即字符指针的情况
我们希望compare通过调用strcmp比较两个字符指针而非比较指针值
实际上我们已经重载了compare函数来处理字符串字面常量
// first version; can compare any two types
template <typename t> int compare(const t&, const t&);
// second version to handle string literals
template<size_t n, size_t m>
int compare(const char (&)[n], const char (&)[m]);

模板特例化

但是只有当我们传递给compare一个字符串字面常量或者一个数组时
编译器才会调用接受两个非类型模板参数的版本
如果我们传递给它字符指针就会调用第一个版本
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);      // calls the first template
compare("hi", "mom"); // calls the template with two nontype parameters
我们无法将一个指针转换为一个数组的引用
因此当参数是p1和p2时第二个版本的compare是不可行的

为了处理字符指针而不是数组),
可以为第一个版本的compare定义一个模板特例化template specialization版本
一个特例化版本就是模板的一个独立的定义
在其中一个或多个模板参数被指定为特定的类型

定义函数模板特例化

当我们特例化一个函数模板时必须为原模板中的每个模板参数都提供实参
为了指出我们正在实例化一个模板应使用关键字template后跟一个空尖括号对<>)。
空尖括号指出我们将为原模板的所有模板参数提供实参
// special version of compare to handle pointers to character arrays
template <>
int compare(const char* const &p1, const char* const &p2)
{
    return strcmp(p1, p2);
}
理解此特例化版本的困难之处是函数参数类型当我们定义一个特例化版本时
函数参数类型必须与一个先前声明的模板中对应的类型匹配本例中我们特例化
template <typename t> int compare(const t&, const t&);
其中函数参数为一个const类型的引用
类似类型别名模板参数类型指针及const之间的相互作用会令人惊讶

我们希望定义此函数的一个特例化版本其中t为const char*。
我们的函数要求一个指向此类型const版本的引用
一个指针类型的const版本是一个常量指针而不是指向const类型的指针
我们需要在特例化版本中使用的类型是const char  const &
即一个指向const char的const指针的引用

函数重载与模板特例化

当定义函数模板的特例化版本时我们本质上接管了编译器的工作
我们为原模板的一个特殊实例提供了定义
重要的是要弄清一个特例化版本本质上是一个实例而非函数名的一个重载版本

特例化的本质是实例化一个模板而非重载它因此特例化不影响函数匹配

我们将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数
会影响到函数匹配例如我们已经定义了两个版本的compare函数模板
一个接受数组引用参数另一个接受const t&
我们还定义了一个特例化版本来处理字符指针这对函数匹配没有影响
当我们对字符串字面常量调用compare时
compare("hi", "mom")
对此调用两个函数模板都是可行的且提供同样好的即精确的匹配
但是接受字符数组参数的版本更特例化因此编译器会选择它

如果我们将接受字符指针的compare版本定义为一个普通的非模板函数
而不是模板的一个特例化版本),此调用的解析就会不同
在此情况下将会有三个可行的函数两个模板和非模板的字符指针版本
所有三个函数都提供同样好的匹配如前所述
当一个非模板函数提供与函数模板同样好的匹配时编译器会选择非模板版本

关键概念:普通作用域规则应用于特例化

为了特例化一个模板原模板的声明必须在作用域中
而且在任何使用模板实例的代码之前特例化版本的声明也必须在作用域中

对于普通类和函数丢失声明的情况通常很容易发现
- 编译器将不能继续处理我们的代码
但是如果丢失了一个特例化版本的声明编译器通常可以用原模板生成代码
由于在丢失特例化版本时编译器通常会实例化原模板
很容易产生模板及其特例化版本声明顺序导致的错误而这种错误又很难查找

如果一个程序使用一个特例化版本
而同时原模板的一个实例具有相同的模板实参集合就会产生错误
但是这种错误编译器又无法发现

模板及其特例化版本应该声明在同一个头文件中
所有同名模板的声明应该放在前面然后是这些模板的特例化版本

类模板特例化

除了特例化函数模板我们还可以特例化类模板
作为一个例子我们将为标准库hash模板定义一个特例化版本
可以用它来将sales_data对象保存在无序容器中
默认情况下无序容器使用hash<key_type>来组织其元素
为了让我们自己的数据类型也能使用这种默认组织方式
必须定义hash模板的一个特例化版本一个特例化hash类必须定义
- 一个重载的调用运算符它接受一个容器关键字类型的对象返回一个size_t。·
- 两个类型成员result_type和argument_type
  分别调用运算符的返回类型和参数类型。· 
- 默认构造函数和拷贝赋值运算符.可以隐式定义

在定义此特例化版本的hash时唯一复杂的地方是
必须在原模板定义所在的命名空间中特例化它

类模板特例化

我们可以向命名空间添加成员为了达到这一目的首先必须打开命名空间
// open the std namespace so we can specialize std::hash
namespace std {
}  // close the std namespace; note: no semicolon after the close curly
花括号对之间的任何定义都将成为命名空间std的一部分
下面的代码定义了一个能处理sales_data的特例化hash版本
namespace std {
template <>             // we're defining a specialization with
struct hash<sales_data> // the template parameter of sales_data
{
    // the type used to hash an unordered container must define these types
    typedef size_t result_type;
    typedef sales_data argument_type; // by default, this type needs ==
    size_t operator()(const sales_data& s) const;
    // our class uses synthesized copy control and default constructor
};

类模板特例化

size_t
hash<sales_data>::operator()(const sales_data& s) const
{
    return hash<string>()(s.bookno) ^
           hash<unsigned>()(s.units_sold) ^
           hash<double>()(s.revenue);
}
} // close the std namespace; note: no semicolon after the close curly
我们的hash<sales_data>定义以template<>开始指出我们正在定义一个全特例化的模板
我们正在特例化的模板名为hash而特例化版本为hash<sales_data>
接下来的类成员是按照特例化hash的要求而定义的

类似其他任何类我们可以在类内或类外定义特例化版本的成员
本例中就是在类外定义的重载的调用运算符必须为给定类型的值定义一个哈希函数
对于一个给定值任何时候调用此函数都应该返回相同的结果
一个好的哈希函数对不相等的对象几乎总是应该产生不同的结果

类模板特例化

在本例中我们将定义一个好的哈希函数的复杂任务交给了标准库
标准库为内置类型和很多标准库类型定义了hash类的特例化版本
我们使用一个未命名的hash<string>对象来生成bookno的哈希值
用一个hash<unsigned>对象来生成units_sold的哈希值
用一个hash<double>对象来生成revenue的哈希值
我们将这些结果进行异或运算形成给定sales_data对象的完整的哈希值

值得注意的是我们的hash函数计算所有三个数据成员的哈希值
从而与我们为sales_data定义的operator==是兼容的默认情况下
为了处理特定关键字类型无序容器会组合使用key_type对应的特例化hash版本
和key_type上的相等运算符

类模板特例化

假定我们的特例化版本在作用域中当将sales_data作为容器的关键字类型时
编译器就会自动使用此特例化版本
// uses hash<sales_data> and sales_data operator==from § 14.3.1 
unordered_multiset<sales_data> sdset;
由于hash<sales_data>使用sales_data的私有成员
我们必须将它声明为sales_data的友元
template <class t> class std::hash;  // needed for the friend declaration
class sales_data {
friend class std::hash<sales_data>;
    // other members as before
};
这段代码指出特殊实例hash<sales_data>是sales_data的友元
由于此实例定义在std命名空间中我们必须记得在friend声明中应使用std::hash

为了让sales_data的用户能使用hash的特例化版本
我们应该在sales_data的头文件中定义该特例化版本

类模板部分特例化

与函数模板不同类模板的特例化不必为所有模板参数提供实参
我们可以只指定一部分而非所有模板参数或是参数的一部分而非全部特性
一个类模板的部分特例化partial specialization本身是一个模板
使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参

我们只能部分特例化类模板而不能部分特例化函数模板

在16.2.3节中我们介绍了标准库remove_reference类型
该模板是通过一系列的特例化版本来完成其功能的
// original, most general template
template <class t> struct remove_reference {
    typedef t type;
};
// partial specializations that will be used for lvalue and rvalue references
template <class t> struct remove_reference<t&>  //references
    { typedef t type; };
template <class t> struct remove_reference<t&&> //references
    { typedef t type; };
第一个模板定义了最通用的模板它可以用任意类型实例化
它将模板实参作为type成员的类型接下来的两个类是原始模板的部分特例化版本

类模板部分特例化

由于一个部分特例化版本本质是一个模板与往常一样我们首先定义模板参数
类似任何其他特例化版本部分特例化版本的名字与原模板的名字相同
对每个未完全确定类型的模板参数在特例化版本的模板参数列表中都有一项与之对应
在类名之后我们为要特例化的模板参数指定实参这些实参列于模板名之后的尖括号中
这些实参与原始模板中的参数按位置对应

部分特例化版本的模板参数列表是
- 原始模板的参数列表的一个子集或者是一个特例化版本
在本例中特例化版本的模板参数的数目与原始模板相同但是类型不同
两个特例化版本分别用于左值引用和右值引用类型
int i;
// decltype(42) is int, uses the original template
remove_reference<decltype(42)>::type a;
// decltype(i) is int&, uses first (T&) partial specialization
remove_reference<decltype(i)>::type b;
// decltype(std::move(i)) is int&&, uses second (i.e., T&&) partial specialization
remove_reference<decltype(std::move(i))>::type c;

三个变量ab和c均为int类型

特例化成员而不是类

我们可以只特例化特定成员函数而不是特例化整个模板
例如如果Foo是一个模板类包含一个成员Bar我们可以只特例化该成员
template <typename T> struct Foo {
    Foo(const T &t = T()): mem(t) { }
    void Bar() { /* ... */ }
    T mem;
    // other members of Foo
};
template<>           // we're specializing a template
void Foo<int>::Bar(){ // we're specializing the Bar member of Foo<int>
     // do whatever specialized processing that applies to ints
}
本例中我们只特例化Foo<int>类的一个成员其他成员将由Foo模板提供
Foo<string> fs;  //instantiates Foo<string>::Foo()
fs.Bar();        //instantiates Foo<string>::Bar()
Foo<int> fi;     //instantiates Foo<int>::Foo()
fi.Bar();        //uses our specialization of Foo<int>::Bar()
当我们用int之外的任何类型使用Foo时其成员像往常一样进行实例化
当我们用int使用Foo时Bar之外的成员像往常一样进行实例化
如果我们使用Foo<int>的成员Bar则会使用我们定义的特例化版本

练习

定义你自己版本的 hash<Sales_data>,
并定义一个 Sales_data 对象的 unorder_multise
将多条交易记录保存到容器中并打印其内容

见book/ch16/ex16.62
编译命令 g++ main.cpp Sales_data.cpp && ./a.out 

练习

定义一个函数模版统计一个给定值在一个vecor中出现的次数测试你的函数
分别传递给它一个double的vector一个int的vector以及一个string的vector
#include <iostream>
#include <vector>
#include <cstring>
using std::string;
// template
template<typename T>
std::size_t  count(const std::vector<T> & vec, T   value)
{
    auto count = 0;
    for(auto const& elem  : vec)
        if(value == elem) ++count;
    return count;
}

练习

// template specialization
template<>
std::size_t count (const std::vector<const char*> & vec, const char*  value)
{
    auto count = 0;
    for(auto const& elem : vec)
        if(0 == strcmp(value, elem)) ++count;
    return count;
}

练习

int main()
{
    // for ex16.63
    std::vector<double> vd = { 1.1, 1.1, 2.3, 4 };
    std::cout << count(vd, 1.1) << std::endl;

    std::vector<int> vi = {1,2,3,1};
    std::cout << count(vi, 1) << std::endl;

    std::vector<string> vs = {"abc","ab","abc"};
    std::cout << count(vs, std::string("abc") )<< std::endl;

    // for ex16.64
    std::vector<const char*> vcc = { "alan", "alan", "alan", "alan", "moophy" };
    std::cout << count(vcc, "alan") << std::endl;

    return 0;
}

练习

为上一题的模版编写特例化版本来处理vector<const char*>编写程序使用这个特例化版本

参考之前

练习

在16.3节中我们定义了两个重载的 debug_rep 版本
一个接受 const char* 参数另一个接受 char * 参数
将这两个函数重写为特例化版本


#include<iostream>
#include<string>
#include<sstream>
template<typename T> std::string debug_rep(const T& t)
{
    std::ostringstream ret;
    ret << t;
    return ret.str();
}

练习

template<typename T> std::string debug_rep(T* p)
{
    std::ostringstream ret;
    ret << "pointer: " << p;

    if(p)
        ret << " " << debug_rep(*p);
    else
        ret << " null pointer";

    return ret.str();
}

练习

template <>
std::string debug_rep(char * p)
{
    std::cout<<"debug_rep(char * p)";
    return debug_rep(std::string(p));
}
template <>
std::string debug_rep(const char * p)
{
    std::cout<<"debug_rep(const char * p)";
    return debug_rep(std::string(p));
}
int main()
{
    char p[] = "alan";
    std::cout << debug_rep(p) << "\n";
    std::cout << debug_rep("abc") << "\n";
    return 0;
}

练习

重载debug_rep 函数与特例化它相比有何优点和缺点

重载函数会改变函数匹配
特例化不影响函数匹配

优点
特例化不会影响重载函数的匹配规则
缺点
不能实现重载函数的匹配规则优先级的提升
需要严格遵守模板的定义规则

练习

定义特例化版本会影响 debug_rep 的函数匹配吗如果不影响为什么

不影响特例化是模板的一个实例并没有重载函数

小结

模板是C++语言与众不同的特性也是标准库的基础
一个模板就是一个编译器用来生成特定类类型或函数的蓝图
生成特定类或函数的过程称为实例化我们只编写一次模板
就可以将其用于多种类型和值编译器会为每种类型和值进行模板实例化

我们既可以定义函数模板也可以定义类模板
标准库算法都是函数模板标准库容器都是类模板

显式模板实参允许我们固定一个或多个模板参数的类型或值
对于指定了显式模板实参的模板参数可以应用正常的类型转换

一个模板特例化就是一个用户提供的模板实例
它将一个或多个模板参数绑定到特定类型或值上
当我们不能或不希望将模板定义用于某些特定类型时特例化非常有用

最新C++标准的一个主要部分是可变参数模板
一个可变参数模板可以接受数目和类型可变的参数
可变参数模板允许我们编写像容器的emplace成员和
标准库make_shared函数这样的函数实现将实参传递给对象的构造函数

实践课

  • 从课程主页 cpp.njuer.org 打开 《面向对象编程基础》 实验课 九 模板 https://developer.aliyun.com/adc/scenario/35d4a17e964b479ebffe7cc4e5d7eae3
  • 使用g++编译代码
  • 编辑一个 readme.md 文档,键入本次实验心得.
  • 使用git进行版本控制 可使用之前的gitee代码仓库
      - 云服务器elastic compute service,简称ecs
      - aliyun linux 2是阿里云推出的 linux 发行版
      - vim是从vi发展出来的一个文本编辑器.
      - g++ 是c++编译器
    
习题
用类模板定义一个类属的队列类
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekn:  mkdir -p weekn ; mv 文件名 weekn;)

vim test.cpp
g++ ./test.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "your name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN

git push 到你的git仓库

git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md

提交

  • 截图或复制文字,提交到群作业.
  • 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
  • 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo

关于使用tmux

sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a, 
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0

vim 共分为三种模式

图片1

- 命令模式
  - 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
    - i 切换到输入模式,以输入字符.
    - x 删除当前光标所在处的字符.
    - : 切换到底线命令模式,可输入命令.
- 输入模式
  - 命令模式下按下i就进入了输入模式.
    - ESC,退出输入模式,切换到命令模式
- 底线命令模式
  - 命令模式下按下:英文冒号就进入了底线命令模式.
    - wq 保存退出

vim 常用按键说明

除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下
- 光标移动
  - j下 k上 h左 l右
  - w前进一个词 b后退一个词
  - Ctrl+d 向下半屏  ctrl+u 向上半屏
  - G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
  - dd 删一行 ndd 删n行
  - yy 复制一行 nyy复制n行
  - p将复制的数据粘贴在下一行 P粘贴到上一行
  - u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
  - /word 向下找word     word 向上找
  - n重复搜索 N反向搜索
  - :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
    取代为 word2

vim 常用按键说明

底线命令模式下
- :set nu    显示行号
- :set nonu    取消行号
- :set paste    粘贴代码不乱序
把caps lock按键映射为ctrl,能提高编辑效率.

Markdown 文档语法

# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
  - 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)

|表格标题1|表格标题2|
 |-|-|
|内容1|内容2|

谢谢