1、面向对象编程(OOP)和泛型编程 都能处理在编写程序时 不知道类型的情况。不同之处在于:OOP 能处理类型 在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了
2、容器、迭代器 和 算法 都是泛型编程的例子。编写一个泛型程序时,是独立于任何特定类型 来编写代码的。当使用一个泛型程序时,提供 类型或值,程序实例 可在其上运行
例如,标准库为每个容器提供了单一的、泛型的定义,如 vector。可以用这个定义 定义很多类型的 vector。它们的差异 就在于包含的元素类型不同
3、模板是泛型编程的基础。不必了解模板是如何定义的 就能使用它们
4、一个模板 就是一个创建类或函数的蓝图或说公式。当使用一个vector这样的泛型类,或find这样的泛型函数时,提供足够的信息,将蓝图转换为 特定的类或函数。这种转换发生在 编译时
1、定义模板
1、希望 编写一个函数来比较两个值,并指出第一个值 是小于、等于 还是大于第二个值。可能想要定义多个函数,每个函数比较一种给定类型的值。我们的初次尝试 可能定义多个重载函数,这两个函数几乎是相同的,唯一的差异是参数的类型,函数体则完全一样
// 如果两个值相等,返回0,如果v1小返回-1,如果v2小返回1
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的所有类型。如果希望 能在用户提供的类型上使用此函数,就失效了
1.1 函数模板
1、定义一个通用的函数模板,一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。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开始,后跟一个模板参数列表,这是一堆写于分隔符<>中的 一个或多个模板参数 的列表
在模板定义中, 模板参数列表 不能为空
2、模板参数列表的作用 很像函数参数列表。函数参数列表 定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时,调用者提供实参 初始化形参
类似的,模板参数 表示 类或函数定义中用到的类型或值。 当使用模板时,(隐式地或显式地)指定模板实参,将其绑定到模板参数上
compare函数声明了一个名为T的类型参数。在compare中,我们用名字T表示一个类型。而T表示的实际类型 则在编译时 根据compare的使用情况来确定
3、实例化函数模板:调用一个函数模板时,编译器(通常)用函数实参 为我们推断模板实参。即,调用compare时,编译器使用实参的类型 来确定 绑定到模板参数T的类型
cout << compare(1, 0) << endl; // T 为 intint compare(const int &v1, const int &v2)
{if (v1 < v2) return -1;if (v2 < v1) return 1;return 0;
}
编译器 用推断出的模板参数 来对我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实形的模板实参 代替对应的模板参数 来创建出模板的一个新“实例”(编译器生成的版本 通常称为模板的实例)
// 实例化出 int compare(const vector<int>&, const vector<int>&)
vector<int> vec1{1, 2, 3}, vec2{4, 5, 6};
cout << compare(vec1, vec2) << endl; // T 为 vector<int>
4、模板类型参数:compare函数有一个模板类型参数,可以 将类型参数看作 类型说明符,就像内置类型 或 类类型说明符一样使用。特别是,类型参数 可以用来指定返回类型 或 函数的参数类型,以及在函数体内用于 变量声明 或 类型转换
// 正确,返回类型和参数类型是相同
template <typename T> T foo(T* p)
{T tmp = *p; // tmp 的类型将是指针 p 指向的类型//...return tmp;
}
类型参数前 必须使用关键字 class 或 typename:
// 错误:U 之前必须加上 class 或 typename
template <typename T, U> T calc(const T&, const U&);
在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表中 可以同时使用这两个关键字:
// 正确,在模板参数列表中,typename 和 class 没有什么不同
template <typename T, class U> T calc(const T&, const U&);
5、非类型模板参数:除了 定义类型参数,还可以 在模板中定义非类型参数。一个非类型参数 表示一个值而非一个类型。通过一个特定的类型名 而非 关键字 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)局部变量或非static成员对象 作为指针 或 引用非类型模板参数的实参。指针参数 也可以用 nullptr 或 一个值为0的常量表达式来实例化
假设我们有一个模板函数,它接受一个指向函数的指针作为非类型模板参数:
#include <iostream>int add(int a, int b) {return a + b;
}int subtract(int a, int b) {return a - b;
}// 定义一个模板,接受一个指向函数的指针作为非类型模板参数
template <int (*func)(int, int)>
void printResult(int x, int y) {std::cout << "Result: " << func(x, y) << std::endl;
}int main() {// 调用模板函数,使用不同的函数指针作为实参printResult<add>(10, 5); // 输出 Result: 15printResult<subtract>(10, 5); // 输出 Result: 5return 0;
}
指向函数类型的指针作为非类型模板参数的实参必须具有静态生存期
假设我们有一个模板函数,它接受一个指向数组的指针作为非类型模板参数:
template <typename T, T* ptr>
void print() {std::cout << *ptr << std::endl;
}
为了 使用这个模板,需要传递 一个指向数组的指针作为模板参数。这个指针必须指向一个具有静态生存期的对象,例如全局变量或静态局部变量
#include <iostream>int globalValue = 42;
static int staticValue = 100;template <typename T, T* ptr>
void print() {std::cout << *ptr << std::endl;
}int main() {// 调用模板函数,使用具有静态生存期的全局变量和静态局部变量作为实参print<int, &globalValue>(); // 输出 42print<int, &staticValue>(); // 输出 100int localValue = 10;// 错误:局部变量没有静态生存期// print<int, &localValue>(); // 这行会导致编译错误return 0;
}
在模板定义中,模板非类型参数 是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小
6、inline 和 constexpr 的函数模板:函数模板 可以声明为 inline 或 constexpr 的。inline 或 constexpr 说明符 放在模板参数列表之后,返回类型之前:
// 正确,inline 说明符紧跟在模板参数列表之后
template <typename T> inline T min(const T&, const T&);
7、编写类型无关的代码
编写泛型代码的两个重要原则:
- 模板中的函数参数是 const 的引用
- 函数体中的条件判断仅使用比较运算符
通过将函数参数设定为 const 的引用,我们保证了函数 可以用于不能拷贝的类型。大多数类型,包括内置类型和我们已经用过的标准库类型(除 unique_ptr 和 IO 类型之外),都是 允许拷贝的
如果 compare 用于处理大对象,这种设计显然能使函数运行得更快
如果编写代码时 只使用 < 运算符,就降低了 compare 函数对要处理的类型的要求。这些类型必须支持 <,但不必同时支持 >
真的希望类型无关和可移植性,可能需要用 less 来定义我们的函数:(与直接使用 < 运算符不同,std::less 的行为在标准库中有明确的定义和实现,保证了在所有标准库实现中的一致性;直接使用 < 和 > 运算符进行比较时,如果操作对象是指针且不属于同一数组,将会导致未定义行为。而 std::less 针对这种情况进行了处理)
// 即使用了指针也正确的 compare 版本
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;
}
模板程序 应尽量减少对实参类型的要求
8、使用(而不是定义)模板时,编译器才生成代码,这一特性 影响了如何组织代码 以及 错误何时被检测到
调用一个函数时,编译器只需要掌握函数的声明。(至于 具体实现,链接器会在链接阶段 将其与文件中的定义匹配)类似的,当我们 使用一个类类型的对象时,类定义 必须是可用的,但成员函数的定义 不必已经出现。将类定义和函数声明 放在头文件中,而普通函数和类的成员函数的定义 放在源文件中
模板则不同:为了 生成一个实例化版本,编译器需要 掌握 函数模板 或 类模板成员函数的定义。因此,与非模板代码不同,模板的头文件 通常既包括声明也包括定义
函数模板和类模板及其成员函数的定义通常放在头文件中
9、关键概念:模板和头文件
模板包含两种符号:
- 那些不依赖于模板参数的符号
- 那些依赖于模板参数的符号
当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的。而且,模板的提供者 必须保证,当模板被实例化时,模板的定义,包括模板的成员函数定义,也是可见的
用来实例化模板的所有函数、类型 以及 与类型相关的运算符的声明都必须是可见的,这是由模板的用户来保证的
通过组织良好的程序结构,恰当使用头文件,这些要求都很容易满足。模板的设计者应该提供一个头文件,包含模板定义 以及在类模板或成员定义中 用到的所有符号的声明。模板的用户 则必须包含模板的头文件,以及用来实例化模板的任何类型的头文件
10、大多数编译错误在实例化期间报告:模板直到实例化时才会生成代码,编译器会在三个阶段报告错误:
第一个阶段是 编译模板本身时。编译器可以检查语法错误
第二个阶段是编译器遇到模板被使用时。对于函数模板的调用,编译器通常会检查实参数目是否匹配。它还能够检查参数类型是否匹配。编译器可以 检查用户是否提供了正确数目的模板实参
第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误。依赖于 编译器如何管理实例化,这类错误可能在链接时才报告
编写模板时,代码不能是针对 特定类型的,但模板代码通常对其所使用的类型有一些假设。例如,最初的 compare 函数中的代码 假定实参类型定义了 < 运算符
if (v1 < v2) return -1; // 要求实参的类型支持<运算符
if (v2 < v1) return 1; // 要求实参的类型支持<运算符
当编译器处理此模板时,它不能验证 if 语句中的条件是否合法。如果传递给 compare 的实参定义了 < 运算符,则代码就是正确的,否则就是错误的
Sales_data data1, data2;
cout << compare(data1, data2) << endl; // 错误:Sales_data 未定义 <
这样的错误 直至编译器在类型 Sales_data 上实例化 compare 时才会被发现
11、编写并测试 自己的 compare 函数
Compare.h
#pragma once
#ifndef COMPARE_H
#define COMPARE_H#include <functional>
#include <cstring>template <typename T>
int compare(const T& n1, const T& n2) {// bool result = std::less<int>()(a, b) 创建一个 std::less<int> 类型的临时对象,并调用其 operator() 函数// 第一个括号 (std::less<T>()):创建一个 std::less<T> 类型的临时对象// 第二个括号((v1, v2)):调用这个临时对象的 operator() 函数if (std::less<T>()(n1, n2)) return -1;else if (std::less<T>()(n2, n1)) return 1;return 0;
}template <unsigned N, unsigned M>
// const char &p1[N]不加括号变成引用数组(p1[N])了
int compare(const char(&p1)[N], const char(&p2)[M]) {return strcmp(p1, p2);
}#endif
16.2.cpp
#include "Compare.h"
#include <iostream>using namespace std;int main() {cout << compare(1, 0) << endl;cout << compare("hi", "mom") << endl;return 0;
}
运行结果
12、模板参数可以有多种类型,主要包括类型参数(typename 或 class 关键字来声明)、非类型参数(非类型参数是指常量表达式,包括整数、枚举、指针或引用)和模板模板参数(用于参数化一个模板,它本身也是一个模板。在模板定义中,模板模板参数可以表示一个模板类)
template <template <typename> class ContainerType>
class Wrapper {ContainerType<int> container;
};
ContainerType 是一个模板模板参数,可以在实例化 Wrapper 时指定具体的模板类
// 模板模板参数
template <template <typename> class ContainerType>
class Wrapper {
public:ContainerType<int> container;void add(int val) {container.push_back(val);}void display() {for (const auto& elem : container) {std::cout << elem << " ";}std::cout << std::endl;}
};Wrapper<std::vector> w;
w.add(10);
w.add(20);
w.add(30);
std::cout << "Wrapper values: ";
w.display();
对两个 Sales_data 对象调用 compare 函数
Sales_data.h
#pragma once
#ifndef TEST_SALES_DATA_H
#define TEST_SALES_DATA_H#include <string>
#include <iostream>class Sales_data {friend bool operator==(const Sales_data&, const Sales_data&);
public:Sales_data() : units_sold(0), revenue(0.0) { }Sales_data(const std::string& s) :bookNo(s), units_sold(0), revenue(0.0) { }Sales_data(const std::string& s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p* n) { }std::string isbn() const { return bookNo; }
private:std::string bookNo;unsigned units_sold;double revenue;
};// 定义了<运算符才可以被顺利调用
inline
bool operator<(const Sales_data & lhs, const Sales_data & rhs)
{return lhs.isbn() < rhs.isbn();
}inline
bool operator==(const Sales_data& lhs, const Sales_data& rhs)
{return lhs.isbn() == rhs.isbn() &&lhs.units_sold == rhs.units_sold &&lhs.revenue == rhs.revenue;
}
inline
bool operator!=(const Sales_data& lhs, const Sales_data& rhs)
{return !(lhs == rhs);
}#endif
16.3.cpp
#include "Compare.h"
#include <iostream>
#include "Sales_data.h"using namespace std;int main() {Sales_data sd1("123", 10, 9.9), sd2("000", 20, 19.9);cout << compare(sd1, sd2) << endl;return 0;
}
运行结果
13、编写行为类似标准库 find 算法的模板。函数需要 两个模板类型参数,一个表示函数的迭代器类型,另一个表示值的类型。使用你的函数在一个 vector<int>
和一个 list<string>
中查找指定值
Find.h
#pragma once
#ifndef FIND_H
#define FIND_Htemplate <typename I, typename T>
I Find(I b, I e, const T &v) {for (I iter = b; iter != e; iter++) {if (*iter == v)return iter;}return e;
}#endif
16.4.cpp
#include "Find.h"
#include <iostream>
#include <vector>
#include <string>using namespace std;int main()
{vector<int> vec1 = { 1, 2, 3, 4 };auto it = Find(vec1.begin(), vec1.end(), 3); // 不能用find,与库函数重合了if (it == vec1.end()) {cout << "3 not found" << endl;}else {cout << "3 found in position " << (it - vec1.begin()) << endl;}vector<string> vec2 = { "1", "2", "3", "4"};auto it2 = Find(vec2.begin(), vec2.end(), "3"); // 不能用find,与库函数重合了if (it2 == vec2.end()) {cout << "\"3\" not found" << endl; // 注意转义字符的使用}else {cout << "\"3\" found in position " << (it - vec1.begin()) << endl;}return 0;
}
运行结果
14、print 函数编写模板版本,它接受一个数组的引用,能处理任意大小、任意元素类型的数组
接受一个数组实参的标准库函数 begin 和 end 是如何工作的?定义你自己版本的 begin 和 end
编写一个 constexpr 模板,返回给定数组的大小
#include <string>
#include <iostream>using namespace std;template<typename T, size_t M>
void print(const T(&arr)[M]) {for (auto i = 0; i < M; i++)cout << arr[i];cout << endl;
}template<typename T, size_t M>
const T* begin(const T(&arr)[M]) {return arr;
}template<typename T, size_t M>
const T* end(const T(&arr)[M]) {return arr + M;
}template<typename T, size_t M>
constexpr size_t size_arr(const T(&arr)[M]) {return M;
}int main()
{int vec1[3] = {1, 2, 3};string vec2[4] = {"Ashergu ", "Embedded_ai ", "Autonamous_driving ", "engineer"};print(vec1);print(vec2);cout << *begin(vec1) << endl;cout << *(end(vec2) - 1) << endl;cout << size_arr(vec2) << endl;return 0;
}
运行结果
15、泛型编程的一个目标就是令算法是 “通用的” —— 适用于不同类型。所有标准库容器都定义了 == 和 != 运算符,但其中只有少数定义了 < 运算符。因此,尽量使用 != 而不是 < ,可减少你的算法适用容器的限制
1.2 类模板
1、类模板 是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板 推断模板参数类型。 为了使用类模板,必须在模板名后的尖括号中 提供模板所需的所有参数(vector<int>
)——用来代替模板参数的模板实参列表
2、定义类模板:类似于 StrBlob,模板会提供 对元素的共享(且核查过的)访问能力。与类不同,模板 可以用于更多类型的元素。与标准库容器相同,当使用 Blob 时,用户需要指定其元素类型
在类模板(及其成员)的定义中,将模板参数 当作替身,代替使用模板时 用户需要提供的类型或值:
template <typename T> class Blob {
public:typedef T value_type;typedef typename std::vector<T>::size_type size_type;// 构造函数Blob();Blob(std::initializer_list<T> il);// 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();// 元素访问T& back();T& operator[](size_type i); private:std::shared_ptr<std::vector<T>> data;// 若 data[i] 无效,则抛出 msgvoid check(size_type i, const std::string &msg) const;
};
Blob 模板有一个名为 T 的模板类型参数,用来表示 Blob 保存的元素的类型
3、实例化类模板
当使用一个类模板时,必须提供额外信息。这些额外信息是 显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参 来实例化出特定的类
Blob<int> ia; // 空的 Blob<int>
Blob<int> ia2 = {0, 1, 2, 3, 4}; // 有 5 个元素的 Blob<int>
当编译器 从 Blob 模板实例化出一个类时,它会重写 Blob 模板,将模板参数 T 的每个实例 替换为给定的模板实参
对 指定的每一种元素类型,编译器都生成一个不同的类:
// 下面的定义实例化出两个不同的 Blob 类型
Blob<string> names; // 保存 string 的 Blob
Blob<double> prices; // 不同的元素类型
一个模板的每个实例 都形成一个独立的类。类型 Blob<string>
与任何其他 Blob 实例没有关系,也不会对任何其他 Blob 类型的成员有特殊访问权限
4、在模板作用域中引用模板类型:类模板的名字不是一个类型名,类模板用来实例化类型,而一个实例化的类型是包含模板参数的
一个类模板中的代码 如果使用了另外一个模板,通常不将一个实例类型(或值)的名字用作其他模板实参(如string / int)。相反,通常将模板自己的参数 当作被使用模板的实参(T)。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>>
5、类模板的成员函数:与其他任何类相同,既可以在类模板内部,也可以在类模板外部 为其定义成员函数,且定义类模板内的成员函数 被隐式地声明为内联函数(普通类也是)
类模板的每个实例 都有其自己版本的成员函数。因此,类模板的成员函数模板 有相同的模板参数,因而,定义在类模板之外的成员函数 就必须以关键字 template 开始,后接 类模板参数列表
在类外定义一个成员时,必须说明成员属于哪个类。而且,从一个模板生成的类的名字中必须包含其模板实参。定义 一个成员函数时,模板参数与模板形参相同。即,对于 StrBlob 的一个给定的成员函数
ret-type StrBlob::member-name(parm-list)
对应的 Blob 的成员
template <typename T>
ret-type Blob<T>::member-name(parm-list)
6、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 成员完全一样
下标运算符 和 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) {// 如果 i 失效,check 会抛出异常,阻止访问一个不存在的元素check(i, "subscript out of range");return (*data)[i];
}
在原 StrBlob 类中,这些运算符返回 string&。而模板版本 也返回一个引用,指向用来实例化 Blob 的类型
pop_back 函数与原 StrBlob 的成员几乎相同:
template <typename T>
void Blob<T>::pop_back() {check(0, "pop back on empty Blob");data->pop_back();
}
7、Blob 构造函数:与其他任何 定义在类模板外的成员一样,构造函数的定义 要以模板参数开始:
template <typename T>
Blob<T>::Blob() : data(std::make_shared<std::vector<T>>()) { }
在作用域 Blob<T>
中定义了名为 Blob 的成员函数
此构造函数分配一个空 vector,并将指向 vector 的指针保存在 data 中。将类模板自己的类型参数 作为 vector 的模板实参 来初始化 vector
接受一个 initializer_list 参数的构造函数将其类型参数 T 作为 initializer_list 参数的元素类型:
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) :data(std::make_shared<std::vector<T>>(il)) { }
用参数 il 来初始化此 vector
为了 使用这个构造函数,必须传递给它一个 initializer_list,其中的元素 必须与 Blob 的元素类型兼容:
Blob<string> articles = {"a", "an", "the"};
构造函数的参数类型为 initializer_list<string>
。列表中的每个字符串字面常量隐式地转换为一个 string
8、类模板成员函数的实例化:一个类模板的成员函数 只有当程序 用到它 时才进行实例化
// 实例化 Blob<int> 和接受 initializer_list<int> 的构造函数
Blob<int> squares = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 实例化 Blob<int>::size() (const)
for (size_t i = 0; i != squares.size(); ++i)squares[i] = i*i; // 实例化 Blob<int>::operator[](size_t)
如果一个成员函数 没有被使用,则它不会被实例化。成员函数只有在 被用到 时才进行实例化,这一特性使得 即便模板类实例不能完全符合 模板操作的要求,仍然能用 该类型实例化类
9、在类代码内 简化模板类型名的使用:使用(不是定义)一个类模板类型时 必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,可以直接使用模板名而不提供实参:
// 若想访问一个不存在的元素,BlobPtr 抛出一个异常
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)为表示指向的 vector} // 递增和递减BlobPtr& operator++(); // 前置递增符BlobPtr& operator--(); // 相当于 BlobPtr<T>& operator++(); // BlobPtr<T>& operator--();private: // 若检查成功,check 返回一个指向 vector 的 shared_ptr std::shared_ptr<std::vector<T>> data; void check(std::size_t, const std::string&) const; // 保有一个 weak_ptr,表示底层 vector 可能被销毁 std::weak_ptr<std::vector<T>> wptr; std::size_t curr; // 数组中当前的位置
};
BlobPtr 的前置递增和递减成员 返回的是 BlobPtr&,而不是 BlobPtr<T>&
。当 处于一个类模板的作用域中时,编译器处理模板自身引用时 就好像已经提供了与模板参数匹配的实参一样
10、在类模板外使用类模板名:在类模板外定义其成员时,并不在 类的作用域中,直到遇到类名 才会进入类的作用域
// 后置递增:遍历对象后返回原值
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int) { // 此处无须检查;调用前置递增时会进行检查 BlobPtr ret = *this; // 保存当前值 ++*this; // 推进一个元素;前置 ++ 检查递增是否合法 return ret; // 返回旧的值
}
返回类型 位于类的作用域之外,必须指出 返回类型是一个实例化的 BlobPtr,在函数体内,已经进入类的作用域,因此在定义 ret 时无须提供模板实参
11、类模板和友元
如果一个类模板 包含一个非模板友元,则友元被授权 可以访问所有模板实例。如果友元自身是模板,类可以授权给 所有友元模板实例,也可以只授权给特定实例
一对一友好关系
类模板与另一个(类模板)模板间友好关系的 最常见的形式是 建立对应实例及其元素间的友好关系。例如,Blob 类应该将 BlobPtr 类和 一个模板版本的 Blob 相等运算符定义为友元
为了引用(类或函数)模板的一个特定实例,必须 首先声明模板自身。一个模板声明 包括模板参数列表:
// 前置声明,表明 Blob 中声明友元所需要的
template <typename> class BlobPtr;
template <typename> class Blob; // 运算符==中的参数所需要的
template <typename T>
bool operator==(const Blob<T>&, const Blob<T>&);template <typename T> class Blob {// 每个 Blob 实例都将访问权限授予用相同类型实例化的 BlobPtr 和相等运算符friend class BlobPtr<T>; // 注意前面没有template,后面有<T>friend bool operator==<T>(const Blob<T>&, const Blob<T>&);...
};
友元声明 用 Blob 模板形参作为它们自己的模板实参。因此,友好关系仅限定在 用相同类型实例化的 Blob 与 BlobPtr 相等运算符之间
Blob<char> ca; // BlobPtr<char> 和 operator==<char> 都是 ca 的友元
Blob<int> ia; // BlobPtr<int> 和 operator==<int> 是 ia 的友元
BlobPtr<char>
的成员可以访问 ca(或任何其他 Blob<char>
对象)的非公共部分,但 ca 对 ia(或任何其他 Blob<int>
对象)或 Blob 的任何其他实例都没有特殊访问权限
通用和特定的模板友好关系
一个类 也可以将另一个模板的每个实例 都声明为自己的友元,或者 限定特定的实例为友元
// 前置声明,在将模板的一个特定实例声明为友元之前引用到
template <typename T> class Pal;
class C { // C 是一个普通的非模板类1、friend class Pal<C>; // 用类实例化 Pal 是 C 的一个友元,跟之前一对一的语法有点类似// 但是这个不是每个实例相同实例化的 Pal 是友元,而是用C实例化的2.1、(模板类型参数命名都需要不同)// Pal2 的所有实例都是 C 的友元;这样情况无须前置声明template <typename T> friend class Pal2;
};template <typename T> class C2 { // C2 本身是一个类模板3、// C2 的每个实例将相同实例化的 Pal 声明为友元friend class Pal<T>; // Pal 的模板参数必须在作用域内2.2、// Pal2 的所有实例是 C2 的每个实例的友元,不需要前置声明template <typename X> friend class Pal2;// Pal3 是一个非模板类,它是 C2 所有实例的友元friend class Pal3; // 不需要 Pal3 的前置声明
};
为了 让所有实例成为友元,友元声明中 必须使用与类模板本身不同的模板形参(是多个的友元 非模板类 / 模板类友元 前面加 template<>)
12、模板类型别名
类模板的一个实例 定义了一个类型型名。与任何其他类型一样,可以定义一个 typedef 来引用实例化的类
typedef Blob<string> StrBlob;
由于模板不是一个类型,不能定义一个 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 成员的类型
13、类模板的 static 成员:与任何其他类相同,类模板可以声明 static 成员
template <typename T> class Foo {
public:static std::size_t count() { return ctr; }// 其他静态成员
private:static std::size_t ctr;// 其他实现成员
};
Foo 是一个类模板,它有一个名为 count 的 public static 成员函数 和 一个名为 ctr 的 private static 数据成员。每个 Foo 的实例都有其自己的 static 成员实例,即,对任意给定类型 X,都有一个 Foo<X>::ctr
和一个 Foo<X>::count
成员。所有 Foo<X>
类型的对象共享相同的 ctr 对象和 count 函数
// 实例化 static 成员 Foo<string>::ctr 和 Foo<string>::count
Foo<string> fs;
// 所有三个对象共享相同的 Foo<int>::ctr 和 Foo<int>::count 成员
Foo<int> fi, fi2, fi3;
与任何其他 static 数据成员相同,模板类的每个 static 数据成员 必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的 static 对象。因此,与定义模板的成员函数类似,我们的 static 数据成员也定义为模板
template <typename T>
size_t Foo<T>::ctr = 0; // 定义并初始化 ctr
与类模板的其他任何成员函数类似,定义的开始部分是 模板参数列表,随后是 定义的成员的类型和名字。与通常一样,成员名包括 成员的类名,对于从模板生成的类来说,类名 包括模板实参。当使用 一个特定的模板实参 实例化 Foo 时,将会为 该类类型实例化一个独立的 ctr,并将其初始化为 0
与非模板类的静态成员相同,可以通过 类类型对象 来访问一个类模板的 static 成员,也可以 使用作用域运算符直接访问成员。为了通过类来 直接访问 static 成员,必须引用一个特定实例
Foo<int> fi; // 实例化 Foo<int> 类和 static 数据成员 ctr
auto ct = Foo<int>::count(); // 实例化 Foo<int>::count
ct = fi.count(); // 使用 Foo<int>::count
ct = Foo::count(); // 错误:使用哪个模板实例的count
类似任何其他成员函数,一个 static 成员函数只有在使用时才会实例化
14、下面 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); // 改成void insert(ListItem<elemType> *ptr, elemType value);
private:ListItem *front, *end;
};
类模版的名字不是一个类型名。类模版只有实例化后 才能形成类型,而实例化 总是要提供模版实参的。因此,在题干代码中直接使用 ListItem 是错误的,应该使用 ListItem<elemType>
,这才是一个类型
15、编写 自己版本的 Blob 和 BlobPtr 模板,包含书中未定义的多个 const 成员
Blob.h
#pragma once
#ifndef BLOB_H
#define BLOB_H#include <vector>
#include <memory>
#include <string>
#include <stdexcept>
#include <iostream>template <typename> class BlobPtr; // 声明可以省略名字T,除非后面要用。声明是因为后面要用
template <typename> class Blob;
template <typename T> bool operator==(const Blob<T>&, const Blob<T>&); // 声明为后面添加友元做准备template <typename T> class Blob {friend class BlobPtr<T>; // 友元只包括相同用T初始化的,不是模板实例化的都是friend bool operator==<T>(const Blob<T>&, const Blob<T>&);public:typedef typename std::vector<T>::size_type size_type; // 不加typename报错:size_type依赖名称不是类型// 在模板类中,编译器在实例化模板之前不会知道std::vector<T>的确切类型,因此它不能确定size_type是一个类型还是一个成员变量、静态成员或其他东西// 为了消除这种歧义,C++标准要求在模板中使用typename来明确指示这是一个类型// 在模板定义中,依赖名称是指依赖于模板参数的名称。std::vector<T>::size_type就是一个依赖名称,因为它依赖于模板参数T// 在模板中,编译器不知道依赖名称是类型还是其他东西。通过使用typename关键字,开发者显式地告诉编译器这是一个类型Blob();template <typename I> Blob(I b, I e);Blob(T*, std::size_t);BlobPtr<T> begin() { return BlobPtr<T>(*this); }BlobPtr<T> end() {BlobPtr<T> ret = BlobPtr<T>(*this, data->size());return ret;}size_type size() const { return data->size(); }bool empty() const { return data->empty(); }void push_back(const T& t) { data->push_back(t); }void pop_back();T& front();T& back();T& at(size_type);const T& back() const;const T& front() const;const T& at(size_type) const;T& operator[](size_type);const T& operator[](size_type i) const;void swap(Blob& b) { data.swap(b.data); } 加上拷贝赋值运算符 就独立了//Blob& operator=(const Blob & other) {// if (this != &other) {// data = std::make_shared<std::vector<T>>(*other.data);// }// return *this;//}private:std::shared_ptr<std::vector<T>> data; // 模板类型可以在容器中使用// data[i]非法就抛出异常void check(size_type, const std::string&) const;
}; template <typename T> Blob<T>::Blob(T* p, std::size_t n): // 成员函数声明注意要带template<typename T>和类名后面的<T>data(new std::vector<T>(p, p + n)) {}
// 用于从给定的范围 [p, p+n) 创建一个新的 std::vector 对象,这个构造函数创建一个 std::vector,其元素是从迭代器范围 [p, p+n) 中复制而来的template <typename T> Blob<T>::Blob():data(new std::vector<T>()) {}template <typename T>
template <typename I>
Blob<T>::Blob(I b, I e) :// 必须分开写两个template,不能写template<typename T, typename I>,不然会显示没有找到重载函数// 当定义类模板的成员函数模板时,需要分别声明类模板参数和成员函数模板参数。这样做的原因是模板参数的作用域和解析规则// template <typename T> 是类模板 Blob 的模板参数声明// template <typename I> 是成员函数模板 Blob(I b, I e) 的模板参数声明// 在成员函数模板的定义中,模板参数 T 和 I 的作用域和解析是独立的。如果将它们写在一起,会导致模板参数的作用域混淆,从而编译器无法正确解析模板参数// 在类模板的定义中,T 是类模板的参数,它的作用域是整个类 Blob// I 是成员函数模板的参数,它的作用域仅限于成员函数 Blob(I b, I e)data(new std::vector<T>(b, e)) {}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>::front() {check(0, "front on empty Blob");return data->front();
}// 用于非 const 的 Blob 对象。返回一个 T& 类型的引用,因此可以修改返回的元素
template <typename T>
T& Blob<T>::back() {check(0, "back on empty Blob");return data->back();
}template <typename T>
void Blob<T>::pop_back() {check(0, "pop_back on empty Blob");data->pop_back();
}// const版本的front()和back(),定义必须和声明的const位置完全匹配
template <typename T>
const T& Blob<T>::front() const {check(0, "front on empty Blob");return data->front();
}// 用于 const 的 Blob 对象。
// 返回一个 const T& 类型的引用,因此不可以修改返回的元素
template <typename T>
const T& Blob<T>::back() const {check(0, "back on empty Blob");return data->back();
}template <typename T>
T& Blob<T>::at(size_type i) {check(i, "subscript out of range");return *(data + i); // 返回vector中的元素
}// 定义const版本的,只需要在返回值和参数(如果有T)以及函数加const即可
template <typename T>
const T& Blob<T>::at(size_type i) const {check(i, "subscript out of range");return *(data + i); // 返回vector中的元素
}// size_type 表示和操作容器的大小和索引
// 表示大小:size_type 可以用来表示容器中元素的数量。比如,当你调用 std::string::size() 或 std::vector<T>::size() 时,返回值的类型就是 size_type
// 这表明容器当前包含的元素数量
//
// 表示索引:size_type 也可以用来表示容器中元素的索引位置
// 因为索引是用来访问容器中的元素的位置,而 size_type 是无符号整数类型,适合表示这种位置索引
//
// 可移植性:std::string::size_type 是一个与平台无关的类型,使用它可以确保代码在不同平台上的一致性和可移植性
// 无符号类型:std::string::size_type 是无符号类型,适合表示大小和索引,不会出现负值template <typename T>
T& Blob<T>::operator[](size_type i) { return (*data)[i];
}template <typename T>
const T& Blob<T>::operator[](size_type i) const {check(i, "subscript out of range");return (*data)[i];
}// 调用之前定义的[]运算符 以及 size()函数
template <typename T>
std::ostream& operator<<(std::ostream& os, const Blob<T> &a) { // 加了一个&for (std::size_t i = 0; i < a.size(); i++)os << a[i] << " ";return os;
}template <typename T>
bool operator==(const Blob<T>& lhs, const Blob<T>& rhs) {if (rhs.size() != lhs.size())return false; // 为后面的for循环做准备for (std::size_t i = 0; i < rhs.size(); i++) {if (lhs[i] != rhs[i])return false;}return true;
}// 先声明,后面友元要用
template <typename T>
bool operator==(const BlobPtr<T>&, const BlobPtr<T>&);template <typename T>
class BlobPtr {friend booloperator==<T>(const BlobPtr<T>&, const BlobPtr<T>&);// 友元限定在跟BlobPtr模板类型一致的operator==函数,函数名后面别忘了<T>public:BlobPtr() : curr(0) {}BlobPtr(Blob<T> &a, size_t sz = 0) :wptr(a.data), curr(sz) {}// shared_ptr<vector<T>> 复制给 weak_ptr<vector<T>>T& operator[](std::size_t sz) {std::shared_ptr<std::vector<T>> r = check(sz, "subscript out of range");return (*r)[sz];}const T& operator[](std::size_t sz) const {std::shared_ptr<std::vector<T>> r = check(sz, "subscript out of range");return (*r)[sz];}T& operator*() const {std::shared_ptr<std::vector<T>> p = check(curr, "dereference past end");return (*p)[curr];}// 直接调用operator*()T* operator->() const {return &this->operator*();}// 前置递增 / 递减运算符BlobPtr& operator++();BlobPtr& operator--();// 后置BlobPtr operator++(int);BlobPtr operator--(int);private:std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const;// weak_ptr表示其指向的vector可能被销毁std::weak_ptr<std::vector<T>> wptr;// 指向vector中的位置std::size_t curr;
};// std::weak_ptr::lock()方法会返回一个std::shared_ptr,该std::shared_ptr指向由std::weak_ptr管理的对象
// 如果管理的对象已被销毁,则返回一个空的std::shared_ptr
// std::shared_ptr::get()方法返回由std::shared_ptr管理的原始指针(裸指针)。如果std::shared_ptr为空,则返回nullptr// std::weak_ptr是一种智能指针,它提供对std::shared_ptr管理的对象的非拥有(弱)引用
// 与std::shared_ptr不同,std::weak_ptr不会增加对象的引用计数,这样可以避免循环引用的问题template <typename T>
bool operator==(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs) {// 不光data指向的值要一样,curr也要一样return lhs.wptr.lock().get() == rhs.wptr.lock().get() && lhs.curr == rhs.curr;
}// 不等直接调用上面的operator==
template <typename T>
bool operator!=(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs) {return !(rhs == lhs);
}// std::weak_ptr::lock()
template <typename T>
std::shared_ptr<std::vector<T>>
BlobPtr<T>::check(std::size_t i, const std::string& msg) const {std::shared_ptr<std::vector<T>> r = wptr.lock();if (!r) // lock()返回值为空,weak_ptr管理的对象已销毁throw std::runtime_error("unbound BlobPtr");if (i >= r->size()) // 越界了,越界的提示根据不同情况不同的字符串throw std::out_of_range(msg);return r;
}// 定义前置递增递减运算符,后置 直接调用前置就行,递增递减什么东西调用就返回什么东西
template <typename T>
BlobPtr<T>& BlobPtr<T>::operator++() {check(curr, "increment past end of BlobPtr");curr++; // 允许到end尾后指针return *this; // *this就是调用的那个对象
}template <typename T>
BlobPtr<T>& BlobPtr<T>::operator--() {curr--;check(curr, "decrement past begin of BlobPtr");return *this;
}// 前置运算符 返回值是引用,后置不是
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int) {++(*this);return *this;
}template <typename T>
BlobPtr<T> BlobPtr<T>::operator--(int) {--(*this);return *this;
}
#endif
16.12.cpp
#include <string>
#include <iostream>
#include "Blob.h"using namespace std;int main() {Blob<string> b1; cout << b1.size() << endl;string temp[] = { "a", "an", "the" };Blob<string> b2(temp, temp + sizeof(temp) / sizeof(*temp)); // 用首尾指针初始化b2b1 = b2;cout << (b1 == b2) << endl; // 注意要有括号,不然会先<<后==b2.push_back("about"); // b1还是跟b2保持一致// 因为在b1 = b2;之后,b1和b2共享了同一个底层数据对象。这是由于使用了std::shared_ptr来管理Blob类中的数据// 如果 修改其中一个对象的数据,另一个对象也会看到这些更改,因为它们引用了同一个底层std::vector对象(使用默认的赋值运算符)// std::shared_ptr允许多个shared_ptr对象共享同一个底层资源,这样当一个shared_ptr对象修改底层资源时,所有共享该资源的shared_ptr对象都会看到这些修改// 加入自己的拷贝构造函数(顺便加上了,不加也独立了,没用到)和拷贝赋值运算符就独立了// // 拷贝构造函数//Blob(const Blob & other) : data(std::make_shared<std::vector<T>>(*other.data)) {} 拷贝赋值运算符//Blob& operator=(const Blob & other) {// if (this != &other) {// data = std::make_shared<std::vector<T>>(*other.data);// }// return *this;//}cout << b1.size() << " " << b2.size() << endl;for (BlobPtr<string> p = b1.begin(); p != b1.end(); ++p)cout << *p << " ";cout << endl;for (int i = 0; i < b2.size(); i++)cout << b2[i] << " ";return 0;
}
运行结果
解释为 BlobPtr 的相等和关系运算符选择哪种类型的友好关系:由于函数模版的实例化 只处理特定类型,因此,对于相等和关系运算符,对每个 BlobPtr 实例与用相同类型实例化的关系运算符建立一对一的友好关系即可
template <typename T>
class BlobPtr {friend booloperator==<T>(const BlobPtr<T> &, const BlobPtr<T> &);... ...
};
16、编写 Screen 类模板,用非类型参数定义 Screen 的高和宽
为你的 Screen 模板实现输入和输出运算符。Screen 类需要哪些友元(如果需要的话)来令输入和输出运算符正确工作
Screen.h
#pragma once
#ifndef SCREEN_H
#define SCREEN_H#include <algorithm>
#include <iostream>
#include <string>using pos = std::string::size_type;template <pos H, pos W>
class Screen; // 后面声明要用template <pos H, pos W>
std::ostream& operator<<(std::ostream& os, Screen<H, W>& src);
// 声明定义时函数名后不加模板参数,因为声明友元要用,所以要先声明
template <pos H, pos W>
std::istream& operator>>(std::istream& is, Screen<H, W>& src);template <pos H, pos W>
class Screen {friend std::ostream& operator<<<H, W>(std::ostream& os, Screen<H, W>& src); // 加模板参数只设定特定参数的模板类才是友元,如果不加参数<H, W>,而在前面加template后面再加参数 就都是友元friend std::istream& operator>><H, W>(std::istream& is, Screen<H, W>& src);public:Screen() = default; // 自己定义了构造函数一般默认的要补上Screen(char c) : content(H* W, c), cur(0) {} // 使用string构造函数初始化char& get() { return content[cur]; }char& get(pos h, pos w) { return content[h * W + w]; } // 模板参数可以当成成员变量来用Screen& move(pos h, pos w) { cur = h * W + w; return *this; }Screen& set(char c) { content[cur] = c; cur++; return *this; }Screen& set(pos h, pos w, char c) { content[h * W + w] = c; return *this; }
private:pos cur;std::string content;
};template <pos H, pos W>
std::istream& operator>>(std::istream& is, Screen<H, W>& src) {std::string str;is >> str;for (char c : str)src.set(c);return is;
}template <pos H, pos W>
std::ostream& operator<<(std::ostream& os, Screen<H, W>& src) {for (pos i = 0; i < H; i++) {for (pos j = 0; j < W; j++) {os << src.get(i, j);}os << std::endl;}return os;
}
#endif
16.14.cpp
#include "Screen.h"int main() {Screen<5, 5> screen('x');screen.set(2, 2, 'o');std::cout << screen << std::endl;std::cout << "输入字符:";std::cin >> screen;std::cout << screen << std::endl;std::cout << "输入字符:";std::cin >> screen;std::cout << screen << std::endl;
}
运行结果
17、将 StrVec 类重写为模板,命名为 Vec
StrVec.h
#pragma once
#include <string>
#include <memory>class StrVec {
public:StrVec() :elements(nullptr), first_free(nullptr), cap(nullptr) {}StrVec(const StrVec&);StrVec(StrVec&&) noexcept; StrVec& operator=(StrVec&&) noexcept; StrVec& operator=(const StrVec&);~StrVec();void push_back(const std::string&);size_t size() const { return first_free - elements; }size_t capacity() const { return cap - elements; }std::string* begin() const { return elements; }std::string* end() const { return first_free; }void reserve(size_t n);void resize(size_t n);void resize(size_t n, const std::string& s);StrVec(std::initializer_list<std::string>);
private:static std::allocator<std::string> alloc;void chk_n_alloc();std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*);void free();void reallocate(); void reallocate(size_t); std::string* elements;std::string* first_free;std::string* cap;
};
StrVec.cpp
#include "StrVec.h"
#include <algorithm>
#include <utility>using namespace std;std::allocator<std::string> StrVec::alloc;void StrVec::chk_n_alloc()
{if (size() == capacity())reallocate();
}void StrVec::push_back(const string& s) {chk_n_alloc();alloc.construct(first_free++, s);
}pair<string*, string*> StrVec::alloc_n_copy(const string* b, const string* e)
{auto data = alloc.allocate(e - b);return { data, uninitialized_copy(b, e, data) };
}void StrVec::free()
{if (elements) {for_each(elements, first_free, [](string& s) {alloc.destroy(&s); });}alloc.deallocate(elements, cap - elements);
}StrVec::StrVec(const StrVec& s)
{auto newdata = alloc_n_copy(s.begin(), s.end());elements = newdata.first;first_free = newdata.second;cap = newdata.second;
}StrVec::~StrVec() { free(); }StrVec& StrVec::operator=(const StrVec& rhs)
{auto data = alloc_n_copy(rhs.begin(), rhs.end());free();elements = data.first;first_free = data.second;cap = data.second;return *this;
}// 用移动赋值的方式重写
void StrVec::reallocate()
{auto newcap = size() ? 2 * size() : 1;auto be = alloc.allocate(newcap);auto en = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), be);free();elements = be;first_free = en;cap = be + newcap;
}// 新加函数,另一种形式 移动赋值
void StrVec::reallocate(size_t newcap) {auto be = alloc.allocate(newcap);for (int i = 0; i < size(); i++) {alloc.construct(be++, std::move(*elements++));// elements 指针先被解引用,再进行递增操作,指向下一个元素的位置// std::move参数为元素,而不是指针}size_t si = size();free();elements = be;first_free = be + si;cap = be + newcap;
}void StrVec::reserve(size_t n) {if (size() < n) {reallocate();}
}void StrVec::resize(size_t n) {resize(n, std::string());
}void StrVec::resize(size_t n, const string& s)
{if (size() > n) {while (first_free != elements + n) {alloc.destroy(--first_free);}alloc.destroy(first_free);}else {while (size() < n) {push_back(s);}}
}StrVec::StrVec(std::initializer_list<std::string> il) {auto newdata = alloc_n_copy(il.begin(), il.end());elements = newdata.first;first_free = newdata.second;cap = newdata.second;
}// 新加 移动构造函数
StrVec::StrVec(StrVec&& svm) noexcept :elements(svm.elements), first_free(svm.first_free), cap(svm.cap)
{svm.elements = svm.first_free = svm.cap = nullptr;
}// 新加 移动赋值运算符,不用新建空间了,因为直接移过来了,直接用旧空间就行,但是自赋值的时候就会出问题,先free掉了
StrVec& StrVec::operator=(StrVec&& svm) noexcept
{if (this != &svm) { // *this != svm对象之间没办法比(StrVec没定义!=),只能比指针free();elements = svm.elements;first_free = svm.first_free;cap = svm.cap;svm.elements = svm.first_free = svm.cap = nullptr;}return *this;
}
Vec.h
#pragma once
#include <memory>
#include <algorithm>
#include <utility>using namespace std;template <typename T>
class Vec {
public:Vec() :elements(nullptr), first_free(nullptr), cap(nullptr) {}Vec(const Vec&);Vec(Vec&&) noexcept;Vec& operator=(Vec&&) noexcept;Vec& operator=(Vec&);~Vec();void push_back(const T&);size_t size() const { return first_free - elements; }size_t capacity() const { return cap - elements; }T* begin() const { return elements; }T* end() const { return first_free; }void reserve(size_t n);void resize(size_t n);void resize(size_t n, const T& s);Vec(std::initializer_list<T>);
private:static std::allocator<T> alloc;void chk_n_alloc();std::pair<T*, T*> alloc_n_copy(const T*, const T*);void free();void reallocate();void reallocate(size_t);T* elements;T* first_free;T* cap;
};// 要将所有 Vec 类模板的成员函数都在头文件中定义,这样可以确保模板的实例化正确地发生template <typename T> std::allocator<T> Vec<T>::alloc;
// 定义一个模板类 Vec 的静态数据成员 alloc,它是一个 std::allocator<T> 类型的对象
// 静态数据成员在类的所有实例之间共享,并且需要在类定义外部进行定义
// 别忘了加 template <typename T>,Vec<T>中的<T>,跟定义函数一致template <typename T>
void Vec<T>::chk_n_alloc()
{if (size() == capacity())reallocate();
}template <typename T>
void Vec<T>::push_back(const T& s) {chk_n_alloc();alloc.construct(first_free++, s);
}template<typename T>
pair<T*, T*> Vec<T>::alloc_n_copy(const T* b, const T* e)
{auto data = alloc.allocate(e - b);// 模板参数只需要在定义类和定义类的成员函数时指定,而在调用成员函数时,编译器能够通过类的实例推断出模板参数return { data, uninitialized_copy(b, e, data) };// uninitialized_copy 函数将输入范围 [b, e) 内的元素复制到以 data 为起始位置的未初始化内存区域// 这意味着它不仅复制元素的值,还在目标位置上构造这些元素// 在目标位置上使用元素的拷贝构造函数来构造新元素。这意味着目标位置上的内存必须是未初始化的原始内存(即尚未有任何对象在这些位置上构造)// alloc.construct 用于在预分配的未初始化内存上构造对象。它调用对象的构造函数,将对象构造在指定的内存位置上// // int* p = alloc.allocate(1); // 分配未初始化的内存// alloc.construct(p, 42); // 在分配的内存位置上构造一个值为 42 的 int 对象// alloc.destroy(p); // 调用析构函数销毁对象// alloc.deallocate(p, 1); // 释放内存
}template <typename T>
void Vec<T>::free()
{if (elements) {for_each(elements, first_free, [](T& s) { alloc.destroy(&s); });// for_each 函数对从 elements 到 first_free 范围内的每个元素执行指定的操作// for_each(InputIt first, InputIt last, UnaryFunction f);// Lambda 表达式:[](T& s) { alloc.destroy(&s); }}alloc.deallocate(elements, cap - elements);
}template <typename T>
Vec<T>::Vec(const Vec& s)
{auto newdata = alloc_n_copy(s.begin(), s.end());elements = newdata.first;first_free = newdata.second;cap = newdata.second;
}template <typename T>
Vec<T>::~Vec() { free(); }template <typename T>
Vec<T>& Vec<T>::operator=(Vec& rhs) // 作用域内不用加<T>
{auto data = alloc_n_copy(rhs.begin(), rhs.end());free();elements = data.first;first_free = data.second;cap = data.second;return *this;
}template <typename T>
void Vec<T>::reallocate()
{auto newcap = size() ? 2 * size() : 1;auto be = alloc.allocate(newcap);auto en = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), be);// uninitialized_copy 和 make_move_iterator 来实现从一个范围到另一块未初始化内存的移动构造// make_move_iterator 将普通迭代器转换为移动迭代器。使用移动迭代器时,会将元素从源范围“移动”到目标范围// 而不是复制。这意味着源范围中的元素会被移动语义处理,即从源元素中“窃取”资源// uninitialized_copy 函数将 [make_move_iterator(begin()), make_move_iterator(end())) 范围内的元素移动到 be 开始的未初始化内存区域// 返回尾后指针free();elements = be;first_free = en;cap = be + newcap;
}template <typename T>
void Vec<T>::reallocate(size_t newcap) { // 对于成员函数的改动就是这两行,然后把之后的string改成Tauto be = alloc.allocate(newcap);for (int i = 0; i < size(); i++) {alloc.construct(be++, std::move(*elements++));// std::move(std::string("Temporary"))}size_t si = size();free();elements = be;first_free = be + si;cap = be + newcap;
}template <typename T>
void Vec<T>::reserve(size_t n) {if (size() < n) {reallocate();}
}template <typename T>
void Vec<T>::resize(size_t n) {resize(n, Vec<T>());
}template <typename T>
void Vec<T>::resize(size_t n, const T& s)
{if (size() > n) {while (first_free != elements + n) {alloc.destroy(--first_free);}alloc.destroy(first_free);}else {while (size() < n) {push_back(s);}}
}template <typename T>
Vec<T>::Vec(std::initializer_list<T> il) {auto newdata = alloc_n_copy(il.begin(), il.end());elements = newdata.first;first_free = newdata.second;cap = newdata.second;
}template <typename T>
Vec<T>::Vec(Vec&& svm) noexcept :elements(svm.elements), first_free(svm.first_free), cap(svm.cap)
{svm.elements = svm.first_free = svm.cap = nullptr;
}template <typename T>
Vec<T>& Vec<T>::operator=(Vec&& svm) noexcept
{if (this != &svm) { // *this != svm对象之间没办法比(StrVec没定义!=),只能比指针free();elements = svm.elements;first_free = svm.first_free;cap = svm.cap;svm.elements = svm.first_free = svm.cap = nullptr;}return *this;
}
16.16.cpp
#include "Vec.h"#include <string>
#include <iostream>using namespace std;void print(const Vec<string>& svec) {for (string* it = svec.begin(); it != svec.end(); ++it)cout << *it << " ";cout << endl;
}Vec<string> getVec(istream& is) {Vec<string> svec;string s;while (is >> s)svec.push_back(s);return svec;
}int main() {Vec<string> svec = getVec(cin);print(svec);cout << "copy " << svec.size() << endl;Vec<string> svec2 = svec;print(svec2);cout << "move " << endl;Vec<string> svec3 = std::move(svec2);print(svec3);Vec<string> v2 = getVec(cin); print(v2);return 0;
}
运行结果
1.3 模板参数
1、模板参数与作用域
一个模板参数名的可用范围 是在其声明之后,至模板声明或定义 结束之前。与任何其他名字一样,模板参数 会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在 模板内 不能重用模板参数名
typedef double A;
template <typename A, typename B> void f(A a, B b) {A tmp = a; // tmp 的类型为模板参数 A 的类型,而非 doubledouble B; // 错误:重用模板参数名 B
}
由于参数名不能重用,所以 一个模板参数名在一个特定模板参数列表中 只能出现一次
// 错误:非法重用模板参数名 V
template <typename V, typename V> // ...
2、模板声明
模板声明必须包含模板参数
// 声明但不定义 compare 和 Blob
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
与函数参数相同,声明中的模板参数的名字 不必与定义中相同
// 3 个 calc 都指向相同的函数模板
template <typename T> T calc(const T&, const T&); // 声明
template <typename U> U calc(const U&, const U&); // 声明
// 模板的定义
template <typename Type>
Type calc(const Type& a, const Type& b) { /* ... */ }
一个给定模板的每个声明和定义 必须具有相同数量和种类(即,类型或非类型)的参数
一个特定文件所需要的所有模板的声明 通常一起放置在 文件开始位置,出现于 任何使用这些模板的代码之前
3、使用类的类型成员
用作用域运算符(::)来访问 static 成员 和 类型成员。在普通(非模板)代码中,编译器掌握类的定义。因此,它知道 通过作用域运算符访问的名字是类型 还是 static 成员
假定 T 是一个模板类型参数,当编译器遇到类似 T::mem 这样的代码时,它不会知道 mem 是一个类型成员还是一个 static 数据成员,直到实例化时才会知道
默认情况下,C++ 语言假定 通过作用域运算符访问的名字 不是类型。因此,如果 希望使用一个模板类型参数的类型成员,就必须显式告诉编译器 该名字是一个类型。通过 使用关键字 typename 来实现这一点:
template <typename T>
typename T::value_type top(const T& c) { if (!c.empty())return c.back();elsereturn typename T::value_type(); // typename,使用 无参构造函数
}
使用 typename 指明其返回类型 并在 c 中没有元素时 生成一个值初始化的元素 返回给调用者
希望通知编译器一个名字表示类型时,必须使用关键字 typename,而不能使用 class,不同于 模板参数(<>)
4、默认模板实参
就像 能为函数参数提供默认实参一样,也可以提供 默认模板实参。在新标准中,可以为函数 和 类模板 提供默认实参。而更早的 C++ 标准只允许 为类模板提供默认实参
// compare 有一个默认模板实参 less<T> 和一个默认函数实参 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;
}
默认模板实参 指定 compare 将使用标准库的 less 函数对象类。它是 使用与 compare 一样的类型参数(<T>
)实例化的。默认函数实参 指出 f 将是类型 F 的一个默认初始化的对象
用户调用这个版本的 compare 时,可以提供 自己的比较操作,但这并不是必须的:
bool i = compare(0, 42); // 使用 less; i 为 -1
与函数默认实参一样
如果我们跳过 b 而仅为 c 提供默认值,会导致编译错误
void func(int a, int b, int c = 20) {// 函数体
}
func(5); // 错误:缺少 b 的实参
对于 一个模板参数,只有 当它右侧的所有参数都有默认实参时(template <typename T = int, typename U = double>
),它才可以有默认实参(这一规则的原因在于,模板参数是从左到右 依次被推导或提供的)
5、模板默认实参与类模板
无论何时 使用一个类模板,都必须 在模板名之后接上尖括号,尖括号 指出 类必须从一个模板实例化而来。特别是,如果一个类模板 为其所有模板参数都提供了默认实参,希望使用这些默认实参,就必须 在模板名之后跟一个尖括号对
template <class T = int> class Numbers { // T 默认为 int
public:Numbers(T v = 0) : val(v) { }// 对数值的各种操作
private:T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; // 空<> 表示我们希望使用默认类型(int代替T实例化得到)
6、声明为 typename 的类型参数和声明为 class 的类型参数有什么不同(如果有的话)?什么时候必须使用 typename
当用来声明模版类型参数时, typename 和 class 是完全等价的,都表明模版参数是一个类型。在 C++ 最初引入模版时,是使用 class 的。但为了避免与类(或类模版)定义中的 class 相混淆,引入了 typename 关键字。从字面上看,typename 还暗示了模版类型参数不必是一个类类型。因此,现在更建议使用 typename。
当在模版类型参数上使用作用域运算符 :: 来访问其成员时,如 T::value_type ,在实例化之前 无法辨别访问的到底是 static 数据成员还是类型成员。对此,C++ 默认通过 :: 访问的是 static 数据成员。为了指明访问的是类型成员,需要在名字前使用 typename 关键字,如
template <typename T>
typename T::value_type top(const T& c) { if (!c.empty())return c.back();elsereturn typename T::value_type(); // typename,使用 无参构造函数
}
typename T::value_type() ,表明 value_type 是类型成员,这里是创建一个 value_type 类型的对象,并进行初始化
7、解释下面两个函数模板声明 并指出它们是否非法
(b) template <typename T> T f2(int &T);
(c) inline template <typename T> T foo(T, unsigned int*);
(b)非法。在作用域中,模版参数名不能重用,而这里重用 T 作为函数参数名
(c)非法。函数模版可以声明为 inline 或 constexpr 的,如同非模版函数一样。inline 或 constexpr 说明符放在模版参数列表之后,返回类型之前
8、编写函数,接受一个容器的引用,打印容器中的元素。使用容器的 size_type 和 size 成员来控制打印元素的循环。使用 begin 和 end 返回的迭代器来控制循环
#include <iostream>
#include <vector>using namespace std;template<typename Container>
void print(Container& c, ostream& os = cout) {using s_type = typename Container::size_type; // typename表示类型,即下标类型for (s_type i = 0; i < c.size(); i++)os << c.at(i) << " ";os << endl;
}template<typename Container>
void print_iter(Container& c, ostream& os = cout) {using s_iter = typename Container::iterator;for (s_iter it = c.begin(); it != c.end(); it++) {os << *it << " ";}os << endl;
}int main()
{vector<int> vec = { 1, 2, 3, 4, 5 };print(vec);print_iter(vec);return 0;
}
1.4 成员模板
1、一个类(无论是普通类还是类模板)可以包含 本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数(在基类中使用 virtual 关键字声明一个函数,使其成为虚函数,通过基类指针或引用调用虚函数时,会根据实际对象的类型调用相应的派生类函数,而不是基类的函数)
2、普通(非模板)类的成员模板
定义一个类,类似于 unique_ptr 所使用的默认删除器类。类将包含 一个重载的函数调用运算符,接受一个指针并对此指针执行 delete。与默认删除器不同,我们的类 还将在删除器被执行时打印一条信息。由于 希望删除器适用于 不同类型,所以 将函数调用运算符定义为一个模板:
// 自定义删除类,对给定指针执行 delete
class DebugDelete {
public:DebugDelete(std::ostream &s = std::cerr) : os(s) { }// 与任何模板实例相同,T 的类型由编译器推断template <typename T> void operator()(T *p) const {os << "deleting unique_ptr" << std::endl; delete p;}
private:std::ostream &os;
};
成员模板 也是以模板参数列表开始的
double* p = new double;
DebugDelete d; // 可像 delete 表达式一样使用的对象
d(p); // 调用 DebugDelete::operator()(double*), 释放 p
int* ip = new int;
// 将一个临时 DebugDelete 对象上调用 operator()(int*)
DebugDelete()(ip);
调用一个 DebugDelete 对象会 delete 其给定的指针,也可以将 DebugDelete 用作 unique_ptr 的删除器。为了重载 unique_ptr 的删除器,在尖括号内 给出删除器类型,并提供一个这种类型的对象 给 unique_ptr 的构造函数
// 销毁 p 指向的对象
// 实例化 DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// 销毁 sp 指向的对象
// 将传给 DebugDelete::operator()<string*>(string*)
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
声明 p 的删除器的类型为 DebugDelete,并在 p 的构造函数中提供了该类型的一个未命名对象
unique_ptr 的析构函数会调用 DebugDelete 的调用运算符。无论何时 unique_ptr 的析构函数实例化时,DebugDelete 的调用运算符会被实例化
3、类模板的成员模板
对于类模板,也可以为其定义成员模板。在此情况下,类和成员 各有自己的、独立的模板参数
希望 支持不同类型序列的迭代器,因此 将构造函数定义为模板:
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)) { }
4、实例化与成员模板
为了实例化 一个类模板的成员模板,必须 同时提供类 和 函数模板的实参。在对象上调用成员模板,编译器就根据该对象的类型 来推断类模板的实参(类的实参必须显式提供)。与普通函数模板相同,编译器通常根据 传递给成员模板的函数实参 来推断它的模板实参
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"};// 实例化 Blob<int> 并且接受两个 int* 参数的构造函数
Blob<int> a1(begin(ia), end(ia));// 实例化 Blob<int> 并且接受两个 vector<long>::iterator 的构造函数
Blob<int> a2(vi.begin(), vi.end());// 实例化 Blob<string> 并且接受两个 list<const char*>::iterator 参数的构造函数
Blob<string> a3(w.begin(), w.end());
定义 a1 时,显式地指出编译器应该实例化一个 int 版本的 Blob。构造函数自己的类型参数 则通过 begin(ia) 和 end(ia) 的类型来推断,结果为 int*。因此,a1 的定义实例化了如下版本:
Blob<int>::Blob(int*, int*);
5、编写 自己的 DebugDelete 版本
DebugDelete.h
#ifndef DEBUGDELETE_H
#define DEBUGDELETE_H#include <iostream>
#include <string>class DebugDelete {
public:DebugDelete(const std::string& str, std::ostream& so = std::cerr) : info(str), os(so) {} // info(str)应该是调用 对应的参数为常量的初始化构造函数template <typename T> void operator()(T *p) { // 成员函数,不是构造函数os << info << " delete" << std::endl;delete p;}
private:std::string info;std::ostream &os; // 无法复制,只能引用
};#endif
16.21.cpp
#include "DebugDelete.h"using namespace std;int main()
{double* d = new double;DebugDelete dd("double");dd(d);int* i = new int();DebugDelete dd2("int");dd2(i);std::shared_ptr<int> sp(new int, DebugDelete("shared_ptr")); // 传入类即可,删除的时候自动调用()运算符return 0;
}
运行结果
初始化方式的选择
使用new int:当你想要分配内存但不需要立即初始化时,可以使用这种方式。这通常在性能关键的代码中使用,避免了不必要的初始化开销,但需要特别小心使用未初始化的值(new int分配了一个整数类型的动态内存,但没有初始化它)
使用new int():当你需要确保变量被初始化(通常初始化为零)时,可以使用这种方式。这种方式更安全,避免了未初始化变量导致的潜在错误(new int()不仅分配了一个整数类型的动态内存,还将其初始化为0)
隐式转换发生在以下几种情况下:
- 基本数据类型之间的转换。例如从 int 到 float,从 char 到 int 等
- 常量性转换。传递参数给函数时,const 限定符允许非 const 对象绑定到 const 引用上
void print(const std::string& str) {std::cout << str << std::endl;
}std::string s = "Hello";
print(s); // 非 const std::string 绑定到 const std::string&
- 类类型的构造函数和转换运算符
类类型可以定义构造函数和转换运算符,允许对象之间进行隐式转换
3.1 单参数构造函数
单参数构造函数可以用于隐式转换,但通常通过 explicit 关键字禁用隐式转换
class MyClass {
public:MyClass(int x) { /*...*/ } // 隐式转换
};MyClass obj = 10; // int 隐式转换为 MyClass
3.2 转换运算符
类可以定义转换运算符,以允许对象转换为其他类型
class MyClass {
public:operator int() const { return 42; }
};MyClass obj;
int i = obj; // MyClass 隐式转换为 int
- 派生类到基类的转换
派生类的指针或引用可以隐式转换为基类的指针或引用。这是因为派生类对象总是包含基类对象
class Base { /*...*/ };
class Derived : public Base { /*...*/ };Derived d;
Base* b = &d; // Derived* 隐式转换为 Base*
- 数组和指针转换
数组名称隐式转换为指向其第一个元素的指针 - 用户定义的类型转换
用户可以通过定义类型转换运算符或单参数构造函数来启用用户定义类型之间的隐式转换。
6.1 转换运算符
class Fraction {
public:operator double() const { return static_cast<double>(numerator) / denominator; }
private:int numerator;int denominator;
};Fraction f;
double d = f; // Fraction 隐式转换为 double
6.2 单参数构造函数
class Fraction {
public:Fraction(int n) : numerator(n), denominator(1) { }
private:int numerator;int denominator;
};Fraction f = 5; // int 隐式转换为 Fraction
- 通过模板进行隐式转换
模板可以定义一种类型到另一种类型的隐式转换
template<typename T>
class Wrapper {
public:Wrapper(T t) : value(t) { }operator T() const { return value; }
private:T value;
};Wrapper<int> w = 42;
int i = w; // Wrapper<int> 隐式转换为 int
禁用隐式转换
为了避免不期望的隐式转换,可以使用 explicit 关键字
class MyClass {
public:explicit MyClass(int x) { /*...*/ }
};MyClass obj = 10; // 编译错误:需要显式转换
MyClass obj2(10); // 正确:显式转换
1.5 控制实例化
1、当模板被使用时 才会进行实例化。这一特性意味着,相同的实例 可能出现在多个对象文件中。当两个或多个独立编译的文件 使用了相同的模板,并提供了相同的模板参数时,每个文件中 就都会有该模板的一个实例
在多个文件中 实例化相同模板的额外开销 可能非常严重。在新标准中,通过显式实例化 来避免这种开销
extern template declaration; // 实例化声明
template declaration; // 实例化定义
declaration 是一个类 或 函数声明,其中所有模板参数 已被替换为模板实参
// 实例化声明与定义
extern template class Blob<string>; // 声明
template int compare(const int&, const int&); // 定义
当编译器遇到 extern 模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为 extern 就表示 承诺在程序其他位置 有实例化的一个非 extern 声明(定义)。对于一个给定的实例化版本,可能有多个 extern 声明,但必须只有一个定义(非extern声明)
由于编译器 在使用一个模板时 自动对其实例化,因此 extern 声明必须出现在任何使用此实例化版本的代码之前:
// Application.cc
// 这些模板实例将在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sal, sa2; // 实例化会出现在其他位置// Blob<int>及其接受 initializer_list 的构造函数在本文件中实例化
Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
Blob<int> a2(al); // 拷贝构造函数 在本文件中实例化
int i = compare(a1[0], a2[0]); // 实例化出现在其他位置
文件 Application.o 将包含 Blob<int>
的实例 及其接受 initializer_list 参数的构造函数 和 拷贝构造函数的实例。而 compare<int>
函数和 Blob<string>
类将不在本文件中进行实例化。这些模板的定义 必须出现在程序的其他文件中:
// templateBuild.cc
// 实例化文件必须为 每个在其他文件中声明为 extern 的类型和函数 提供一个(非 extern)定义
template int compare(const int&, const int&);
template class Blob<string>; // 实例化类模板的所有成员
当编译器 遇到一个实例化定义(与声明相对)时,它为其生成代码。因此,文件 templateBuild.o 将包含 compare 的 int 实例化版本的定义 和 Blob<string>
类的定义。编译此应用程序时,必须将 templateBuild.o 和 Application.o 链接到一起
2、实例化定义会实例化所有成员:一个类模板的实例化定义 会实例化该模板的所有成员,包括 内联的成员函数。当编译器遇到 一个实例化定义时,它不了解程序使用哪些成员函数。即使 不使用某个成员,它也会被实例化。因此,用来显式实例化一个类模板的类型,必须能用于模板的所有成员
假设 NoDefault 是一个没有默认构造函数的类,可以显式实例化 vector<NoDefault>
吗?
不能,如果没有默认构造函数,该类vector无法实例化
对下面每条带标签的语句,解释发生了什么样的实例化(如果有的话)。如果一个模版被实例化,解释为什么;如果未实例化,解释为什么没有
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)实例化,在类中定义;
(d)没有实例化,引用和指针不需要实例化;
(e)实例化,调用时需要实例化;
(f)实例化,sizeof 需要知道 Stack<string>
的定义才能给出一个 Stack<string>
对象的大小,会实例化
模板被实例化:
显式实例化:在源文件中显式地实例化模板,例如 template class MyClass<int>;
。这告诉编译器生成 MyClass<int>
的实例。
隐式实例化:当代码使用特定类型的模板时,例如 MyClass<int> obj;
,编译器隐式地生成该类型的实例,因为需要完成代码的编译和链接
模板未实例化:
从未使用:如果模板从未在代码中被使用,无论是显式实例化还是隐式实例化,编译器都不会生成模板的实例。例如,在 main 函数中未创建 MyClass<double>
对象,则 MyClass<double>
不会被实例化。
仅声明无定义:如果模板仅声明而未定义,且没有实例化需求,模板不会被实例化。例如,只有 MyClass<T>
的声明而无定义,且未在代码中使用该模板,则编译器不会实例化该模板
template <typename T>
class MyClass {
public:void doSomething();
};#include <iostream>// 模板成员函数的定义
template <typename T>
void MyClass<T>::doSomething() {std::cout << "Doing something with type " << typeid(T).name() << std::endl;
}// 显式实例化
// template class MyClass<int>;int main() {MyClass<int> obj;// 隐式实例化:需要 MyClass<int> 的定义,没有显式初始化也会自己定义obj.doSomething(); // MyClass<int> 被隐式实例化,因为对象 obj 被创建并使用return 0;
}
使用显式模板实参的场景:无法自动推导模板参数
有些情况下,模板参数无法从函数实参中推导出来,必须显式指定。例如,使用默认值或涉及到复杂类型时
template <typename T>
T createDefault() {return T(); // 需要显式指定 T
}int main() {int value = createDefault<int>(); // 必须显式指定 intreturn 0;
}
1.6 效率与灵活性
1、shared_ptr 和 unique_ptr 之间的明显不同是 它们管理所保存的指针的策略——前者给予共享指针所有权的能力;后者则独占指针
这两个类的另一个差异是 它们允许用户重载默认删除器的方式
可以很容易地重载一个 shared_ptr 的删除器,只要在创建或 reset 指针时 传递给它一个可调用对象即可以
这是通过将删除器 作为可调用对象传递给 shared_ptr 的构造函数或 reset 方法来实现的。删除器可以是函数指针、函数对象或 lambda 表达式
#include <iostream>
#include <memory>// 自定义删除器
void customDeleter(int* ptr) {std::cout << "Custom deleting an integer: " << *ptr << std::endl;delete ptr;
}int main() {// 创建 shared_ptr,使用自定义删除器std::shared_ptr<int> sp(new int(42), customDeleter);// 重置 shared_ptr,使用 lambda 表达式作为删除器sp.reset(new int(55), [](int* ptr) {std::cout << "Lambda custom deleting an integer: " << *ptr << std::endl;delete ptr;});return 0;
}
运行结果
Custom deleting an integer: 42
Lambda custom deleting an integer: 55
删除器的类型 是一个 unique_ptr 对象的类型的一部分。用户必须在定义 unique_ptr 时 以显式模板实参的形式 提供 删除器的类型。对于 unique_ptr 的用户来说,这使得 unique_ptr 在使用自定义删除器时类型更加固定
这意味着在定义 unique_ptr 时,必须在模板参数中显式地指定删除器的类型。这使得删除器在编译时就确定了
#include <iostream>
#include <memory>// 自定义删除器
struct CustomDeleter {void operator()(int* ptr) const {std::cout << "Custom deleting an integer: " << *ptr << std::endl;delete ptr;}
};int main() {// 创建 unique_ptr,使用自定义删除器std::unique_ptr<int, CustomDeleter> up(new int(42), CustomDeleter()); // 后面还要加CustomDeleter()// 重置 unique_ptrup.reset(new int(55));return 0;
}
输出:
Custom deleting an integer: 42
Custom deleting an integer: 55
unique_ptr 的类型中包含了自定义删除器 CustomDeleter。这意味着 unique_ptr 的类型为 std::unique_ptr<int, CustomDeleter>。删除器类型在编译时确定,不能在运行时更改
2、在运行时绑定删除器
shared_ptr 必须直接访问其删除器,即,删除器 必须保存为一个指针 或 一个封装了指针的类
可以确定 shared_ptr 不是将删除器直接保存为一个成员,因为删除器的类型 直到运行时才会知道。实际上,在一个 shared_ptr 的生存期间中,可以随时改变其删除器的类型。可以使用一种类型的删除器 构造一个 shared_ptr,随后使用 reset 赋予此 shared_ptr 另一种类型的删除器。类成员的类型在运行时是不能改变的。因此,不能直接保存删除器
(类成员的类型在编译时就必须确定,不能在运行时改变。这是因为 C++ 是一种静态类型的语言,编译器需要在编译时知道所有类型信息,以便生成正确的机器代码和执行类型检查。例如,如果你有一个类 A,其中有一个成员变量 int x,那么 x 的类型始终是 int,在程序运行过程中不能变成 double 或其他类型:class A { public: int x; // x 的类型在编译时就确定了,且不能改变 };
)
假定 shared_ptr 将它管理的指针保存在一个成员 p 中,且删除器是通过 一个名为 del 的成员来访问的,则 shared_ptr 的析构函数 必须包含类似下面这样的语句:
// del 的类型是在运行时才知道,通过一个指针去调用它
del ? del(p): delete p; // del(p) 需要运行时跳转到 del 的地址,如果del不存在就用默认的 delete
由于删除器是间接保存的,调用 del(p)
需要一次运行时的跳转操作,转到 del 中保存的地址 来执行相应的代码(delete函数本身不是 shared_ptr 类的成员)
3、在编译时绑定删除器
考察 unique_ptr 可能的工作方式。在这个类中,删除器的类型是 类类型的一部分,即,unique_ptr 有两个模板参数,一个表示它所管理的指针,另一个表示删除器的类型。由于删除器的类型是 unique_ptr 类型的一部分,因此删除器成员的类型 在编译时是知道的,从而 删除器可以直接保存在 unique_ptr 对象中
unique_ptr 的析构函数与 shared_ptr 的析构函数类似,也是 对其保存的指针调用 用户提供的删除器或者默认的 delete
del 的类型 或者是默认删除器类型,或者是 用户提供的类型。到底是哪种情况没有关系,应该执行的代码 在编译时肯定会知道
通过 在编译时绑定删除器,unique_ptr 避免了间接调用删除器的运行时开销
通过在运行时 绑定删除器,shared_ptr 使用户重载删除器更为方便
4、编写 自己的 shared_ptr 和 unique_ptr
Deleter.h
#pragma once // 不加报错“Deleter” : “class”类型重定义 #include <iostream>class Deleter {
public:Deleter(const std::string& str = "smart pointer", std::ostream &os = std::cerr): os(os), type(str) {}template <typename T>void operator()(T* p) {os << "deleting " << type << std::endl;delete p;}
private:std::ostream& os; // 输出提示信息std::string type; // 什么类型的智能指针正在删除
};
SharedPtr.h
#pragma once
#ifndef SHAREDPTR_H
#define SHAREDPTR_H#include "Deleter.h"
#include <functional>
// 要使用std::function,需要包含头文件<functional>template <typename T>
class SharedPtr;template <typename T>
bool operator==(const SharedPtr<T>& lhs, const SharedPtr<T>& rhs);template <typename T>
class SharedPtr {friend bool operator==<T>(const SharedPtr<T>&, const SharedPtr<T>&);// 只有友元才可能函数名称后面加<T>
public:SharedPtr() : ptr(nullptr), cnt(nullptr) {} // std::function<void(T*)> del不需要初始化SharedPtr(T *p, std::function<void(T*)> d = Deleter()) // std::function的初始化: ptr(p), del(d), cnt(new std::size_t(1)) {} // 使用size_t的构造函数,返回指针SharedPtr(const SharedPtr& p) // 复制控制函数: ptr(p.ptr), del(p.del), cnt(p.cnt){++*cnt; // 复制计数+1debug(); // 输出相关信息,如果操作的为空指针抛出错误}SharedPtr& operator=(const SharedPtr& p);T operator*() const { return *ptr; }T* operator->() const { return ptr; }// 使用 ptr->member 时,编译器实际上将其转换为 (*ptr).member// 当你重载 operator-> 时,必须遵循这种语义。因此,重载 operator-> 的返回类型通常是一个指针// 以便编译器能够继续使用这个指针访问对象的成员operator bool() const { return ptr != nullptr; }// 当定义类型转换操作符(例如 operator bool())时,实际上是在定义一个将对象转换为特定类型的隐式转换函数// 类型转换操作符不需要显式的返回类型声明,因为返回类型隐含在操作符的名称void reset(T* p);void reset(T* p, std::function<void(T*)> deleter);~SharedPtr();void debug() {if (cnt)std::cout << "ref cnt" << *cnt << std::endl;elsethrow std::runtime_error("cnt points to none");}private:T* ptr; // 指针指向什么std::function<void(T*)> del;// std::function<void(int*)> 是一个函数包装器,可以存储任何返回类型为 void,参数类型为 int* 的可调用目标。// 将 Deleter<int> 对象存储在 std::function<void(int*)> 中std::size_t* cnt; // 注意是指针
};template <typename T>
SharedPtr<T>& SharedPtr<T>::operator=(const SharedPtr& p) {++*p.cnt;if (--*cnt == 0) {del ? del(ptr) : delete ptr; // del函数只负责删除ptrdelete cnt;}ptr = p.ptr;del = p.del;cnt = p.cnt;debug();return *this;
}template <typename T>
void SharedPtr<T>::reset(T* p) {if (--*cnt == 0) {if del ? del(ptr) : delete ptr;delete cnt;}ptr = p.ptr;cnt = new std::size_t(1);
}template <typename T>
void SharedPtr<T>::reset(T* p, std::function<void(T*)> deleter) { // 传递函数reset(p);del = deleter;
}template <typename T>
SharedPtr<T>::~SharedPtr() {if (--*cnt == 0) { // 需要--if del ? del(ptr) : delete ptr;delete cnt;}// 当一个SharedPtr对象被销毁时,它需要递减引用计数器,以表示当前有一个SharedPtr对象不再持有对资源的引用// 递减后,如果引用计数器的值为0,则表示没有任何SharedPtr对象再持有对资源的引用,此时应该删除资源和引用计数器
}// == / != 运算符是对ptr操作,即指向对象是否一致
template <typename T>
bool operator==(const SharedPtr<T>& lhs, const SharedPtr<T>& rhs) {return lhs.ptr == rhs.ptr;
}// 借助==
template <typename T>
bool operator!=(const SharedPtr<T>& lhs, const SharedPtr<T>& rhs) {return !(lhs == rhs);
}template <typename T>
SharedPtr<T> &make_shared() { // 多加了一个&也没事SharedPtr<T> s(new T);return s;
}#endif
UniquePtr.h
#pragma once
// 每个资源只能有一个std::unique_ptr拥有,std::unique_ptr: 较小的性能开销,因为没有引用计数
// 不能复制std::unique_ptr,只能移动它
// 当std::unique_ptr超出作用域或被销毁时,它会自动释放所管理的资源
#ifndef UNIQUEPTR_H
#define UNIQUEPTR_H#include "Deleter.h"template <typename T, typename D = Deleter> // 保存函数类型的话只要写类名就行,有默认类型
class UniquePtr {
public:UniquePtr(T *p = nullptr, D d = Deleter()) : ptr(p), del(d) {}~UniquePtr(){del(ptr);}/* 当UniquePtr对象被销毁时:1、~UniquePtr()析构函数被调用。2、 del(ptr); 这行代码调用删除器来释放指针管理的资源,D怎么析构是Deleter的事3、 当UniquePtr对象超出作用域时,ptr和del这两个成员变量都会自动销毁如果del是一个用户定义的类或者是一个函数对象,其析构函数也会自动调用进行清理*/UniquePtr(UniquePtr&& u) : ptr(u.ptr), del(u.del){u.ptr = nullptr; // 注意移动了之后被移动的就无了}UniquePtr& operator=(UniquePtr&& u);T operator*() const { return *ptr; }T* operator->() const { return ptr; }void reset(T* p) {del(ptr); // 先把原来的删掉ptr = p; // 传递指针}void reset(T* p, D d) {reset(p);del = d;}private:T* ptr;D del;
};// 防止自我赋值,自我赋值是指一个对象试图将自己赋值给自己。这种情况在赋值运算符中是合法的,但如果不处理,会导致未定义行为或者错误
// 具体来说,UniquePtr类的移动赋值运算符中,自我赋值可能会导致不必要的资源释放和错误的状态变更
template <typename T, typename D>
UniquePtr<T, D>& UniquePtr<T, D>::operator=(UniquePtr&& u) {if (this != &u) {del(ptr);ptr = u.ptr;u.ptr = nullptr;del = u.del;}return *this;
}#endif
16.28.cpp
#include "Deleter.h"
#include "SharedPtr.h"
#include "UniquePtr.h"int main() {// Deleter 构造函数有默认实参,可传可不传。没有传的话用默认实参SharedPtr<int> sp1(new int(42), Deleter());UniquePtr<int> sp2(new int(43), Deleter("unique_ptr"));return 0;
}
对于 shared_ptr(我们的版本命名为 SharedPtr),关于引用计数的管理、拷贝构造函数和拷贝赋值运算符等的设计,参考 HasPtr 即可
对于 unique_ptr(我们的版本命名为 UniquePtr),无需管理引用计数,也不支持拷贝构造函数和拷贝赋值运算符,只需设计 release 和 reset 等函数实现资源释放即可
unique_ptr 在编译时绑定删除器,shared_ptr 在运行是绑定编译器
如果 将 DebugDelete 与 unique_ptr 一起使用,解释编译器将删除器处理为内联形式的可能方式
#ifndef DEBUGDELETE_H
#define DEBUGDELETE_H#include <iostream>
#include <string>class DebugDelete {
public:DebugDelete(const std::string& str, std::ostream& so = std::cerr) : info(str), os(so) {}template <typename T> void operator()(T *p) { // 成员函数,不是构造函数os << info << " delete" << std::endl;delete p;}
private:std::string info;std::ostream &os; // 无法复制,只能引用
};#endif
删除器类型 在编译时就可知道,删除器可直接保存在 unique_ptr 对象中。unique_ptr 避免了间接调用删除器的运行时开销,而编译时还可以将自定义的删除器,如 DebugDelete 编译为内联形式(上文的 Deleter 删除器也是这个原理)
2、模板实参推断
1、从函数实参来确定模板实参的过程称为模板实参推断。模板实参推断时,编译器 使用调用中的实参类型 来寻找模板实参,用这些模板实参 生成的函数版本 与给定的函数调用最为匹配
2.1 类型转换与模板类型实参
1、与非模板函数一样,在一次调用中 传递给函数模板的实参 被用来初始化函数的形参。如果一个函数形参的类型 使用了模板类型实参(T),那么它采用特定的初始化规则
只有很有限的几种类型转换 会自动地应用于这些实参。编译器通常不对实参进行类型转换,而是生成一个新的模板实例
2、与往常一样,顶层 const 无论是在形参 还是在实参中 都会忽略(值传递时创建的是原始对象的副本,副本是一个新的独立对象,与原始对象的属性(如 const)无关)
顶层 const 和底层 const
顶层 const:表示变量本身是 const 的
底层 const:表示指针或引用指向的对象是 const 的
- const 转换:可以将一个非 const 对象的引用(或指针)传递给一个 const 的引用(或指针)形参(普通类型的不行,非模板类可以)
- 数组或函数指针转换:如果数组形参 不是引用类型,则可以 对数组和函数类型的实参 应用于常用的指针转换。一个数组实参 可以转换为一个指向其首元素的指针。类似的,一个函数实参 可以转换为一个该函数类型的指针
其他类型转换,如算术转换(算术类型包括整型(如 int、short、long 等)和浮点型(如 float、double 等))、派生类向基类的转换 以及用户定义的转换,都不能应用于函数模板
用户定义类型转换
1)转换构造函数
转换构造函数是一种接受一个参数的构造函数,它可以实现从一个类型到另一个类型的转换。例如:
class MyClass {
public:MyClass(int value) : data(value) {}
private:int data;
};int main() {MyClass obj = 10; // 通过转换构造函数将 int 转换为 MyClassreturn 0;
}
2)转换运算符
转换运算符是一种特殊的成员函数,它定义了对象可以转换为另一种类型。例如:
class MyClass {
public:MyClass(int value) : data(value) {}// 定义到 int 的转换运算符operator int() const {return data;}
private:int data;
};int main() {MyClass obj(10);int value = obj; // 通过转换运算符将 MyClass 转换为 intreturn 0;
}
类型转换:隐式类型转换;显式类型转换(static_cast, dynamic_cast, const_cast, 或 reinterpret_cast)
template <typename T1, typename T2>
T2 convertType(T1 input) {return static_cast<T2>(input);
}int main() {int a = 10;double b = convertType<int, double>(a); // Convert int to doublecout << "Converted value: " << b << endl;double x = 3.14;int y = convertType<double, int>(x); // Convert double to intcout << "Converted value: " << y << endl;return 0;
}
template <typename T> T fobj(T, T); // 实参被拷贝
template <typename T> T fref(const T&, const T&); // 引用
string s1("a value");
const string s2("another value");
fobj(s1, s2); // 调用 fobj(string, string); const 被忽略
fref(s1, s2); // 调用 fref(const string&, const string&) // 将 s1 转换为 const 是允许的
int a[10], b[42];
fobj(a, b); // 调用 f(int*, int*)
fref(a, b); // 错误:数组类型不匹配
对于 一个引用参数来说, 转换为 const 是允许的, 因此这个调用也是合法的
在下一对调用中,传递了数组实参, 两个数组大小不同, 因此是不同类型
在 fobj 调用中, 数组大小不同无关紧要。 两个数组 都被转换为指针。 fobj 中的模板类型为int*。 但是,fref 调用是不合法的,如果形参是一个引用, 则数组不会转换为指针,a和b的类型是不匹配的, 因此调用是错误的
3、使用 相同模板参数类型的函数形参:如果推断出的类型不同,则调用 就是错误的。 compare 函数接受两个 const T& 参数,其实参必须是相同类型:
long lng;
compare(lng, 1024); // 错误:不能实例化 compare(long, int)
// 模板函数没有隐式数值转换
如果希望 允许对函数实参 进行正常的类型转换,可以将函数模板 定义为两个类型参数:
// 实参类型可以不同,但必须兼容
template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2)
{if (v1 < v2) return -1;if (v2 < v1) return 1;return 0;
}
现在用户可以提供不同类型的实参了:
long lng;
flexibleCompare(lng, 1024); // 正确:调用 flexibleCompare(long, int)
必须定义了能比较这些类型的值的 < 运算符
4、正常类型转换 应用于普通函数实参
函数模板 可以有用 普通类型定义的参数(不涉及模板类型参数的类型)。这种函数实参 不进行特殊处理:正常转换为 对应形参的类型
template <typename T> ostream &print(ostream &os, const T &obj)
{return os << obj;
}
由于 os 的类型是固定的,因此调用 print 时,传递给它的实参 会进行正常的类型转换:
print(cout, 42); // 实例化 print(ostream&, int)
ofstream f("output");
print(f, 10); // 使用 print(ostream&, int);将 f 转换为 ostream&(非模板转换)
5、
template <typename T> T calc(T, int);
char c;
calc(c, 'c');
调用合法。因为 calc 的第二个参数是 int 类型,所以可以进行正常的类型转换,将 char 转换为 int。而 T 被推断为 char(非模板参数 数值转换)
template <typename T> f1(T, T);
int i = 0, j = 42, *p1 = &i;
const int *cp1 = &i;
f1(p1, cp1);
illegal: error: no matching function for call to ‘ff1(int*&, const int*&)’ (底层const)
2.2 函数模板显式实参
编译器无法推断出模板实参的类型;希望允许用户控制模板实例化。当函数返回类型 与参数列表中任何类型 都不相同时,这两种情况最常出现
1、指定显式模板实参
可以定义 表示返回类型的第三个模板参数,从而 允许用户控制返回类型:
// 编译器无法推断 T1,它由调用显式指定
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
没有任何函数实参的类型 可用来推断 T1 的类型。每次调用 sum 时 调用者都必须为 T1 提供一个显式模板实参
提供显式模板实参的方式 与定义类模板实例的方式相同。显式模板实参 在尖括号中给出,位于函数名之后,实参列表之前
// T1 是显式指定的,T2 和 T3 是从函数实参类型推断而来的
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
显式指定 T1 的类型,而 T2 和 T3 的类型 则由编译器从 i 和 lng 的类型推断出来
显式模板实参 按左至右的顺序与对应的模板参数匹配;只有尾部(最右)参数的显式模板实参才可以忽略,而前提是 它们可以从函数参数推断出来
// 糟糕的设计:用户必须指定所有三个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
总是必须为所有三个形参指定实参:在这个调用中,只显式指定了第一个模板参数 T1 为 long long。然而,编译器还需要知道 T2 和 T3 的类型。编译器会按照从左到右的顺序匹配模板参数:
(模板实参和函数实参 分别从左到右匹配)
编译器首先将 i 的类型匹配到 T2
然后将 lng 的类型匹配到 T1
但是,T3 的类型无法通过函数参数推断出来,因为 T3 既不是函数参数的类型,也不是返回值的类型。因此,编译器无法确定 T3 的类型,这会导致编译错误
// 错误:不能推断前两个模板实参
auto val3 = alternative_sum<long long>(i, lng);
// 正确:显式指定了所有三个实参类型
auto val2 = alternative_sum<long long, int, long>(i, lng);
2、正常类型转换 应用于显式指定的实参
对于 用于普通类型实参的函数参数,允许进行正常的类型转换,出于同样的原因,对于模板实参类型 也已经显式指定了的函数实参,也进行正常的类型转换:
template <typename T>
int compare(const T &v1, const T &v2)
{if (v1 < v2) return -1;if (v2 < v1) return 1;return 0;
}
long lng;
compare(lng, 1024); // 错误:模板参数不匹配
compare<long>(lng, 1024); // 正确:实例化 compare(long, long)
compare<int>(lng, 1024); // 正确:实例化 compare(int, int)
因为传递给 compare 的实参 必须具有相同的类型。如果 显式指定模板类型参数,就可以 进行正常类型转换。因此,调用 compare<long>
等价于 调用一个 接受两个 const long& 参数的函数。int 类型的参数被自动转换为 long。在第二个调用中,T 被显式指定为 int,因此 lng 被转换为 int
3、标准库 max 函数有两个参数,它返回实参中的较大者。此函数有一个模版类型参数。你能在调用 max 时传递给它一个 int 和一个 double 吗?可以用一个 int 和 一个 double 调用 max,显式指定模版实参即可:
int a = 1;
double b = 2.0;
max<double>(a, b);
为什么需要显式模板实参
- 模板参数推断失败
- 强制特定类型
以 make_shared 为例 如何使用显式模板实参
1)从构造函数参数推断类型
#include <memory>
#include <vector>std::shared_ptr<std::vector<int>> ptr1 = std::make_shared<std::vector<int>>(10, 42);
编译器可以从构造函数参数 10, 42 中推断出模板参数类型 std::vector<int>
,因此不需要显式指定模板实参
2)显式指定模板实参
创建一个空的 std::vector<int>
,而不传递任何构造函数参数:
#include <memory>
#include <vector>std::shared_ptr<std::vector<int>> ptr2 = std::make_shared<std::vector<int>>();
编译器仍然可以从 std::vector<int>
的默认构造函数推断出模板参数类型 std::vector<int>
假设我们有一个自定义类,其构造函数参数可以是不同类型,而我们希望强制使用特定类型:
#include <memory>class MyClass {
public:MyClass(int x) {}MyClass(double x) {}
};std::shared_ptr<MyClass> ptr3 = std::make_shared<MyClass>(3.14);
编译器会推断模板参数类型为 MyClass,并选择 MyClass(double) 构造函数
强制使用特定类型
希望显式指定模板参数类型为 MyClass 并使用 int 构造函数,可以显式指定模板实参:
std::shared_ptr<MyClass> ptr4 = std::make_shared<MyClass, int>(42);
2.3 尾置返回类型与类型转换
1、希望用户 确定返回类型时,用显式模板实参 表示模板函数的返回类型是很有效的。但在其他情况下,要求 显式指定模板实参 会给用户增加额外负担,而且 不会带来什么好处
例如,可能希望编写一个函数,接受 表示序列的一对迭代器 和 返回序列中的一个元素的引用:
template <typename It>
??? fcn(It beg, It end)
{// 处理序列return *beg; // 返回序列中一个元素的引用
}
并不知道 返回结果的准确类型,但知道 所需类型是所处理的序列的元素类型
函数应该返回 *beg, 而且知道 可以用 decltype(*beg) 来获取 此表达式的类型。但是,在编译器遇到函数的参数列表之前,beg 都是不存在的。为了定义函数,必须用 尾置返回类型。由于尾置返回 出现在参数列表之后,它可以使用函数的参数:
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{// 处理序列return *beg; // 返回序列中一个元素的引用
}
解引用运算符 返回一个左值 (变量 和 解引用指针的结果 都是左值),因此通过 decltype 推断的类型为 beg 表示的元素的类型的引用。如果 对于一个 string 序列调用 fcn,返回类型 将是 string&
2、进行类型转换的标准库模板类:有时 无法直接获得 所需要的类型。可能希望 编写一个类似 fcn 的函数,但是返回一个元素的值 而非引用
对于 传递的参数的类型,几乎一无所知。在此函数中,知道唯一可以使用的操作是 迭代器构造,而 所有迭代器操作 都不会生成元素,只能生成元素的引用
为了获得元素类型,可以使用 标准库的类型转换模板。这些模板 定义在头文件 type_traits 中
在本例中,可以使用 remove_reference 来获得元素类型。remove_reference 模板 有一个模版类型参数 和 一个名为 type 的 (public) 类型成员。如果我们 用一个引用类型实例化 remove_reference,则 type 将表示 被引用的类型。例如,如果我们实例化 remove_reference<int&>, 则 type 成员将是 int
在这里插入代码片
显式模板实参的必要性
- 参数不够明确:例如,当构造函数的参数类型可以对应多个模板参数时,编译器无法确定该使用哪个模板参数
- 模板参数与传递参数类型不一致:有时候,我们希望传递的参数类型与模板参数类型不同,需要显式指定模板参数
- 返回类型不明确:例如,当返回类型依赖于模板参数,而编译器无法通过参数推断返回类型时
remove_reference<decltype(*beg)>::type
将获得 beg 引用的元素的类型:decltype(*beg) 返回元素类型的引用类型。remove_reference::type 脱去引用,剩下元素类型本身
组合使用 remove_reference,尾置返回及 decltype,就可以在函数中返回元素的拷贝:
// 为了使用模板参数的成员,必须用 typename:type 是一个类的成员,而该类依赖于一个模板参数。因此,我们必须在返回类型的声明中使用 typename 来告知编译器,type 表示一个类型
template <typename It>
auto fcn2(It beg, It end) ->typename remove_reference<decltype(*beg)>::type
{// 处理序列return *beg; // 返回序列中一个元素的拷贝
}
3、
每个类型转换模板的工作方式 都与 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;
}
函数是合法的,但用 decltype(*beg + 0) 作为尾置返回类型,导致两个问题:
1)序列元素类型必须支持 + 运算
2)*beg + 0 是右值,因此 fcn3 的返回类型被推断为元素类型的常量引用
编写一个新的 sum 版本,它返回类型保证足够大,足以容纳加法结果
sum 有两个模版参数,分别表示 两个加法运算对象的类型,当然 它们应该是相容的类型。在设计尾置返回类型时,首先计算 sum 的两个函数参数的和,然后对它们应用 decltype 来获得足以容纳和的返回类型
template <typename T1, typename T2>
auto sum(T1 a, T2 b) -> decltype(a + b) {return a + b;
}
利用表达式类型 来确定返回类型,甚至包括 字符串连接,比如:auto result = sum(std::string("hello, "), "world!");
又如:sum(1, 2.5) int + double - > double
2.4 函数指针和实参推断
1、当 用一个函数模板初始化一个函数指针 或 为一个函数指针赋值时,编译器使用 指针的类型 来推断模板实参
有一个函数指针,它指向的函数返回 int,接受两个参数,每个参数都是指向 const int 的引用,可以使用该指针指向 compare 的一个实例
template <typename T> int compare(const T&, const T&);
// pf1 指向实例 int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
pf1 的参数类型 决定了 T 的模板实参类型。本例中,T 的模板实参类型为 int。指针 pf1 指向 compare 的 int 版本实例。如果不能 从函数指针类型推断模板实参,则产生错误:
// func 的重载版本;每个版本接受一个不同的函数指针类型
void func(int (*)(const string&, const string&));
void func(int (*)(const int&, const int&));
func(compare); // 错误:使用 compare 的哪个实例?
通过 func 的参数类型 无法确定模板实参的唯一类型
通过显式模板实参 来消除 func 调用时的歧义:
// 正确:显式指定实例化 compare 版本,T为int
func(compare<int>); // 传递 compare(const int&, const int&)
2.5 模板实参推断和引用
1、template <typename T> void f(T &p);
函数参数 p 是 一个模板类型参数 T 的引用
编译器会应用正常的引用绑定规则;如果传入 const 是底层的,不是顶层的
2、从左值引用函数参数 推断类型:当一个函数参数是 模板类型参数的一个普通(左值)引用时,绑定规则告诉我们,只能 传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。实参可以是 const 类型,也可以不是。如果实参是 const 的,则 T 将被推断为 const 类型:
template <typename T> void f1(T&); // 实参必须是一个左值
// 对 f1 的调用使用实参所引用的类型 作为模板参数类型
int i;
const int ci = 42;
f1(i); // i 是一个 int;模板参数类型 T 是 int
f1(ci); // ci 是一个 const int;模板参数类型 T 是 const int
f1(42); // 错误:传递一个临时量给参数是无效的
如果一个函数参数的类型是 const T&,正常的绑定规则告诉我们 可以传递任何类型的实参给它(包括 const 或非 const 的实参)、一个临时对象 或 一个字面常量值。当函数参数本身是 const 时,T 的类型推断结果 不会是一个 const 类型。const 已经是函数参数类型的一部分
template <typename T> void f2(const T&); // 可以接受一个右值
// f2中的参数是 const & ;实参中的 const 是无关的
// 在每个调用中,f2的函数参数 都被推断为 const int&f2(i); // i 是一个 int;模板参数 T 是 int
f2(ci); // ci 是一个 const int,但模板参数 T 是 int
f2(42); // 一个const &参数可以绑定到一个右值;T是int
3、从 右值引用函数参数 推断类型:当一个函数参数是 一个右值引用类型(即,形如 T&&)时,正常绑定规则告诉我们 可以传递一个右值给它,推断出的 T 的类型是该右值实参的类型
template <typename T> void f3(T&&);
f3(42); // 实参是一个 int 类型的右值,模板参数类型是 int
4、引用折叠 和 右值引用参数:假定 i 是一个 int 对象,可能认为 像 f3(i) 这样的调用 是不合法的。i 是一个左值,而通常 不能将一个右值引用绑定到一个左值。但是,C++ 语言在正常的绑定规则以外 定义了两个例外规则(参数模板特有的)
第一个例外规则 影响右值引用参数的推断过程。将一个左值(如 i)传递给 函数的右值引用参数,且此右值引用指向 模板类型参数(如 T&&) 时,编译器推断模板类型参数为 实参的左值引用类型。调用 f3(i),编译器推断 T 的类型为 int&,而非 int
T被推断为 int& 看起来好像意味着 f3 的函数参数应该是 一个类型 int& 的右值引用
通常,不能(直接)定义 一个引用的引用。但是,通过类型别名(using,typedef typedef void (*Callback)(int); using Callback = void (*)(int); C++11还允许定义模板类型别名 template<typename T> using Vec = std::vector<T>; Vec<int> numbers = {1, 2, 3, 4, 5};
) 或 通过模板类型参数 间接定义是可以的(引用折叠只能应用于 间接创建的引用的引用)
可以使用 第二个例外绑定规则:如果 间接创建一个引用的引用,在所有情况下(除了一个例外),引用会折叠成 一个普通的左值引用类型。只有在一种特殊情况下 引用会折叠为右值引用:右值引用的右值引用时
即,对于一个给定类型 X:
- X& & 和 X& && 都折叠成类型 X&
- X&& & 和 X&& && 都折叠成 X&&
将引用折叠规则 和 右值引用的特殊类型推断规则 结合在一起,则意味着 可以对一个左值调用 f3。将一个左值传递给 f3 的(右值引用)参数时,编译器推断 T 为一个左值引用类型:
f3(i); // 实参是一个左值;模板参数 T 是 int&
f3(ci); // 实参是一个左值;模板参数 T 是一个 const int&
f3 是一个完美转发模板函数。它使用 T&& 来表示 它可以接受任何类型的参数,无论是左值还是右值
右值引用:如果函数参数是右值引用且函数调用时的实参是右值,那么模板参数T被推断为传递的类型(如int)
int x = 5;
f3(5); // 5 是右值,T 被推断为 int,val 的类型是 int&&
左值引用:如果函数参数是右值引用且函数调用时的实参是左值,那么模板参数T被推断为 左值引用,函数参数将被实例化为一个(普通)左值引用参数(T&)
x 是左值,T 被推断为 int&,val 的类型是 int&
int x = 5;
f3(x); // x 是左值,T 被推断为 int&,val 的类型是 int&
当一个模板参数 T 被推断为 引用类型时,折叠规则告诉我们 函数参数 T&& 折叠为一个左值引用类型(T&)
// 无效代码,演示用
void f3<int&>(int& &&); // 当T是int&时,函数参数为int& &&
5、编写接受右值引用参数的模板函数
template <typename T> void f3(T&& val)
{T t = val; // 拷贝还是绑定一个引用?t = fcn(t);
}
对一个右值调用 f3 时, 例如字面常量 42, T为int。 在此情况下, 局部变量 t 的类型为int, 且通过拷贝参数val的值被初始化 。 当我们对 t 赋值时, 参数val保持不变
对一个左值 i 调用 f3 时, 则 T 为int&。 定义并初始化局部变量 t 时, 赋予它类型int&。对 t 赋值时, 也同时改变了 val 的值
右值引用通常用于两种情况: 模板转发其实参或模板被重载
使用右值引用的函数模板来进行重载:
template <typename T> void f(T&&); // 绑定到非const右值
template <typename T> void f(const T&); // 左值和 "const右值"
template <typename T> void f(const T&);int main() {f(5); // 传入一个 const 右值
}
5、是一个整型字面量,是一个右值,但由于 f 的参数是 const T&,T 会被推断为 int
对下面每个调用,确定 T 和 val 的类型:
template <typename T> void g(T&& val);
int i = 0; const int ci = i;(b) g(ci);
(c) g(i * ci);
(d) g(i = ci)
(b) T: const int& val: const int& && -> const int &
T 为 const int& ,val 的类型为 const int&
(c) T: int val: int && (int * const int 结果为 int)
T 为 int ,val 的类型为 int&&
(d)注意,这里是 g(i = ci) ,而不是 g(i == ci) 。因此,实参不是一个右值,而是一个左值 —— i = ci 返回的是左值 i,最终 T 被推断为 int& ,val 经过引用折叠被确定为 int&
如果 g 的函数参数声明为 T(而不是T&&),确定T的类型。如果g的函数参数是 const T&呢
当 g 的函数参数声明为 T 时,表明参数传递是传值的;当 g 的函数参数声明为 const T& 时,正常的绑定规则告诉我们可以传递给 g 任何类型的实参 —— 一个对象(const 或非 const)、一个临时对象或是一个字面值常量
不管 g 的函数参数的声明为 T 或 const T& ,第一题的三个调用中 T 的类型都是 int 。在 g 的函数参数的声明为 T 时,三个调用中 val 的类型都是 int ;在 g 的函数参数的声明为 const T& 时,第一题的三个调用中 val 的类型都是 const int&
template <typename T> void g(T&& val) { vector<T> v; }
对 g(42) ,T 被推断为 int ,val 的类型为 int&& 。因此,v 是 int 的 vector
对 g(i) (i为int),T 被推断为 int& ,val 的类型为 int&(int& && ==> int&)。因此,v 是 int& 的 vector(引用不能作为容器的元素)
T&& 是一个万能引用(也称为转发引用),在这种情况下,传递的实参是左值 T会被推断为 int&,传递的是右值 T 会推断为 int
6、当传递参数时,C++忽略顶层const。这意味着在模板实例化和函数参数传递过程中,顶层const不会影响类型推断
模板函数:
template <typename T>
void func(T param) {// ...
}int main() {const int x = 10;func(x);
}
x 是一个 const int,它具有顶层const。
当调用 func(x) 时,T 被推断为 int,而不是 const int。顶层const被忽略
传递引用
template <typename T>
void func(const T& param) {// ...
}int main() {const int x = 10;func(x);
}
x 是一个 const int,它具有顶层const
当调用 func(x) 时,T 被推断为 int,函数 func 实例化为 void func(const int& param)
函数参数
void func(int param) {// ...
}int main() {const int x = 10;func(x);
}
x 是一个 const int,它具有顶层const。
当调用 func(x) 时,顶层const被忽略,x 作为 int 类型传递给函数
2.6 理解std::move
使用右值引用的模板的一个很好的例子
虽然不能直接将一个右值引用绑定到一个左值上,但可以用 move获得一个绑定到左值上的右值引用
1、std::move 是如何定义的
// 在返回类型和类型推换中也要用到 typename
// remove_reference
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{// static_cast return static_cast<typename remove_reference<T>::type&&>(t);
}
move 的函数参数 T&& 是一个指向模板类型参数的 右值引用。通过引用折叠,此参数可以与 任何类型的实参匹配
既可以传递给 move 一个左值,也可以传递给它一个右值
2、std::move 是如何工作的
传递给 move 的实参是 string 的构造函数的右值结果 —— string(“bye!”)。当向一个右值引用函数参数 传递一个右值时,由实参推断出的类型为 被引用的类型
函数体返回 static_cast<string&&>(t)。t 的类型已经是 string&&,于是类型转换操作什么都不做。因此,此调用的结果 就是它所接受的右值引用
传递给 move 的实参是一个左值,函数体返回 static_cast<string&&>(t)
推断出的 T 的类型为 string &
因此,remove_reference 用 string& 进行实例化。
remove_reference<string&>的 type 成员是 string。
move 的返回类型仍是 string&&。
move 的函数参数 t 实例化为 string& &&,会折叠为 string&
3、从一个左值 static_cast 到一个右值引用是允许的:虽然不能隐式地将一个左值转换为 右值引用,但 可以用 static_cast 显式地将一个左值转换为一个右值引用
将一个右值引用 绑定到 一个左值的特性 允许它们截断左值(截断指 将一个左值转换成 右值,或 从一个对象中提取部分信息)
2.7 转发
1、需要 将其一个或多个实参连同类型不 变地转发给其他函数。需要保持 被转发实参的所有性质,包括实参类型是否是 const 的 以及 实参是左值还是右值
2、一个函数,接受一个可调用对象和两个实参。我们的函数 将调用给定的可调用对象, 将两个额外参数 按逆序传递给它
// 接受一个可调用对象和另外两个参数的模板
// 将 "翻转" 的参数调用给 给定的可调用对象
// flip1 是一个不完整的实现:顶层 const 和引用丢失了
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{f(t2, t1);
}
希望 用它调用一个接受引用参数的函数时 就会出现问题:
void f(int v1, int &v2) // 注意 v2 是一个引用
{cout << v1 << " " << ++v2 << endl;
}f(42, i); // 改变了实参 i
flip1(f, j, 42); // 通过 flip1 调用 f 不会改变 j
T1 的类型是 int,因为 i 是一个 int 类型的变量。
T2 的类型是 int,因为 42 是一个 int 类型的右值常量,当模板参数推断发生时,const 修饰符被视为顶层 const,它不会影响类型推断过程。实际上,const 修饰符在传递时被忽略
问题在于 j 被传递给 flip1 的参数 t1。此参数是一个普通的、非引用的类型 int, 而非 int&
3、定义能 保持类型信息的函数参数:为了 通过翻转函数传递一个引用,需要重写函数,使其参数能保持 给定实参的“左值性”。更进一步,也希望保持参数的 const 属性
通过 将一个函数参数定义为一个指向模板类型参数的右值引用(&&),可以保持 其对应实参的所有类型信息。而使用 引用参数 (无论是左值还是右值) 使得 可以保持 const属性。因为 在引用类型中的 const 是底层的。如果我们将函数参数定义为 T1&& 和 T2&&,通过引用折叠 就可以保持精确实参的左值/右值属性
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{f(t2, t1);
}
将传递给参数 t1 一个左值 j。在 flip2 中, 推断出的 T1 的类型为 int&, 这意味着 t1 的类型会折叠为 int&。当 filp2 调用 f 时, f 中的引用参数 v2 被绑定到 t1,也是被绑定到 j。当 f 递增 v2 时, 它也同时改变了 j 的值
它对于 接受一个左值引用的函数工作(T1(int &)->j)得很好,但不能用于 接受右值引用参数的函数(T2(int)->i)
void g(int &&i, int& j)
{cout << i << " " << ++ j << endl;
}
试图通过 flip2 调用 g, 则参数 t2 将被传递给 g 的右值引用参数。即使传递一个右值给 flip2:
flip2(g, i, 42); // 错误:不能从一个左值实例化 int&&,t2是int,要传给 右值引用i,不行
4、在调用中使用 std::forward 保持类型信息:
可以使用一个名为 forward 的新标准库设施 来传递 flip2 的参数,它能保持原始实参的类型(T&& 对右值引用传入会去掉引用)。类似 move, forward 定义在头文件 utility 中。与 move 不同,forward 必须通过显式模板实参来调用。forward 返回 该显式实参类型的右值引用。即,forward<T>
的返回类型是 T&&
通常情况下,使用 forward 传递那些定义为模板类型参数的 右值引用的函数参数。通过 其返回类型上的引用折叠,forward 可以保持传递参数的左值/右值属性:
// 当用于一个指向模板参数类型的右值引用函数参数 (T&&) 时,forward 会保持实参类型的所有细节
template <typename Type> intermediary(Type &&arg)
{finalFcn(std::forward<Type>(arg));// ...
}
由于 arg 是一个模板类型参数的右值引用,Type 将表示传递给 arg 的实参的所有类型信息
如果实参是一个右值,则 Type 是一个普通 (非引用) 类型,forward<Type>
将返回 Type&&。如果实参是一个左值,则通过引用折叠,Type 本身是一个左值引用类型。在此情况下,返回类型是 一个指向左值引用类型的右值引用,将返回一个左值引用类型
重写翻转函数
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{f(std::forward<T2>(t2), std::forward<T1>(t1));
}
5、与 std::move 相同, 对 std::forward 不使用 using 声明是一个好主意
3、重载与模板
1、函数模板 可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数目或类型的参数
- 对于一个调用,其候选函数 包括所有模板实参推断成功的函数模板实例
- 候选的函数模板总是可行的,因为模板实参推断 会排除任何不可行的模板
- 与往常一样,可行函数 (模板与非模板) 按类型转换 (如果对此调用需要的话) 来排序。可以用于函数模板调用的类型转换是非常有限的
- 与往常一样,如果恰有一个函数提供比任何其他函数更好的匹配,则选择此函数。
- 但是,如果有多个函数提供同样好的匹配,则:
- 如果同样好的函数中只有一个是非模板函数,则选择非模板
- 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化(允许 为某些特定的类型 或 条件提供特殊的实现,从而 覆盖通用模板行为),则选择此模板
- 否则,此调用有歧义
2、编写重载模板:构造一组函数。将这些调试函数命名为 debug_rep, 每个函数 都返回一个给定对象的 string 表示。首先编写此函数的最通用版本,将它定义为一个模板,接受一个 const 对象的引用:
// 打印任意类型的类型
template <typename T> string debug_rep(const T &t)
{ostringstream ret; ret << t; // 使用 T 的输出运算符打印 t 的一个表示形式return ret.str(); // 返回 ret 绑定的 string 的一个副本
}
定义打印指针的 debug_rep 版本:
// 打印指针的值,后跟指针指向的对象
// 注意:此函数不能用于 char*
template <typename T> string debug_rep(T *p)
{ostringstream ret; ret << "pointer: " << p; // 打印指针本身的值if (p)ret << " " << debug_rep(*p); // 打印p指向的值,调用template <typename T> string debug_rep(const T &t)else ret << " null pointer"; // 表示出为空return ret.str(); // 返回 ret 绑定的 string 的一个副本
}
注意 此函数不能用于打印字符指针,因为 IO 库为 char* 值定义了一个 << 版本。此 << 版本假定指针表示一个空字符结尾的字符串,并打印 数组的内容而非地址值
string s("hi!");
cout << debug_rep(s) << endl;
对于这个调用,只有 第一个版本的 debug_rep 是可行的。第二个 debug_rep 版本要求一个指针参数,但在此调用中 传递的是一个非指针对象。因此编译器 无法从一个非指针实参 实例化一个期望指针类型参数的函数模板
cout << debug_rep(&s) << endl;
两个函数都生成可行的实例:
- debug_rep(const string*&),由第一个版本的 debug_rep 实例化而来,T被绑定到 string*
- debug_rep(string*),由第二个版本的 debug_rep 实例化而来,T 被绑定到 string
第二个版本的 debug_rep 的实例是 此调用的精确匹配。第一个版本的实例 需要进行普通指针到 const 指针的转换。应该选择第二个模板
3、多个可行模板
const string *sp = &s;
cout << debug_rep(sp) << endl;
两个模板都是可行的,而且两个都是精确匹配:
- debug_rep(const string*&),由第一个版本的 debug_rep 实例化而来,T 被绑定到 string*
- debug_rep(const string*),由第二个版本的 debug_rep 实例化而来,T 被绑定到 const string
正常函数匹配规则无法区分这两个函数。根据 重载函数模板的特殊规则,此调用 被解析为 debug_rep(T*),即,更特例化的版本
设计这条规则的原因是,没有它,将无法 对一个 const 的指针 调用指针版本的debug_rep。问题在于模板 debug_rep(const T&) 本质上 可以用于任何类型,包括非指针类型
此模板比 debug_rep(T*) 更通用,后者只能用于 指针类型。没有这条规则,传递 const 的指针的调用(const string*) 永远是有歧义的
4、非模板和模板重载
// 打印引号包围的 string
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_rep(const string&),普通非模板函数
两个函数具有相同的参数列表,因此显然两者提供同样好的匹配。但是,编译器会选择非模板版本
对于一个调用,如果一个非函数模板与一个函数模板提供 同样好 的匹配,则选择非模板版本
5、重载模板和类型转换
C 风格字符串指针 和 字符串字面常量:
cout << debug_rep("hi world!") << endl; // 调用 debug_rep(T*)
所有三个 debug_rep 版本都是可行的:
- debug_rep(const T&),T 被绑定到 char[10]
- debug_rep(T*),T 被绑定到 const char
- debug_rep(const string&),要求从 const char* 到 string 的类型转换
两个模板 都提供精确匹配——第二个模板需要进行一次 (允许的) 数组到指针的转换,而对于 非模板版本来说,这种转换认为是精确匹配 。非模板版本是可行的,但需要 进行一次用户定义的类型转换,因此它没有精确匹配那么好,所以两个模板成为可能调用的函数
与之前一样,T* 版本更具特例化,编译器会选择它
如果 希望将字符串指针转 string 处理,可以定义另外两个非模板重载版本:
// 将字符串转为 string, 并调用 string 版本的 debug_rep
string debug_rep(char *p)
{return debug_rep(string(p));
}string debug_rep(const char *p)
{return debug_rep(string(p));
}
6、函数调用时的精确匹配
精确匹配发生在以下几种情况之一:
1)完全匹配:实参类型与形参类型完全一致
2)常量转换:实参可以转换为形参,例如将非const对象传递给const引用(一定要是引用,赋值不行)
3)数组到指针转换:例如,将字符数组传递给接受const char*的函数
4)函数到指针转换:例如,将函数名称传递给接受函数指针的函数
完全匹配优先于任何转换
常量转换和数组到指针转换等简单转换在需要时会被考虑
用户定义转换(如从const char*到string)通常会被视为最后的选择
7、缺少声明可能导致程序行为异常:通常,如果使用了一个忘记声明的函数,代码将编译失败。但对于重载函数模板的函数而言,则不是这样
template <typename T> string debug_rep(const T &t);
template <typename T> string debug_rep(T *p);
// 为了使 debug_rep(char*) 的定义正常工作,下面的声明必须在作用域中
string debug_rep(const string &);
string debug_rep(char *p)
{// 如果接受一个 const string& 版本的声明不在作用域中,// 返回语句将调用 debug_rep(const T&) 的 T 实例化为 string 的版本return debug_rep(string(p));
}
编写 自己版本的 debug_rep 函数
#include <iostream>
#include <memory>
#include <sstream>template <typename T> std::string debug_rep(const T& t);
template <typename T> std::string debug_rep(T* p);
std::string debug_rep(const std::string& s);
std::string debug_rep(char* p);
std::string debug_rep(const char* p);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);elseret << " null pointer";return ret.str();
}std::string debug_rep(const std::string& s) {return '"' + s + '"';
}std::string debug_rep(char* p)
{std::cout << "debug_rep(char* p)" << std::endl;return debug_rep(std::string(p));
}std::string debug_rep(const char* p)
{std::cout << "debug_rep(const char *p)" << std::endl;return debug_rep(std::string(p));
}int main()
{char a[] = { 'a', '\0' };std::cout << debug_rep(a) << std::endl; // debug_rep(char* p) -> debug_rep(const std::string& s)const char ca[] = { 'a', 'b', 'c', '\0' };std::cout << debug_rep(ca) << std::endl;// debug_rep(const char* p) -> debug_rep(const std::string& s)std::string str = "abcdef";std::cout << debug_rep(str) << std::endl;// std::string debug_rep(const std::string& s)int b[] = { 1, 2 };std::cout << debug_rep(b) << std::endl; // std::string debug_rep(T* p) -> std::string debug_rep(const T& t)return 0;
}
运行结果
template <typename T> void f(T);
template <typename T> void f(const T*);
int i = 42, *p = &i;
f(p); // 实例化模板template <typename T> void f(T);
因为 f(T) 可以直接匹配 int* 类型,而 f(const T*) 需要类型转换
不允许同时定义 f(T) 和 f(const T),因为 模板参数类型推导无法区分 const 和 非const版本
4、可变参数模板
1、一个可变参数模板 就是一个接受可变数目参数的模板函数 或 模板类。可变数目的参数称为参数包 。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数
用一个省略号 来指出一个模板参数 或 函数参数表示一个包。模板参数列表中,class…或 typename…指出接下来的参数 表示零个或多个类型的列表;一个类型名 后面跟一个省略号表示 零个或多个指定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型 是一个模板参数包,则此参数 也是一个函数参数包(这两个参数包就是一个数量 / 类型未定的参数 / 类型列表,就是一堆用逗号分隔的参数的省略)
// Args 是一个模板参数包;rest 是一个函数参数包
// Args 表示零个或多个模板类型参数
// rest 表示零个或多个函数参数
template <typename T, typename... Args> // 省略号加在类型及引用号后面,名字前面
void foo(const T &t, const Args&... rest);
声明了 foo 是一个可变参数模板。它有一个名为 T 的类型参数,和一个名为 Args的模板参数包。这个包表示零个或多个额外的类型参数
与往常一样,编译器从函数调用的实参 推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目,也可以是空包
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"); // 空包
编译器会为 foo 实例化出四个不同的版本:
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
2、sizeof…运算符:需要知道包中有多少元素时,可以使用 sizeof…运算符。类似 sizeof,sizeof…也返回一个常量表达式
template<typename... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // 是类型参数的数目,参数是包的名字
cout << sizeof...(args) << endl; // 函数参数的数目
}
4.1 编写可变参数函数模板
1、可以使用一个 initializer_list 来定义 一个可接受可变数目实参的函数。但是,所有实参必须具有相同的类型 (或它们的类型 可以转换为同一个公共类型)
当我们 既不知道想要处理的实参的数目 也不知道它们的类型时,可变参数模板 是很有用的
可变参数函数 通常是递归的。第一步调用处理包中的 第一个实参,然后剩余实参 递归调用自身
print 函数也是这样的模式,每次递归调用将第一个实参打到第一个实参表示的流中。为了终止递归,还需要定义一个非可变参数的 print 函数,它接受一个流和一个对象
// 用来终止递归并打印最后一个元素的函数
// 此函数必须在可变参数版本的 print 定义之前声明(必须在作用域中,不然无限递归)
template<typename T>
ostream &print(ostream &os, const T &t)
{return os << t; // 包中最后一个元素之后不打印分隔符
}// 包中除了最后一个元素之外的其他元素 都会调用这个版本的 print
template<typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{os << t << ", "; // 打印第一个实参// 关键部分是可变参数函数中对 print 的调用return print(os, rest...); // 递归调用,打印其他实参
}
可变参数版本的 print 函数接受三个参数,一个 ostream&, 一个 const T& 和 一个参数包。而此调用 只传递了两个实参。结果就是 rest 中的第一个实参 被绑定到 t,剩余参数形成下一个 print 调用的参数包。因此,在每个调用中,包中的第一个实参被移除,成为绑定到 t 的实参
print(cout, i, s, 42); // 包中有两个参数
递归会执行如下:
对于最后一个调用,两个函数提供同样好的匹配。但是,非可变参数版本 比 可变参数模板 更特例化。因此编译器选择非可变参数版本
编写自己的版本的 print 函数
#include <iostream>using namespace std;template <typename T>
void print(ostream& os, const T& t) {os << t << endl;
}template <typename T, typename ... Args>
void print(ostream& os, const T& t, const Args &... rest) {os << t << " ";print(os, rest...); // 调用的时候也需要在名字后加...,rest... 是一个参数包,表示零个或多个额外参数// rest... 是展开参数包的语法。它告诉编译器要将 rest 参数包中的每个参数都独立传递给 print 函数// 这是递归调用的一部分,在每次调用中,print 函数会处理一个参数并继续处理剩余的参数,直到没有参数为止// 如果没有 ...,即写成 print(os, rest);,编译器会将整个参数包 rest 作为一个单独的参数传递(16.52),而不是展开每一个独立的参数// 这会导致错误,因为 print 函数期望的是一系列独立的参数,而不是一个参数包// 16.52//template <typename T, typename ... Args>//void foo(const T& t, const Args &... rest) {// std::cout << sizeof...(Args) << " ";// std::cout << sizeof...(rest) << std::endl; // 传入整个参数包
//}
}int main()
{print(cout, 1, "ashergu", 'a', "engineer");return 0;
}
运行结果
可变参数版本 print 的定义之后声明非可变参数版本,解释可变参数的版本会如何执行:编译报错:error: no matching function for call to ‘print(std::ostream&)’
4.2 包扩展
1、对于 一个参数包,除了获取其大小外,能对它做的唯一的事情就是 扩展 它。当扩展一个包时,还要 提供用于每个扩展元素的模式。扩展一个包就是 将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式末位放一个省略号 (…) 来 触发扩展操作
template <typename T, typename... Args>
ostream &
print(ostream &os, const T &t, const Args&... rest) // 扩展 Args
{os << t << ", "; // 打印第一个实参return print(os, rest...); // 扩展 rest
}
第一个扩展操作 扩展模板参数包,为 print 生成函数参数列表。第二个扩展操作出现在 对 print 的调用中。此模式为 print 调用生成实参列表
对 Args 的扩展中,编译器将模式 const Args& 用到模板参数包 Args 中的每个元素。因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形成 const type&
print(cout, i, s, 42); // 包中有两个参数
最后两个参数的类型和模式 一起确定了尾置参数的类型。此调用被实例化为:
ostream&
print(ostream&, const int&, const string&, const int&);
第二个扩展 发生在对 print 的 (递归) 调用中。在此情况下,模式是函数参数包的名字 (即 rest)。此模式扩展出一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于:
print(os, s, 42);
2、理解包扩展:print 中的函数参数包扩展 仅仅将包扩展为其构成元素。C++ 语言还允许更复杂的扩展模式。例如,可以编写一个可变参数函数,对其每个实参调用 debug_rep,然后调用 print 打印结果 string:(注意执行先后顺序)
// 在 print 调用中对每个实参调用 debug_rep
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_rep(rest)。此模式表示 我们希望对函数参数包 rest 中的每个元素调用 debug_rep。扩展结果将是一个逗号分隔的 debug_rep 调用列表
与之相对,下面的模式会编译失败:
// 将包传递给 debug_rep: print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // 错误:此调用无匹配函数
编写并测试可变参数版本的 errorMsg
#include <iostream>
#include <string>
#include <sstream>using namespace std;template <typename T> string debug_rep(const T& t);
template <typename T> string debug_rep(const T* p);
string debug_rep(const string& str);
string debug_rep(const char* p);
string debug_rep(char* p);
// 如果只有一个模板函数 template <typename T> string debug_rep(const T* p);,并且没有特定的重载版本来处理 const char* 和 char*
// 那么在传递这些类型的参数时,编译器确实能够推导出模板参数并正确实例化模板函数。但这样会失去为这些特殊类型提供特定行为的机会template <typename T>
string debug_rep(const T& t) {ostringstream oss;oss << t; // 直接转字符串相当于return oss.str();
}template <typename T>
string debug_rep(const T* p) {ostringstream oss;oss << "pointer: " << p << " ";if (p)oss << (*p);// oss << debug_rep(*p); // 可以复用函数elseoss << "null pointer";return oss.str();
}string debug_rep(const string& str) {return '"' + str + '"';
}string debug_rep(const char* p) {// cout << "debug_rep(const char* p) " << string(p) << endl;return debug_rep(string(p)); // 将一个 const char* 类型的 C 字符串转换为一个 std::string 类型的对象// 构造函数的目的是初始化一个对象,而不是返回一个对象的指针。构造函数不返回任何值,它只是用来初始化对象// std::string str("Hello, World");// new string(p) 会返回一个指向 std::string 对象的指针,这个对象是在堆上创建的
}string debug_rep(char* p) {// cout << "debug_rep(char* p) " << *p << endl;return debug_rep(string(p));
}// 参数包print函数
template <typename T>
ostream& print(ostream &os, const T& t) { // 别忘了写返回值return os << t << endl;
}template <typename T, typename ... Args>
ostream& print(ostream& os, const T& t, const Args& ...rest) {os << t << " ";return print(os, rest...); // 把rest拆开成列表
}// 参数包扩展到多个函数调用
template <typename ... Args>
ostream& errorMsg(ostream& os, const Args& ...rest) {return print(os, debug_rep(rest)...); // 把列表中的rest都转成string的列表传入print// 先把所有列表中的元素都调用debug_rep,再一起放入print函数中
}int main()
{char c[] = {'e', 'n', 'g', 'i', 'n', 'e', 'e', 'r', '\0'}; // c自动转化为 char *,字符数组必须以 null 结尾,不然会打印出跟在后面的乱码// 声明了一个大小为3的字符数组,并用'a', 'b', 和 'c'进行初始化,不需要是const char,但是调用的是debug_rep(const char* p) // 编译器需要解析哪个debug_rep函数适用于参数c。C++的函数重载解析规则会根据参数类型选择最合适的函数// 因为字符串字面量和字符数组在某些情况下会被转换为const char* ,并且C++的函数重载解析规则在这种情况下倾向于选择const char* 重载double d = 1.1;double* dp = &d; // 要指向const才会调用string debug_rep(const T* p),不然调用 string debug_rep(const T& t),这不是引用,引用的话不允许同时有 const T& 和 T& 的存在// dp 是一个 double* 类型的指针,不是 const 指针,因此会调用 string debug_rep(const T& t) 版本,其中 T 被推断为 double*。因为指针类型也是普通类型,所以会匹配到 const T& 版本const double * dp2 = &d;char ch = 'a';// char* chp = &ch; // 作为参数放到errorMsg,会调用debug_rep(char* p),但是会乱码,最后还是转成string()errorMsg(cout, 1, "ashergu", 'a', c, dp, dp2);// cout << "errorMsg: " << errorMsg(cout, 1, "ashergu", 'a', c, dp); 报错// cout 期望插入一个字符串或者另一个类型,它可以将其插入到流中。然而,由于 errorMsg 返回的是 ostream&,在这里将流插入到另一个流中是不合法的,这导致了类型不匹配的错误cout << "errorMsg: "; // 拆开来写errorMsg(cout, 1, "ashergu", 'a', c, dp, dp2) << "finish" << endl; // 这个是可以的return 0;
}
ostringstream:使用插入运算符(<<)将 各种类型 的数据插入到字符串流中
int i = 123;
double d = 4.56;
std::string s = "example";oss << i << " " << d << " " << s;
整数 i,浮点数 d 和字符串 s 被插入到字符串流中,并且被格式化为一个连续的字符串。适用于 转换数据类型 和 动态生成字符串
4.3 转发参数包
1、在新标准下,可以组合使用可变参数模板 与 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&&...);// 其他成员的定义同13.5
};
当 emplace_back 将这些实参 传递给 construct 时,必须使用 forward 来保持实参的原始类型
template <class... Args>
inline
void StrVec::emplace_back(Args&&... args) {chk_n_alloc(); // 如果需要的话重新分配 StrVec 内存空间alloc.construct(first_free++, std::forward<Args>(args)...);
}
construct 调用中的扩展为
std::forward<Args>(args)...
既 扩展了 模板参数包 Args,也扩展了 函数参数包 args。此模式生成如下形式的元素
std::forward<T_i>(t_i) // T_i 表示模板参数包中的第i个元素的类型,t_i 表示函数参数包中的第i个元素
svec.emplace_back(10, 'c'); // 将 10 和 c 添加为新的元素
construct 调用中的模式扩展出
std::forward<int>(10), std::forward<char>(c)
在调用中使用 forward,保证 如果用一个右值调用 emplace_back,则 construct 也会得到一个右值
std::forward<string>(string("the end"))
传递给 emplace_back 的实参是一个右值,forward<string>
的结果类型是 string&&, 因此 construct 将得到一个右值引用实参。construct 会继续将此实参传递给 string 的移动构造函数以创建新元素
2、可变参数函数 通常将它们的参数转发给其他函数。这种函数通常具有 与我们在emplace_back 函数一样的形式:
// fun 接受一堆参数,每个参数都是一个模板参数类型的右值引用
template<typename... Args>
void fun(Args&&... args) // 将 Args 扩展为一个右值引用的列表
{// work 的参数既扩展 Args 又扩展 argswork(std::forward<Args>(args)...);
}
希望将 fun 的所有实参 转发给另一个名为 work 的函数,假定它完成该函数的实际工作。类似 emplace_back 中对 construct 的调用, work 调用中的扩展 既扩展了模板参数包也扩展了函数参数包
// 泛型包装函数,使用std::forward实现完美转发
template <typename T>
void wrapper(T&& t) {func(std::forward<T>(t));
}
std::forward<string>
的结果类型是 string&,std::forward<string&>
的结果类型是 string&&
2、定义 自己版本的 make_shared
#include <memory>
#include <iostream>namespace ch16{template <typename T, typename... Args>std::shared_ptr<T> make_shared(Args&&... args){return std::shared_ptr<T>(new T(std::forward<Args>(args)...));}
}int main()
{auto sp1 = ch16::make_shared<int>(1);std::cout << *sp1 << std::endl;auto str = ch16::make_shared<std::string>(10, 'c');std::cout << *str << std::endl;return 0;
}
5、模板特例化
1、编写单一模板,使之 对任何可能的模板实参都是最适合的,都能实例化,这并不总是能办到。在某些情况下,通用模板的定义 对特定类型是不适合的;通用定义可能编译失败 或 做得不正确。其他时候,也可以利用某些特定知识 来编写更高效的代码,而不是 从通用模板实例化的代码。当 不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本
compare 函数展示了 函数模板的通用定义 不适合一个特定类型(即字符指针)的情况。希望 compare 通过调用 strcmp 比较两个字符串值 而非比较指针值。重载了 compare 函数来处理字符字符串常量
// 第一个版本,可以比较任意两个类型
template <typename T> int compare(const T&, const T&);// 第二个版本处理字符串字面常量
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); // 调用第一个模板,无法将一个指针转换为一个数组的引用
compare("hi", "mom"); // 调用有两个非类型模板参数的版本
为了处理字符串(而不是数组),可以为第一个版本的 compare 定义一个 模板特例化版本。一个特例化版本就是 模板的一个独立的定义,在其中一个或多个模板参数 被指定为特定的类型
2、定义函数模板特例化:特例化一个函数模板时,必须为原模板中的 每个 模板参数都提供实参。为了指出我们正在实例化一个模板,应该用关键字 template 后跟一个空尖括号对 (<>)。空尖括号指出 我们将为原模板的所有模板参数 提供实参:
// compare 的特殊版本,处理字符串数组的指针
template <>
int compare(const char* const &p1, const char* const &p2) {return strcmp(p1, p2);
}
定义一个特例化版本时,函数参数类型 必须与一个先前声明的模板的对应的类型匹配。本例中我们特例化:
template <typename T> int compare(const T&, const T&);
其中 T 为 const char*。函数要求 一个指向此类型 const 版本的引用(顶层const直接忽略)
3、函数重载与模板特例化:当定义 函数模板的特例化版本时,本质上 接管了编译器的工作。即,为原模板的一个特例实例 提供了定义。一个特例化版本 本质上是一个实例,而非函数名的一个重载版本。因此,特例化不影响函数匹配
当某个模版是最佳匹配时,且需要实例为这个特殊实例时,不再从原模版进行实例化,而是直接使用这个特殊化版本
将一个特殊的函数定义为一个特例化版本 还是一个独立的非模板函数,会影响到函数匹配。例如,已经定义了两个版本的 compare 函数模板。一个接受数组引用参数,另一个接受 const T&。还定义了 一个特例化版本来处理字符指针,这对函数匹配没有影响
当我们对字符串字面常量调用 compare 时:
compare("hi", "mom")
两个函数模板都是可行的,且提供同样好的(即精确的)匹配。但是,接受字符数组参数的版本更特例化template<size_t N, size_t M> int compare(const char (&)[N], const char (&)[M]);
,因此编译器会选择它
如果 将接受字符串指针的 compare 版本定义为一个普通的非模板函数(而不是 模板版本的一个特例化版本),此调用的解析就会不同。在此情况下,将会有三个可行的函数:两个模板和非模板的字符指针版本。所有三个函数模板提供同样好的匹配。如前所述,当一个非模板函数提供与模板函数同样好的匹配时,编译器会选择非模板版本
4、为了特例化一个模板,原模板的声明必须在作用域中。而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中
如果某个源文件失去了一个特例化版本的声明,编译器通常可以用原模板生成代码,很容易产生模板及其特例化版本声明顺序导致的错误
模板及其特例化版本 应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本
5、类模板特例化:除了 特例化函数模板,还可以 特例化类模板。将为标准库 hash 模板 定义一个特例化版本,可以用它来将 Sales_data 对象保存在无序容器中。默认情况下,无序容器使用 hash<key_type>
来组织其元素。为了让自己的数据类型 也能使用这种默认组织方式,必须定义 hash 模板的一个特例化版本
必须在原模板定义所在的命名空间中特例化它。为了达到这一目的,首先必须打开命名空间:
// 打开 std 名空间,仅使特例化 std::hash
namespace std {
} // 关闭 std 命名空间,注意:右花括号之后没有分号
花括号内的任何定义 都将成为命名空间 std 的一部分
定义了一个能处理 Sales_data 的特例化 hash 版本:
// 打开 std 命名空间,以便特例化 std::hash
namespace std {template <> // 定义一个全特例化版本,模板参数为 Sales_datastruct hash<Sales_data>{// 用来散列一个无序容器的类型 必须定义以下列类型typedef size_t result_type;typedef Sales_data argument_type; // 默认情况下,此类型需要==size_t operator()(const Sales_data& s) const;// 使用合成的拷贝控制成员和默认构造函数};size_thash<Sales_data>::operator()(const Sales_data& s) const{return hash<string>()(s.bookNo()) ^hash<unsigned>()(s.units_sold) ^hash<double>()(s.revenue);}
} // 关闭 std 命名空间,注意:右花括号之后没有分号
组合多个哈希值:当需要组合多个字段的哈希值时,异或可以有效地将这些值混合在一起,减少哈希冲突的概率。例如,在自定义类型 Sales_data 中,有三个成员变量:bookNo(字符串),units_sold(无符号整数),revenue(双精度浮点数)。对每个字段分别计算哈希值,并将它们异或在一起,得到一个唯一的哈希值
降低冲突概率:异或运算能将不同字段的哈希值混合,产生一个相对均匀分布的哈希值,从而降低哈希冲突的概率
类似其他任何类,可以在类内 或 类外 定义特例化版本的成员,重载的调用运算符 必须为给定类型的值 定义一个哈希函数。对于一个确定值,任何时候调用此函数都应该返回相同的结果。一个好的哈希函数对于不相符的对象几乎总是应该产生不同的结果
使用一个(未命名的)hash<string>
对象来生成 bookNo 的哈希值,用一个 hash<unsigned>
对象来生成 units_sold 的哈希值,用一个 hash<double>
对象来生成 revenue 的哈希值。将这些结果进行异或运算,形成指定 Sales_data 对象的完整的哈希值
hash 函数计算 所有三个数据成员的哈希值,从而与我们为 Sales_data 定义的 operator==是兼容的。默认情况下,为了处理特定关键字类型,无序容器 会组合使用 key_type 对应的特例化 hash 版本和 key_type 上的相等运算符
特例化版本 在作用域中,当将 Sales_data 作为容器的关键字类型时,编译器 就会自动使用此特例化版本, hash<Sales_data>
使用 Sales_data 的私有成员,我们必须将它声明为Sales_data 的友元:
template <class T> class std::hash; // 友元声明所需要的
class Sales_data {friend class std::hash<Sales_data>;// 其他成员定义,如前
};
由于此实例定义在 std 命名空间中,必须记得 friend 声明中应使用 std::hash
6、类模板部分特例化:与函数模板不同,类模板的特例化 不必为所有模板参数提供实参。可以只指定一部分而非所有模板参数,或是 参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时 用户还必须为那些在特例化版本中未指定的模板参数提供实参
只能部分特例化类模板,而不能部分特例化函数模板
完全特例化 是指为特定的类型提供一个完整的特化版本,即为模板提供具体的类型参数。例如:
template <> struct remove_reference<int> {typedef int type;
};
这段代码完全特例化了 remove_reference 模板,使其针对 int 类型工作
部分特例化 是指为一部分类型模式提供特化版本,即模板参数仍然包含类型参数,但特化版本只处理这些参数的特定模式。例如:
template <class T> struct remove_reference<T&> {typedef T type;
};template <class T> struct remove_reference<T&&> {typedef T type;
};
这段代码部分特例化了 remove_reference 模板,使其分别处理左值引用和右值引用类型
标准库 remove_reference 类型。该模板是 通过一系列的特例化版本 来完成其功能的:
// 原始的,最通用的版本
template <class T> struct remove_reference {typedef T type;
};// 部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> {typedef T type;
};template <class T> struct remove_reference<T&&> {typedef T type;
};
先定义模板。类似任何其他特例化版本,部分特例化版本的名字 与原模板的名字相同。对每个未完全确定类型的模板参数,在实例化版本的模板参数列表中都有一项与之对应。在类名之后, 为要特例化的模板参数 指定实参, 这些实参 列于模板名之后的尖括号中。这些实参与原始模板中的参数 按位置对应
部分特例化版本的模板参数列表 是原始模板的参数列表的一个子集或者是一个特例化版本
在本例中, 特例化版本的模板参数的数目 与 原始模板相同,但是类型不同。两个特例化版本分别用于左值引用和右值引用类型
int i;
// decltype(42) 为int,使用原始模板
remove_reference<decltype(42)>::type a;
// decltype(i) 为 int&, 使用第一个 (T&) 移分特例化版本
remove_reference<decltype(i)>::type b;// decltype(std::move(i)) 为 int&&, 使用第二个 (即 T&&) 部分特例化版本
remove_reference<decltype(std::move(i))>::type c;
7、特例化成员而不是类:
可以 只特例化特定成员函数 而不是特例化整个模板。例如,如果 Foo 是一个模板类,包含一个成员 Bar,可以只特例化该成员:
template <typename T> struct Foo {Foo(const T& t = T()) : mem(t) { }void Bar() { /* ... */ }T mem;// Foo 的其他成员
};template<> // 我们正去特例化一个模板
void Foo<int>::Bar() // 我们正在特例化 Foo<int> 的成员 Bar
{// 进行应用于 int 的特例化处理
}
只特例化 Foo<int>
类的一个成员,其他成员将由 Foo 模板提供,用 int 使用 Foo 时,Bar 之外的成员像往常一样进行实例化。如果我们用 Foo<int>
的成员 Bar,则会使用我们定义的特例化版本
8、定义 自己版本的 hash<Sales_data>
, 并定义一个 Sales_data 对象的 unorder_multiset。将多条交易记录保存到容器中,并打印其内容
Sales_data_62.h
#ifndef SALES_DATA_H
#define SALES_DATA_H#include <string>
#include <iostream>
#include <cstddef> // using std::size_t;template <class T> class std::hash; // 友元声明所需要的,后面的std::unordered_multiset<Sales_data>会自己调用这个库函数class Sales_data {friend std::ostream& operator<<(std::ostream&, const Sales_data&);friend std::istream& operator>>(std::istream&, Sales_data&);friend bool operator==(const Sales_data&, const Sales_data&);friend std::ostream& print(std::ostream&, const Sales_data&);friend std::istream& read(std::istream&, Sales_data&);friend class std::hash<Sales_data>; // 声明友元public:// constructorsSales_data() : units_sold(0), revenue(0.0) { }Sales_data(const std::string& s) :bookNo(s), units_sold(0), revenue(0.0) { }Sales_data(const std::string& s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p* n) { }Sales_data(std::istream&);std::string isbn() const { return bookNo; }Sales_data& operator+=(const Sales_data&);
private:double avg_price() const;std::string bookNo;unsigned units_sold;double revenue;
};inline
bool compareIsbn(const Sales_data& lhs, const Sales_data& rhs)
{return lhs.isbn() < rhs.isbn();
}inline
bool operator==(const Sales_data& lhs, const Sales_data& rhs)
{return lhs.isbn() == rhs.isbn() &&lhs.units_sold == rhs.units_sold &&lhs.revenue == rhs.revenue;
}
inline
bool operator!=(const Sales_data& lhs, const Sales_data& rhs)
{return !(lhs == rhs);
}Sales_data add(const Sales_data&, const Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);Sales_data operator+(const Sales_data&, const Sales_data&);
std::ostream& operator<<(std::ostream&, const Sales_data&);
std::istream& operator>>(std::istream&, Sales_data&);std::istream& operator>>(std::istream& is, Sales_data& item)
{double price = 0;is >> item.bookNo >> item.units_sold >> price;if (is)item.revenue = price * item.units_sold;elseitem = Sales_data();return is;
}std::ostream& operator<<(std::ostream& os, const Sales_data& item)
{os << item.isbn() << " " << item.units_sold << " " << item.revenue;return os;
}namespace std {template <> struct std::hash<Sales_data> {
// 用来散列一个无序容器的类型必须要定义下列类型
// 当你为自定义类型实现哈希函数以便在无序容器(如 std::unordered_map 或 std::unordered_set)中使用时,按照 C++ 标准库的要求,哈希函数对象必须满足某些类型和接口要求
// 这些要求包括定义两个类型别名:result_type 和 argument_type。这是为了确保哈希函数对象符合标准库的哈希函数对象的概念// 这个类型通常是 std::size_t,表示哈希值。result_type 使得容器和算法能够知道哈希函数返回值的类型// argument_type 使得容器和算法能够知道哈希函数期望的输入类型,即要散列的类型,例如 Sales_datatypedef std::size_t result_type;typedef Sales_data argument_type; std::size_t operator()(const Sales_data& s) const {// std::hash<Sales_data>::operator() 方法没有被声明为 const,导致无法在 const 环境中调用return std::hash<std::string>()(s.bookNo) ^std::hash<unsigned>()(s.units_sold) ^std::hash<double>()(s.revenue);
// 为了将多个成员变量的哈希值组合成一个单一的哈希值
// 组合多个哈希值:当需要组合多个字段的哈希值时,异或可以有效地将这些值混合在一起,减少哈希冲突的概率
// 例如,在自定义类型 Sales_data 中,有三个成员变量:bookNo(字符串),units_sold(无符号整数),revenue(双精度浮点数)
// 对每个字段分别计算哈希值,并将它们异或在一起,得到一个唯一的哈希值
// 降低冲突概率:异或运算能将不同字段的哈希值混合,产生一个相对均匀分布的哈希值,从而降低哈希冲突的概率}// 类使用合成的拷贝控制成员和默认构造函数};
}
// 关闭 std 命名空间;注意:右花括号之后没有分号
#endif
16.62.cpp
#include "Sales_data_62.h"
#include <unordered_set>int main()
{std::unordered_multiset<Sales_data> set;Sales_data sd;while (std::cin >> sd) {set.insert(sd);}std::cout << set.size() << std::endl;for (auto sd : set)std::cout << sd << std::endl;return 0;
}
运行结果
978-7-121-15535-2 4 99.99
978-7-121-15535-0 5 100
978-7-121-15535-1 2 99.5
^z
3
978-7-121-15535-0 5 500 100
978-7-121-15535-2 4 399.96 99.99
978-7-121-15535-1 2 199 99.5
9、定义一个函数模板,统计一个给定值在一个 vector 中出现的次数。测试你的函数,分别传递给它一个 double 的 vector、一个 int 的 vector 以及一个 string 的 vector,编写特例化版本来处理vector<const char*>。编写程序使用这个特例化版本
#include <vector>
#include <string>
#include <iostream>using namespace std;template <typename T>
size_t count(vector<T>& vec, T val) {size_t cnt = 0;for (T& v : vec) {if (v == val)cnt++;}return cnt;
}template <>
size_t count(vector<const char*>& vec_cc, const char* tar) { // 定义模板特化时,确保它们匹配模板的声明,不能随便加const,只有对应T的地方可以加const,而且全部T的位置都要加size_t cnt = 0;for (const char* v : vec_cc) {if (strcmp(v, tar) == 0) // 比指针所指的对象而不是指针本身,注意相等为0cnt++;}return cnt;
}int main()
{vector<int> vec1 = { 1, 2, 3, 2, 2, 5 };vector<string> vec2 = { "123", "234", "234" };cout << count(vec1, 2) << endl;cout << count(vec2, string("234")) << endl;std::vector<const char*> vec3 = { "123", "234", "234" }; // 字符串常量可以直接初始化const char*cout << count(vec3, "234") << endl;return 0;
}
运行结果