《C++ Primer》第16章 模板与泛型编程
导读本章介绍了模板的相关知识,包括:
●如何定义模板。
●模板参数推断过程。
●重载模板。
●可变参数模板及模板特例化。
本章的练习着重帮助读者掌握如何利用模板这种语言特性来实现泛型编程,包括理解模板的基本概念、练习定义函数模板和类模板、理解模板参数推断过程、理解重载模板对函数匹配的影响以及理解练习可变参数模板和特例化的设计。
16.1节定义模板 习题答案
练习16.1:给出实例化的定义。
【出题思路】
理解实例化的基本概念。
【解答】
当调用一个函数模板时,编译器会利用给定的函数实参来推断模板实参,用此实际实参代替模板参数来创建出模板的一个新的“实例”,也就是一个真正可以调用的函数,这个过程称为实例化。
练习16.2:编写并测试你自己版本的compare函数。
【出题思路】
本题练习定义和使用函数模板。
【解答】
#include <iostream>
#include <string>
using namespace std;
template <typename T>
int compare(const T &v1, const T v2)
{
if(v1 < v2)
return -1;
if(v1 > v2)
return 1;
return 0;
}
int main()
{
int n1 = 32, n2 = 54;
double d1 = 34.5, d2 = 32.8;
string s1 = "camel";
string s2 = "plue";
float f1 = 34.5;
float f2 = 34.5;
cout << "compare(n1, n2)================" << compare(n1, n2) << endl;
cout << "compare(d1, d2)================" << compare(d1, d2) << endl;
cout << "compare(s1, s2)================" << compare(s1, s2) << endl;
cout << "compare(f1, f2)================" << compare(f1, f2) << endl;
cout << "Hello World!" << endl;
return 0;
}
运行结果:
练习16.3:对两个Sales_data对像调用你的compare函数,观察编译器在实例化过程中如何处理错误。
【出题思路】
理解函数模板对参数类型的要求。
【解答】
在Qt环境编译中,对两个Sales_data对象调用compare函数模板,编译器会报告如下错误。原因是compare是用<运算符来比较两个对象的,需要类型T事先定义<运算符。但Sales_data类并未定义<运算符,因此会报告错误。
error: invalid operands to binary expression ('const Sales_data' and 'const Sales_data')
练习16.4:编写行为类似标准库find算法的模板。函数需要两个模板类型参数,一个表示函数的迭代器参数,另一个表示值的类型。使用你的函数在一个vector<int>和一个list<string>中查找给定值。
【出题思路】
本题练习设计函数模板。
【解答】
用模板类型参数I表示迭代器类型,用T表示值的类型。find算法接受两个类型为I的参数b、e表示迭代器,和一个类型为T的参数v表示要查找的值。函数遍历范围[b,e)查找v,因此对I和T的要求是I必须支持++运算符和!=运算符,来实现遍历,并支持*运算符来获取元素值,且*运算的结果类型必须为T。当对vector<int>调用find时,I被解析为vector<int>::iterator,T被解析为int;当对list<string>调用find时,I被解析为list<string>:: iterator,T被解析为string。
#include <iostream>
#include <string>
#include <vector>
#include <list>
using namespace std;
template <typename I, typename T>
I findValue(I b, I e, const T &v)
{
while(b != e && *b !=v)
b++;
return b;
}
int main()
{
vector<int> vi = {0, 2, 4, 6, 8, 10};
list<string> ls = {"Hello", "World", "!"};
auto iter = findValue(vi.begin(), vi.end(), 6);
if(iter == vi.end())
cout << "can not find 6" << endl;
else
cout << "find 6 at position " << iter - vi.begin() << endl;
auto iter2 = findValue(ls.begin(), ls.end(), "mom");
if(iter2 == ls.end())
cout << "can not find mom" << endl;
else
cout << "found mom" << endl;
return 0;
}
练习16.5:为6.2.4节(第195页)中的print函数编写模板版本,它接受一个数组的引用,能处理任意大小、任意元素类型的数组。
【出题思路】
本题练习设计多模板参数的函数模板。
【解答】
由于希望print处理任意大小和任意元素类型的数组,因此需要两个模板参数:T是类型参数,表示数组元素类型;N是size_t类型常量,表示数组大小。
#include <iostream>
#include <string>
using namespace std;
template <typename T, size_t N>
void print(const T (&a)[N])
{
for(auto iter = begin(a); iter != end(a); iter++)
cout << *iter << " ";
cout << endl;
}
int main()
{
int a[6] = {0, 2, 4, 6, 8, 10};
string vs[3] = {"Hello", "world", "!"};
print(a);
print(vs);
return 0;
}
运行结果:
练习16.6:你认为接受一个数组实参的标准库函数begin和end是如何工作的?定义你自己版本的begin和end。
【出题思路】
本题练习设计begin和end。
【解答】
begin应返回数组首元素指针,因此是return &a[0]。end返回尾后指针,因此在begin上加上数组大小N即可。完成两个函数的编写后,可利用上一题的程序进行验证。
#include <iostream>
#include <string>
using namespace std;
template <typename T, size_t N>
const T* my_begin(const T (&a)[N])
{
return &a[0];
}
template <typename T, size_t N>
const T* my_end(const T (&a)[N])
{
return &a[0] + N;
}
template <typename T, size_t N>
void print(const T (&a)[N])
{
for(auto iter = my_begin(a); iter != my_end(a); iter++)
cout << *iter << " ";
cout << endl;
}
int main()
{
int a[6] = {0, 2, 4, 6, 8, 10};
string vs[3] = {"Hello", "world", "!"};
print(a);
print(vs);
return 0;
}
运行结果:
练习16.7:编写一个constexpr模板,返回给定数组的大小。
【出题思路】
本题练习设计constexpr模板。
【解答】
由于数组大小是数组类型的一部分,通过模板参数可以获取,因此在constexpr模板中直接返回它即可。
#include <iostream>
#include <string>
using namespace std;
template <typename T, size_t N>
constexpr int arr_size(const T (&a)[N])
{
return N;
}
template<typename T, size_t N>
void print(const T (&a)[N])
{
for(int i = 0; i < arr_size(a); i++)
cout << a[i] << " ";
cout << endl;
}
int main()
{
int a[6] = {0, 2, 4, 6, 8, 10};
string vs[3] = {"camel", "red", "blue"};
print(a);
print(vs);
return 0;
}
运行结果:
练习16.8:在第97页中的“关键概念”中,我们注意到,C++程序员喜欢使用!=而不喜欢<。解释这个习惯的原因。
【出题思路】
理解泛型编程的一个重点:算法对类型要求决定了算法的适用范围。
【解答】
泛型编程的一个目标就是令算法是“通用的”——适用于不同类型。所有标准库容器都定义了==和!=运算符,但其中只有少数定义了<运算符。因此,尽量使用!=而不是<,可减少你的算法适用容器的限制。
练习16.9:什么是函数模板?什么是类模板?
【出题思路】
理解模板的基本概念。
【解答】
简单来说,函数模板是可以实例化出特定函数的模板,类模板是可以实例化出特定类的模板。从形式上来说,函数模板与普通函数相似,只是要以关键字template开始,后接模板参数列表,类模板与普通类的关系类似。在使用上,编译器会根据调用来为我们推断函数模板的模板参数类型,而使用类模板实例化特定类就必须显式指定模板参数。
练习16.10:当一个类模板实例化时,会发生什么?
【出题思路】
理解类模板的实例化过程。
【解答】
当我们使用一个类模板时,必须显式提供模板实参列表,编译器将它们绑定到模板参数,来替换类模板定义中模板参数出现的地方,这样,就实例化出一个特定的类。我们随后使用的其实是这个特定的类。
练习16.11:下面List的定义是错误的。应如何修正它?
template <typename elemType> class ListItem;
template <typename elemType> class List{
public:
List<elemType>();
List<elemType>(const List<elemType> &;
List<elemType>& operator=(const List<elemType> &);
~List();
void insert(ListItem *ptr, elemType value);
private:
ListItem *front, *end;
};
【出题思路】
理解类模板不是一个类型。
【解答】
我们应该牢记,类模板的名字不是一个类型名,只有实例化后才能形成类型,而实例化总是要提供模板实参的。因此,在上述代码中直接使用ListItem是错误的,应该使用ListItem<elemType>,这才是一个类型。这个规则有一个例外,就是在类模板作用域中,可以不提供实参,直接使用模板名,也就是说,上述代码中,类内的List<elemType>可简化为List。
练习16.12:编写你自己版本的Blob和BlobPtr模板,包含书中未定义的多个const成员。
【出题思路】
本题练习定义类模板。
【解答】
#include <iostream>
#include <string>
#include <vector>
using namespace std;
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> i1);
//Blob中的元素数目
size_type size() const
{
return data->size();
}
bool empty() const
{
return data->empty();
}
//添加和删除元素
void push_back(const T &t)
{
data->push_back(t);
}
//移动版本, 参见13.6.3节
void push_back(T &&t)
{
data->push_back(std::move(t));
}
void pop_back();
//元素访问
T& back();
T& operator[](size_type i);//在14.5节
private:
std::shared_ptr<std::vector<T>> data;
//若data[i]无效,则抛出msg
void check(size_type i, const std::string &msg) const;
};
template <typename T>
Blob<T>::Blob()
{
}
template <typename T>
Blob<T>::Blob(std::initializer_list<T> i1)
:data(std::make_shared<std::vector<T>>(i1))
{
}
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];
}
template <typename T>
void Blob<T>::pop_back()
{
check(0, "pop_back on empty Blob");
data->pop_back();
}
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);
}
int main()
{
Blob<string> articles = {"a", "an", "the"};
//实例化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)
cout << "articles.size====================" << articles.size()<< endl;
cout << "articles.back====================" << articles.back()<< endl;
cout << "articles.empty===================" << articles.empty()<< endl;
for(size_t i = 0; i < articles.size(); ++i)
{
cout << articles[i] << " ";
}
cout << endl;
cout << endl;
cout << "squares.size=====================" << squares.size()<< endl;
cout << "squares.back=====================" << squares.back()<< endl;
cout << "squares.empty====================" << squares.empty()<< endl;
for(size_t i = 0; i < squares.size(); ++i)
{
cout << squares[i] << " ";
}
cout << endl;
return 0;
}
运行结果:
练习16.13:解释你为BlobPtr的相等和关系运算符选择哪种类型的友好关系?
【出题思路】
理解对模板如何设定友好关系。
【解答】
由于函数模板的实例化只处理特定类型,因此,对于相等和关系运算符,对每个BlobPtr实例与用相同类型实例化的运算符建立一对一的友好关系即可。
template <typename T> class BlobPtr{
friend bool operator==<T>(const BlobPtr<T>&, const BlobPtr<T>&);
}
练习16.14:编写Screen类模板,用非类型参数定义Screen的高和宽。
【出题思路】
本题练习定义类模板。
【解答】
类模板有两个非类型参数H和W,表示屏幕的高和宽。这样,成员height和width就不再需要了。注意,在类外给出成员函数的定义时,需要给出完整的模板实参列表。头文件如下所示:
#ifndef PROGRAM16_14SCREEN_H
#define PROGRAM16_14SCREEN_H
#include <string>
#include <iostream>
template <int H, int W>
class Screen
{
public:
Screen(): contents(H * W, ' ') { }
Screen(char c): contents(H * W, c) { }
friend class Window_mgr;
char get() const //获取光标的内容
{ return contents[cursor]; } //隐含是内联的
inline char get(int, int) const; //显示指定内联
Screen &clear(char = bkground);
Screen &move(int r, int c); //随后指定内联
Screen &set(char c);
Screen &set(int, int, char);
//重载display:普通版本和const版本
Screen &display(std::ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(std::ostream &os) const
{
do_display(os);
return *this;
}
private:
static const char bkground = ' ';
//实际完成显示的函数
void do_display(std::ostream &os) const
{ os << contents; }
int cursor = 0;
std::string contents;
};
template <int H, int W>
Screen<H, W> &Screen<H, W>::clear(char c)
{
contents = std::string(H * W, c);
return *this;
}
template <int H, int W> //可以定义时指定内联
inline Screen<H, W> &Screen<H, W>::move(int r, int c)
{
int row = r * W; //计算行位置
cursor = row + c; //将光标移动到此行指定列
return *this; //返回当前对象(左值)
}
template <int H, int W>
char Screen<H, W>::get(int r, int c) const //在类内已声明为内联
{
int row = r * W; //计算行位置
return contents[row + c]; //将光标移动到此行指定列
}
template <int H, int W>
inline Screen<H, W> &Screen<H, W>::set(char c)
{
contents[cursor] = c; //将光标处的内容设置为新值
return *this; //返回当前对象(左值)
}
template <int H, int W>
inline Screen<H, W> &Screen<H, W>::set(int r, int col, char ch)
{
contents[r * W + col] = col; //设置给定位置内容为新值
return *this; //返回当前对象(左值)
}
#endif // PROGRAM16_14SCREEN_H
主函数如下所示:
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
#include "program16_14Screen.h"
int main()
{
Screen<5, 3> myScreen;
myScreen.display(cout);
//将光标移动到特定位置,并设置其内容
myScreen.move(4, 0).set('#');
Screen<5, 5> nextScreen('X');
nextScreen.move(4, 0).set('#').display(cout);
cout << "\n";
nextScreen.display(cout);
cout << endl;
const Screen<5, 3> blank;
myScreen.set('#').display(cout); //调用非const版本
cout << endl;
blank.display(cout); //调用const版本
cout << endl;
myScreen.clear('Z').display(cout);
cout << endl;
myScreen.move(4, 0);
myScreen.set('#');
myScreen.display(cout);
cout << endl;
myScreen.clear('Z').display(cout);
cout << endl;
//由于temp类型是Screen<5, 3>而非Screen<5,3>&
Screen<5, 3> temp = myScreen.move(4, 0);//则返回值被拷贝
temp.set('#'); //改变temp就不会影响myScreen
myScreen.display(cout);
cout << endl;
return 0;
}
运行结果:
练习16.15:为你的Screen模板实现输入和输出运算符。Screen类需要哪些友元(如果需要的话)来令输入和输出运算符正确工作?解释每个友元声明(如果有的话)为什么是必要的。
【出题思路】
本题练习定义模板的友元。
【解答】
#ifndef PROGRAM16_15SCREEN_H
#define PROGRAM16_15SCREEN_H
#include <string>
#include <iostream>
template <int H, int W> class Screen;
template <int H, int W>
std::ostream &operator<<(std::ostream &, Screen<H, W> &);
template <int H, int W>
std::istream &operator>>(std::istream &, Screen<H, W> &);
template <int H, int W>
class Screen
{
public:
Screen(): contents(H * W, ' ') { }
Screen(char c): contents(H * W, c) { }
friend class Window_mgr;
char get() const //获取光标的内容
{ return contents[cursor]; } //隐含是内联的
inline char get(int, int) const; //显示指定内联
Screen &clear(char = bkground);
Screen &move(int r, int c); //随后指定内联
Screen &set(char c);
Screen &set(int, int, char);
//重载display:普通版本和const版本
Screen &display(std::ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(std::ostream &os) const
{
do_display(os);
return *this;
}
//添加友元函数 (为何要加<H, W>)
friend std::ostream &operator<< <H, W>(std::ostream &, Screen<H, W>&);
friend std::istream &operator>> <H, W>(std::istream &, Screen<H, W>&);
private:
static const char bkground = ' ';
//实际完成显示的函数
void do_display(std::ostream &os) const
{ os << contents; }
int cursor = 0;
std::string contents;
};
template <int H, int W>
Screen<H, W> &Screen<H, W>::clear(char c)
{
contents = std::string(H * W, c);
return *this;
}
template <int H, int W> //可以定义时指定内联
inline Screen<H, W> &Screen<H, W>::move(int r, int c)
{
int row = r * W; //计算行位置
cursor = row + c; //将光标移动到此行指定列
return *this; //返回当前对象(左值)
}
template <int H, int W>
char Screen<H, W>::get(int r, int c) const //在类内已声明为内联
{
int row = r * W; //计算行位置
return contents[row + c]; //将光标移动到此行指定列
}
template <int H, int W>
inline Screen<H, W> &Screen<H, W>::set(char c)
{
contents[cursor] = c; //将光标处的内容设置为新值
return *this; //返回当前对象(左值)
}
template <int H, int W>
inline Screen<H, W> &Screen<H, W>::set(int r, int col, char ch)
{
contents[r * W + col] = col; //设置给定位置内容为新值
return *this; //返回当前对象(左值)
}
//添加友元函数实现
template <int H, int W>
std::ostream& operator<<(std::ostream &os, Screen<H, W> &s)
{
os << s.contents;
return os;
}
template <int H, int W>
std::istream& operator>>(std::istream &is, Screen<H, W>& s)
{
std::string t;
is >> t;
s.contents = t.substr(0, H*W);
//s.contents = "camel";
return is;
}
#endif // PROGRAM16_15SCREEN_H
#include <iostream>
#include <string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
#include "program16_15Screen.h"
int main()
{
Screen<5, 3> myScreen;
myScreen.display(cout);
//将光标移动到特定位置,并设置其内容
myScreen.move(4, 0).set('#');
Screen<5, 5> nextScreen('X');
nextScreen.move(4, 0).set('#').display(cout);
cout << "\n";
nextScreen.display(cout);
cout << endl;
const Screen<5, 3> blank;
myScreen.set('#').display(cout); //调用非const版本
cout << endl;
blank.display(cout); //调用const版本
cout << endl;
myScreen.clear('Z').display(cout);
cout << endl;
myScreen.move(4, 0);
myScreen.set('#');
myScreen.display(cout);
cout << endl;
myScreen.clear('Z').display(cout);
cout << endl;
//由于temp类型是Screen<5, 3>而非Screen<5,3>&
Screen<5, 3> temp = myScreen.move(4, 0);//则返回值被拷贝
temp.set('#'); //改变temp就不会影响myScreen
myScreen.display(cout);
cout << endl;
cout << endl;
//cin >> myScreen;
cout << myScreen << endl << nextScreen << endl << temp << endl;
return 0;
}
运行结果:
练习16.16:将StrVec类(参见13.5节,第465页)重写为模板,命名为Vec。
【出题思路】
本题练习模板的定义和使用。
【解答】
类模板有一个类型参数T,表示向量的元素类型。在模板定义中,将原来的string用T替换即可。类模板编写方式与前几题类似,没有特别需要注意的地方。读者编写完毕后可与配套网站中的代码对照。