《C++ Primer》第16章 模板与泛型编程
16.1节定义模板 习题答案
练习16.17:声明为typename的类型参数和声明为class的类型参数有什么不同(如果有的话)?什么时候必须使用typename?
【出题思路】
理解typename和class。
【解答】
当用来声明模板类型参数时,typename和class是完全等价的,都表明模板参数是一个类型。在C++最初引入模板时,是使用class的。但为了避免与类(或类模板)定义中的class相混淆,引入了typename关键字。从字面上看,typename还暗示了模板类型参数不必是一个类类型。因此,现在更建议使用typename。
如本节所述,typename还有其他用途,当在模板类型参数上使用作用域运算符::来访问其成员时,如T::value_type,在实例化之前可能无法辨别访问的到底是静态成员还是类型成员。对此,C++默认通过::访问的是静态成员。为了指明访问的是类型成员,需要在名字前使用typename关键字,如typename T::value_type(),表明value_type是类型成员,这里是创建一个value_type类型的对象,并进行值初始化。
练习16.18:解释下面每个函数模板声明并指出它们是否非法。更正你发现的每个错误。
(a) template <typename T, U, typename V> void f1(T, U, V);
(b) template <typename T> T f2(int &T);
(c) inline template <typename T> T foo(T, unsigned int *);
(d) template <typename T> f4(T, T);
(e) typedef char Ctype;
template <typename Ctype> Ctype f5(Ctype a);
【出题思路】
理解模板参数的作用域等特性。
【解答】
(a)非法。必须指出U是类型参数(用typename)还是非类型参数。
(b)非法。在作用域中,模板参数名不能重用,而这里重用T作为函数参数名。
(c)非法。在模板定义时才能指定inline。
(d)非法。未指定函数模板返回类型。
(e)合法。在模板作用域中,类型参数Ctype屏蔽了之前定义的类型别名Ctype。
练习16.19:编写函数,接受一个容器的引用,打印容器中的元素。使用容器的size_type和size成员来控制打印元素的循环。
【出题思路】
练习用typename指明类型成员。
【解答】
我们设定循环变量的类型为容器类型(模板参数)的size_type,用容器对象(函数参数)的size控制循环的终止条件。在循环体中用at来获取容器元素,进行打印。由于size_type是容器的类型成员而非静态数据成员,因此在前面加上typename特别指出。
#include <iostream>
#include <vector>
#include <string>
using std::endl;
using std::cout;
using std::vector;
using std::string;
template <typename C>
void print(const C &c)
{
for(typename C::size_type i = 0; i < c.size(); ++i)
cout << c.at(i) << " ";
}
int main()
{
vector<int> vec = {10,20,30,40,50,60};
print(vec);
cout << endl;
vector<string> str = {"camle", "scott"};
print(str);
cout << endl;
string s1 = "smart";
print(s1);
cout << endl;
return 0;
}
运行结果:
练习16.20:重写上一题的函数,使用begin和end返回的迭代器来控制循环。
【出题思路】
练习定义函数模板,复习用迭代器遍历容器。
【解答】
比上一题更为简单,用begin获取容器首位置迭代器,将判定迭代器是否到尾后迭代器(end)作为循环判定条件,在循环中解引用迭代器获得元素值。显然,这种方法的适用范围比上一题的方法更宽,可用于list和forward_list。
#include <iostream>
#include <vector>
#include <string>
#include <list>
using std::endl;
using std::cout;
using std::vector;
using std::string;
using std::list;
template <typename C>
void print(const C &c)
{
for(auto it = c.begin(); it != c.end(); ++it)
cout << *it << " ";
}
int main()
{
vector<int> vec = {10,20,30,40,50,60};
print(vec);
cout << endl;
vector<string> str = {"camle", "scott"};
print(str);
cout << endl;
string s1 = "smart";
print(s1);
cout << endl;
list<string> listColor = {"red", "blue", "green", "gray", "white"};
print(listColor);
cout << endl;
return 0;
}
运行结果:
练习16.21:编写你自己的DebugDelete版本。
【出题思路】
本题练习定义成员模板。
【解答】
#include <iostream>
#include <string>
using std::cout;
using std::string;
using std::endl;
//函数对象类,对给定指针执行delete
class DebugDelete
{
public:
DebugDelete(std::ostream &s = std::cerr):
os(s)
{ }
//与任何函数模板相同,T的类型由编译器推断
template<typename T> void operator()(T *p) const
{
os << "deleting *p = " << *p << std::endl;
delete p;
p = nullptr;
}
private:
std::ostream &os;
};
int main()
{
double *pd = new double(56.5);
DebugDelete d;//可像delete表达式一样使用的对象
d(pd);
int *pi = new int(98);
DebugDelete d2;
d2(pi);
return 0;
}
运行结果:
练习16.22:修改12.3节(第430页)中你的TextQuery程序,令shared_ptr成员使用DebugDelete作为它们的删除器(参见12.1.4节,第415页)。
【出题思路】
本题练习使用自定义删除器。
【解答】
只需对TextQuery.cpp中file的初始化进行修改,创建一个DebugDelete对象作为第二个参数即可:
TextQuery::TextQuery(ifstream &is)
: file(new vector<string>, DebugDelete("shared_ptr"))
练习16.23:预测在你的查询主程序中何时会执行调用运算符。如果你的预测和实际不符,确认你理解了原因。
【出题思路】
本题旨在理解删除器的工作机制。
【解答】
当shared_ptr的引用计数变为0,需要释放资源时,才会调用删除器进行资源释放。分析查询主程序,runQueries函数结束时,TextQuery对象tq生命期结束,此时shared_ptr的引用计数变为0,会调用删除器释放资源(string的vector),此时调用运算符被执行,释放资源,打印一条信息。由于runQueries是主函数最后执行的语句,因此运行效果是程序结束前最后打印出信息。
编译运行上一题的程序,观察输出结果是否如此。
练习16.24:为你的Blob模板添加一个构造函数,它接受两个迭代器。
【出题思路】
本题练习定义类模板的成员模板。
【解答】
参考书中本节内容编写即可。然后修改主程序,添加相应的测试代码,如下:
Blob<string> b3(b1.begin(), b1.end());
for(auto p = b3.begin(); p != b3.end(); ++p)
cout << *p << endl;
练习16.25:解释下面这些声明的含义:
extern template class vector<string>;
template class vector<Sales_data>;
【出题思路】
理解实例化控制。
【解答】
第一条语句的extern表明不在本文件中生成实例化代码,该实例化的定义会在程序的其他文件中。第二条语句用Sales_data实例化vector,在其他文件中可用extern声明此实例化,使用此定义。
练习16.26:假设NoDefault是一个没有默认构造函数的类,我们可以显式实例化vector<NoDefault>吗?如果不可以,解释为什么。
【出题思路】
理解显式实例化类模板会实例化所有成员函数。
【解答】
答案是否定的。原因是,当我们显式实例化vector<NoDefault>时,编译器会实例化vector的所有成员函数,包括它接受容器大小参数的构造函数。vector的这个构造函数会使用元素类型的默认构造函数来对元素进行值初始化,而NoDefault没有默认构造函数,从而导致编译错误。
练习16.27:对下面每条带标签的语句,解释发生了什么样的实例化(如果有的话)。如果一个模板被实例化,解释为什么;如果未实例化,解释为什么没有。
template <typename T> class Stack { };
void f1(Stack<char>); //(a)
class Exercise {
Stack<double> &rsd; //(b)
Stack<int> si; //(c)
};
int main(){
Stack<char> *sc; //(d)
f1(*sc); //(e)
int iObj = sizeof(Stack<string>); //(f)
}
【出题思路】
理解显式实例化。
【解答】
(a)、(b)、(c)和(f)分别发生了Stack对char、double、int和string的实例化,因为这些语句都要用到这些实例化的类。
(d)、(e)未发生实例化,因为在本文件之前的位置已经发生了所需的实例化。
练习16.28:编写你自己版本的shared_ptr和unique_ptr。
【出题思路】
本题练习定义复杂的类模板。
【解答】
对于shared_ptr(我们的版本命名为SP),关于引用计数的管理、拷贝构造函数和拷贝赋值运算符等的设计,参考HasPtr即可。对于unique_ptr(我们的版本命名为UP),无须管理引用计数,也不支持拷贝构造函数和拷贝赋值运算符,只需设计release和reset等函数实现资源释放即可。
#ifndef PROGRAM16_28SP_H
#define PROGRAM16_28SP_H
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class SP {
public:
SP():p(nullptr), use(nullptr) { }
explicit SP(T *pt)
:p(pt), use(new size_t(1))
{ }
SP(const SP &sp)
:p(sp.p), use(sp.use)
{ if(use) ++*use; } //拷贝构造函数
SP& operator=(const SP&); //拷贝赋值运行符
~SP(); //析构函数
T& operator*() { return *p; } //解引用
T& operator*() const { return *p; } //const版
private:
T *p;
size_t *use;
};
template <typename T>
SP<T>::~SP<T>()
{
if(use && --*use == 0)
{
//如果引用计数变为0
delete p; //释放对象内存
delete use; //释放计数器内存
}
}
template <typename T>
SP<T>& SP<T>::operator=(const SP<T> &rhs)
{
if(rhs.use)
++*rhs.use; //递增右侧运算对象的引用计数
if(use && --*use == 0)
{
//然后递减本对象的引用计数
delete p; //发果没有其他用记
delete use; //释放本对象分配的成员
}
p = rhs.p; //拷贝指针
use = rhs.use;
return *this; //返回本对象
}
template <typename T, class... Args>
SP<T> make_SP(Args&&... args)
{
return SP<T>(new T(std::forward<Args>(args)...));
}
template <typename T>
class UP {
public:
UP(): p(nullptr) { } //禁止拷贝构造函数
UP(const UP &) = delete; //构造函数
explicit UP(T* pt): p(pt) { } //禁止拷贝赋值运算符
~UP();
T* release(); //交出控制权
void reset(T *new_p); //释放对象
T& operator*() { return *p; } //解引用运算符
T& operator*() const { return *p; } //const版
private:
T *p;
};
template <typename T>
UP<T>::~UP()
{
if(p) //如果已经分配了空间
delete p; //释放对象内存
}
template <typename T>
void UP<T>::reset(T *new_p)
{
if(p)
delete p; //释放对象内存
p = new_p; //指向新对象
}
template <typename T>
T* UP<T>::release()
{
T *q = p;
p = nullptr; //清空指针
return q; //返回对象指针
}
#endif // PROGRAM16_28SP_H
#include "program16_28SP.h"
#include <iostream>
#include <string>
using namespace std;
int main()
{
UP<int> u1(new int(78));
cout << *u1 << endl;
UP<int> u2(u1.release());
cout << *u2 << endl;
return 0;
}
运行结果:
练习16.29 修改你的Blob类,用你自己的shared_ptr代替标准库中的版本。
【出题思路】
本题练习使用类模板。
【解答】
对Blob类模板,需将shared_ptr替换为SP,将make_shared替换为make_SP。可以看出,我们并未完整实现shared_ptr和unique_ptr的全部功能,如,未实现->运算符。因此,在Blob类模板中,我们将使用->运算符的地方都改为先使用解引用运算符*,再使用.运算符。同时可以看到,我们也并没有将所有->改为*和.,这是由于类模板的特性,未使用的成员函数是不实例化的。因此,主函数中未用到的地方不进行修改,程序也能正确编译运行。读者可尝试实现shared_ptr和unique_ptr的完整功能,并把Blob中未修改的地方也修改过来。Blob中的修改比较零散,这里不再列出,读者阅读源代码即可。
练习16.30:重新运行你的一些程序,验证你的shared_ptr类和修改后的Blob类。(注意:实现weak_ptr类型超出了本书范围,因此你不能将BlobPtr类与你修改后的Blob一起使用。)
【出题思路】
本题练习使用类模板。
【解答】
主程序如下所示,其中测试了使用SP的Blob和UP。由于不能与BlobPtr一起使用,因此打印Blob所有内容时,使用的是at获取指定下标元素的方式。
#ifndef PROGRAM16_28SP_H
#define PROGRAM16_28SP_H
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class SP {
public:
SP():p(nullptr), use(nullptr) { }
explicit SP(T *pt)
:p(pt), use(new size_t(1))
{ }
SP(const SP &sp)
:p(sp.p), use(sp.use)
{ if(use) ++*use; } //拷贝构造函数
SP& operator=(const SP&); //拷贝赋值运行符
~SP(); //析构函数
T& operator*() { return *p; } //解引用
T& operator*() const { return *p; } //const版
private:
T *p;
size_t *use;
};
template <typename T>
SP<T>::~SP<T>()
{
if(use && --*use == 0)
{
//如果引用计数变为0
delete p; //释放对象内存
delete use; //释放计数器内存
}
}
template <typename T>
SP<T>& SP<T>::operator=(const SP<T> &rhs)
{
if(rhs.use)
++*rhs.use; //递增右侧运算对象的引用计数
if(use && --*use == 0)
{
//然后递减本对象的引用计数
delete p; //发果没有其他用记
delete use; //释放本对象分配的成员
}
p = rhs.p; //拷贝指针
use = rhs.use;
return *this; //返回本对象
}
template <typename T, class... Args>
SP<T> make_SP(Args&&... args)
{
return SP<T>(new T(std::forward<Args>(args)...));
}
template <typename T>
class UP {
public:
UP(): p(nullptr) { } //禁止拷贝构造函数
UP(const UP &) = delete; //构造函数
explicit UP(T* pt): p(pt) { } //禁止拷贝赋值运算符
~UP();
T* release(); //交出控制权
void reset(T *new_p); //释放对象
T& operator*() { return *p; } //解引用运算符
T& operator*() const { return *p; } //const版
private:
T *p;
};
template <typename T>
UP<T>::~UP()
{
if(p) //如果已经分配了空间
delete p; //释放对象内存
}
template <typename T>
void UP<T>::reset(T *new_p)
{
if(p)
delete p; //释放对象内存
p = new_p; //指向新对象
}
template <typename T>
T* UP<T>::release()
{
T *q = p;
p = nullptr; //清空指针
return q; //返回对象指针
}
#endif // PROGRAM16_28SP_H
#ifndef PROGRAM16_30SP_BLOB_H
#define PROGRAM16_30SP_BLOB_H
#include <iostream>
#include <vector>
#include <stdexcept>
#include <memory>
#include "program16_28SP.h"
using namespace std;
template <typename> class BlobPtr;
template <typename T> class Blob {
friend class BlobPtr<T>;
public:
typedef typename vector<T>::size_type size_type;
Blob();
Blob(initializer_list<T> il);
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& front();
T& back();
T& operator[](size_type i);
const T& front() const;
const T& back() const;
const T& at(size_type) const;
const T& operator[](size_type i) const;
private:
SP<vector<T>> data;
void check(size_type i, const string &msg) const;
};
template <typename T>
void Blob<T>::check(size_type i, const string &msg) const
{
if (i >= (*data).size())
throw out_of_range(msg);
}
template <typename T>
Blob<T>::Blob(): data(make_SP<vector<T>>()) { }
template <typename T>
Blob<T>::Blob(initializer_list<T> il):
data(make_SP<vector<T>>(il)) { }
template <typename T>
void Blob<T>::pop_back()
{
check(0, "pop back on empty Blob");
(*data).pop_back();
}
template <typename T>
T& Blob<T>::front()
{
check(0, "front on empty Blob");
return (*data).front();
}
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)
{
check(i, "subscritpt out of range");
return (*data)[i];
}
template <typename T>
const T& Blob<T>::front() const
{
check(0, "front on empty Blob");
return (*data).front();
}
template <typename T>
const T& Blob<T>::back() const
{
check(0, "back on empty Blob");
return (*data).back();
}
template <typename T>
const T& Blob<T>::operator[](size_type i) const
{
check(i, "subscritpt out of range");
return (*data)[i];
}
template <typename T>
const T& Blob<T>::at(size_type i) const
{
check(i, "subscritpt out of range");
return (*data).at(i);
}
#endif // PROGRAM16_30SP_BLOB_H
#include "program16_30SP_Blob.h"
#include <iostream>
#include <string>
using namespace std;
int main()
{
Blob<string> b1;//空Blob
cout << b1.size() << endl;
{//新作用域
Blob<string> b2 = {"red", "green", "blue"};
b1 = b2;//b1和b2共享相同的元素
b2.push_back("color");
cout << b1.size() << " " << b2.size() << endl;
}//b2被销毁,但它指向的元素不能被销毁
cout << b1.size() << endl;
for(size_t i = 0; i < b1.size(); ++i)
cout << b1.at(i) << " ";
cout << endl << endl;
UP<int> u1(new int(98));
cout << *u1 << endl;
UP<int> u2(u1.release());
cout << *u2 << endl;
return 0;
}
运行结果:
练习16.31:如果我们将DebugDelete与unique_ptr一起使用,解释编译器将删除器处理为内联形式的可能方式。
【出题思路】
理解shared_ptr和unique_ptr使用删除器的方式。
【解答】
shared_ptr是运行时绑定删除器,而unique_ptr则是编译时绑定删除器。unique_ptr有两个模板参数,一个是所管理的对象类型,另一个是删除器类型。因此,删除器类型是unique_ptr类型的一部分,在编译时就可知道,删除器可直接保存在unique_ptr对象中。通过这种方式,unique_ptr避免了间接调用删除器的运行时开销,而编译时还可以将自定义的删除器,如DebugDelete编译为内联形式。