面向对象编程基础¶
本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org
ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book
第16章¶
模板与泛型编程¶
模板与泛型编程¶
面向对象编程(OOP)和泛型编程
- 都能处理在编写程序时不知道类型的情况。
- 不同之处在于:
- OOP能处理类型在程序运行之前都未知的情况;
- 而在泛型编程中,在编译时就能获知类型了。
容器、迭代器和算法都是泛型编程的例子。
当我们编写一个泛型程序时,是独立于任何特定类型来编写代码的。
当使用一个泛型程序时,我们提供类型或值,程序实例可在其上运行。
- 标准库为每个容器提供了单一的、泛型的定义,如vector。
我们可以使用这个泛型定义来定义很多类型的vector,
它们的差异就在于包含的元素类型不同。
模板与泛型编程¶
模板是泛型编程的基础。
我们不必了解模板是如何定义的就能使用它们,实际上我们已经这样用了。
在本章中,我们将学习如何定义自己的模板。
模板是C++中泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。
当使用一个vector这样的泛型类型,或者find这样的泛型函数时,
- 我们提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译时。
我们已经学习了如何使用模板。在本章中,我们将学习如何定义模板。
定义模板¶
假定我们希望编写一个函数来比较两个值,并指出第一个值是小于、等于还是大于第二个值。
在实际中,我们可能想要定义多个函数,每个函数比较一种给定类型的值。
我们的初次尝试可能定义多个重载函数:
// returns 0 if the values are equal, -1 if v1 is smaller, 1 if v2 is smaller
int compare(const string &v1, const string &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
int compare(const double &v1, const double &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
定义模板¶
这两个函数几乎是相同的,唯一的差异是参数的类型,函数体则完全一样。
如果对每种希望比较的类型都不得不重复定义完全一样的函数体,是非常烦琐且容易出错的。
更麻烦的是,在编写程序的时候,我们就要确定可能要compare的所有类型。
如果希望能在用户提供的类型上使用此函数,这种策略就失效了。
函数模板¶
我们可以定义一个通用的函数模板(function template),
而不是为每个类型都定义一个新函数。
一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
compare的模板版本可能像下面这样:
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板定义以关键字template开始,后跟一个模板参数列表(template parameterlist)
这是一个逗号分隔的一个或多个模板参数(template parameter)的列表,
用小于号(<)和大于号(>)包围起来。
在模板定义中,模板参数列表不能为空。
函数模板¶
模板参数列表的作用很像函数参数列表。
函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。
在运行时,调用者提供实参来初始化形参。
类似的,模板参数表示在类或函数定义中用到的类型或值。
当使用模板时,我们(隐式地或显式地)指定模板实参(template argument),
- 将其绑定到模板参数上。
我们的compare函数声明了一个名为T的类型参数。
在compare中,我们用名字T表示一个类型。
而T表示的实际类型则在编译时根据compare的使用情况来确定。
实例化函数模板¶
当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。
即,当我们调用compare时,编译器使用实参的类型来确定绑定到模板参数T的类型。
例如,在下面的调用中:
cout << compare(1, 0) << endl; // T is int
实参类型是int。编译器会推断出模板实参为int,并将它绑定到模板参数T。
编译器用推断出的模板参数来为我们实例化(instantiate)一个特定版本的函数。
当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出
模板的一个新“实例”。例如,给定下面的调用:
// instantiates int compare(const int&, const int&)
cout << compare(1, 0) << endl; // T is int
// instantiates int compare(const vector<int>&, const vector<int>&)
vector<int> vec1{1, 2, 3}, vec2{4, 5, 6};
cout << compare(vec1, vec2) << endl; // T is vector<int>
编译器会实例化出两个不同版本的compare。
实例化函数模板¶
cout << compare(1, 0) << endl; // T is int
cout << compare(vec1, vec2) << endl; // T is vector<int>
对于第一个调用,编译器会编写并编译一个compare版本,其中T被替换为int:
int compare(const int &v1, const int &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
对于第二个调用,编译器会生成另一个compare版本,其中T被替换为vector<int>。
这些编译器生成的版本通常被称为模板的实例(instantiation)。
模板类型参数¶
我们的compare函数有一个模板类型参数(type parameter)。
一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。
特别是,类型参数可以用来指定返回类型或函数的参数类型,
以及在函数体内用于变量声明或类型转换:
// ok: same type used for the return type and parameter
template <typename T> T foo(T* p){
T tmp = *p; // tmp will have the type to which p points
// ...
return tmp;
}
类型参数前必须使用关键字class或typename:
// error: must precede U with either typename or class
template <typename T, U> T calc(const T&, const U&);
在模板参数列表中,这两个关键字的含义相同,可以互换使用。
一个模板参数列表中可以同时使用这两个关键字:
template <typename T, class U> calc (const T&, const U&);
看起来用关键字typename来指定模板类型参数比用class更为直观。
毕竟,我们可以用内置(非类)类型作为模板类型实参。
而且,typename更清楚地指出随后的名字是一个类型名。
但是,typename是在模板已经广泛使用之后才引入C++语言的,某些程序员仍然只用class。
非类型模板参数¶
除了定义类型参数,还可以在模板中定义非类型参数(nontype parameter)。
一个非类型参数表示一个值而非一个类型。
我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。
这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
例如,我们可以编写一个compare版本处理字符串字面常量。
这种字面常量是const char的数组。由于不能拷贝一个数组,所以我们将自己的参数
定义为数组的引用。由于我们希望能比较不同长度的字符串字面常量,因此为模板定
义了两个非类型的参数。
第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度:
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
非类型模板参数¶
当我们调用这个版本的compare时:compare("hi", "mom")
编译器会使用字面常量的大小来代替N和M,从而实例化模板。
记住,编译器会在一个字符串字面常量的末尾插入一个空字符作为终结符
因此编译器会实例化出如下版本:
int compare(const char (&p1)[3], const char (&p2)[4])
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。
绑定到非类型整型参数的实参必须是一个常量表达式。
绑定到指针或引用非类型参数的实参必须具有静态的生存期。
我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参
指针参数也可以用nullptr或一个值为0的常量表达式来实例化
在模板定义内,模板非类型参数是一个常量值。
在需要常量表达式的地方,可以使用非类型参数,
例如,指定数组大小。
非类型模板参数的模板实参必须是常量表达式。
inline和constexpr的函数模板¶
函数模板可以声明为inline或constexpr的,如同非模板函数一样。
inline或constexpr说明符放在模板参数列表之后,返回类型之前:
// ok: inline specifier follows the template parameter list
template <typename T> inline T min(const T&, const T&);
// error: incorrect placement of the inline specifier
inline template <typename T> T min(const T&, const T&);
编写类型无关的代码¶
编写泛型代码的两个重要原则:·
- 模板中的函数参数是const的引用。·
- 函数体中的条件判断仅使用<比较运算。
通过将函数参数设定为const的引用,我们保证了函数可以用于不能拷贝的类型。
大多数类型,包括内置类型和我们已经用过的标准库类型(除unique_ptr和IO类型之外),
都是允许拷贝的。但是,不允许拷贝的类类型也是存在的。
通过将参数设定为const的引用,保证了这些类型可以用我们的compare函数来处理。
而且,如果compare用于处理大对象,这种设计策略还能使函数运行得更快。
编写类型无关的代码¶
你可能认为既使用<运算符又使用>运算符来进行比较操作会更为自然:
// expected comparison
if (v1 < v2) return -1;
if (v1 > v2) return 1;
return 0;
但是,如果编写代码时只使用<运算符,我们就降低了compare函数对要处理的类型的要求。
这些类型必须支持<,但不必同时支持>。
实际上,如果我们真的关心类型无关和可移植性,可能需要用less来定义我们的函数:
// version of compare that will be correct even if used on pointers; see § 14.8.2
template <typename T> int compare(const T &v1, const T &v2)
{
if (less<T>()(v1, v2)) return -1;
if (less<T>()(v2, v1)) return 1;
return 0;
}
原始版本存在的问题是,如果用户调用它比较两个指针,且两个指针未指向相同的数组
则代码的行为是未定义的
(less<T>的默认实现用的就是<,所以这其实并未起到让这种比较有一个良好定义的作用)。
模板程序应该尽量减少对实参类型的要求。
模板编译¶
当编译器遇到一个模板定义时,它并不生成代码。
只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。
当我们使用(而不是定义)模板时,编译器才生成代码,
- 这一特性影响了我们如何组织代码以及错误何时被检测到。
通常,当我们调用一个函数时,编译器只需要掌握函数的声明。
类似的,当我们使用一个类类型的对象时,类定义必须是可用的,
但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,
而普通函数和类的成员函数的定义放在源文件中。
模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。
因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
函数模板和类模板成员函数的定义通常放在头文件中。
关键概念:模板和头文件¶
模板包含两种名字:·
- 那些不依赖于模板参数的名字·
- 那些依赖于模板参数的名字
当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的。
而且,模板的提供者必须保证,当模板被实例化时,
- 模板的定义,包括类模板的成员的定义,也必须是可见的。
用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,
这是由模板的用户来保证的。
通过组织良好的程序结构,恰当使用头文件,这些要求都很容易满足。模板的设计者
应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。
模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。
大多数编译错误在实例化期间报告¶
模板直到实例化时才会生成代码,这一特性影响了我们何时才会获知模板内代码的编译错误。
通常,编译器会在三个阶段报告错误。
- 第一个阶段是编译模板本身时。
在这个阶段,编译器通常不会发现很多错误。编译器可以检查语法错误,
例如忘记分号或者变量名拼错等,但也就这么多了。
- 第二个阶段是编译器遇到模板使用时。在此阶段,编译器仍然没有很多可检查的。
对于函数模板调用,编译器通常会检查实参数目是否正确。它还能检查参数类型是否匹配。
对于类模板,编译器可以检查用户是否提供了正确数目的模板实参,但也仅限于此了。
- 第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误。
依赖于编译器如何管理实例化,这类错误可能在链接时才报告。
当我们编写模板时,代码不能是针对特定类型的,
但模板代码通常对其所使用的类型有一些假设。
//例如,我们最初的compare函数中的代码就假定实参类型定义了<运算符。
if (v1 < v2) return -1; // requires < on objects of type T
if (v2 < v1) return 1; // requires < on objects of type T
return 0; // returns int; not dependent on T
当编译器处理此模板时,它不能验证if语句中的条件是否合法。
大多数编译错误在实例化期间报告¶
如果传递给compare的实参定义了<运算符,则代码就是正确的,否则就是错误的。例如,
Sales_data data1, data2;
cout << compare(data1, data2);// error: no < on Sales_data
此调用实例化了compare的一个版本,将T替换为Sales_data。
if条件试图对Sales_data对象使用<运算符,但Sales_data并未定义此运算符。
此实例化生成了一个无法编译通过的函数版本。
但是,这样的错误直至编译器在类型Sales_data上实例化compare时才会被发现。
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
练习¶
给出实例化的定义。
当编译器实例化一个模版时,
它使用实际的模版参数代替对应的模版参数来创建出模版的一个新“实例”。
练习¶
编写并测试你自己版本的 compare 函数。
template<typename T>
int compare(const T& lhs, const T& rhs)
{
if (lhs < rhs) return -1;
if (rhs < lhs) return 1;
return 0;
}
练习¶
对两个 Sales_data 对象调用你的 compare 函数,
观察编译器在实例化过程中如何处理错误。
error: no match for 'operator<'
练习¶
编写行为类似标准库 find 算法的模版。
函数需要两个模版类型参数,一个表示函数的迭代器参数,另一个表示值的类型。
使用你的函数在一个 vector<int> 和一个list<string>中查找给定值。
template<typename Iterator, typename Value>
Iterator find(Iterator first, Iterator last, const Value& v)
{
for ( ; first != last && *first != value; ++first);
return first;
}
练习¶
为6.2.4节中的print函数编写模版版本,
它接受一个数组的引用,能处理任意大小、任意元素类型的数组。
template<typename Array>
void print(const Array& arr)
{
for (const auto& elem : arr)
std::cout << elem << std::endl;
}
练习¶
你认为接受一个数组实参的标准库函数 begin 和 end 是如何工作的?
定义你自己版本的 begin 和 end。
template<typename T, unsigned N>
T* begin(const T (&arr)[N])
{
return arr;
}
template<typename T, unsigned N>
T* end(const T (&arr)[N])
{
return arr + N;
}
练习¶
编写一个 constexpr 模版,返回给定数组的大小。
template<typename T, size_t N> constexpr
unsigned size(const T (&arr)[N])
{
return N;
}
练习¶
在第97页的“关键概念”中,我们注意到,
C++程序员喜欢使用 != 而不喜欢 < 。解释这个习惯的原因。
因为大多数类只定义了 != 操作而没有定义 < 操作,
使用 != 可以降低对要处理的类型的要求。
类模板¶
类模板(class template)是用来生成类的蓝图的。
与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。
如我们已经多次看到的,为了使用类模板,我们必须在模板名后的尖括号中提供额外信息
— 用来代替模板参数的模板实参列表。
定义类模板¶
作为一个例子,我们将实现StrBlob的模板版本。
我们将此模板命名为Blob,意指它不再针对string。
- 类似StrBlob,我们的模板会提供对元素的共享(且核查过的)访问能力。
- 与类不同,我们的模板可以用于更多类型的元素。
与标准库容器相同,当使用Blob时,用户需要指出元素类型。
类似函数模板,类模板以关键字template开始,后跟模板参数列表。
在类模板(及其成员)的定义中,我们将模板参数当作替身,
代替使用模板时用户需要提供的类型或值:
定义类模板¶
template <typename T> class Blob {
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// constructors
Blob();
Blob(std::initializer_list<T> il);
// number of elements in the Blob
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// add and remove elements
void push_back(const T &t) {data->push_back(t);}
// move version; see § 13.6.3
void push_back(T &&t) { data->push_back(std::move(t)); }
void pop_back();
// element access
T& back();
T& operator[](size_type i); // defined in § 14.5
定义类模板¶
private:
std::shared_ptr<std::vector<T>> data;
// throws msg if data[i] isn't valid
void check(size_type i, const std::string &msg) const;
};
我们的Blob模板有一个名为T的模板类型参数,用来表示Blob保存的元素的类型。
例如,我们将元素访问操作的返回类型定义为T&。
当用户实例化Blob时,T就会被替换为特定的模板实参类型。
除了模板参数列表和使用T代替string之外,此类模板的定义与12.1.1节中定义的
类版本及12.1.6节和第13章、第14章中更新的版本是一样的。
实例化类模板¶
我们已经多次见到,当使用一个类模板时,我们必须提供额外信息。
我们现在知道这些额外信息是显式模板实参(explicit template argument)列表,
它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类。
例如,为了用我们的Blob模板定义一个类型,必须提供元素类型:
Blob<int> ia; // empty Blob<int>
Blob<int> ia2 = {0,1,2,3,4}; // Blob<int> with five elements
//ia和ia2使用相同的特定类型版本的Blob(即Blob<int>)。
从这两个定义,编译器会实例化出一个与下面定义等价的类:
template <> class Blob<int> {
typedef typename std::vector<int>::size_type size_type;
Blob();
Blob(std::initializer_list<int> il);
// ...
int& operator[](size_type i);
private:
std::shared_ptr<std::vector<int>> data;
void check(size_type i, const std::string &msg) const;
};
实例化类模板¶
当编译器从我们的Blob模板实例化出一个类时,它会重写Blob模板,
将模板参数T的每个实例替换为给定的模板实参,在本例中是int。
对我们指定的每一种元素类型,编译器都生成一个不同的类
// these definitions instantiate two distinct Blob types
Blob<string> names; // Blob that holds strings
Blob<double> prices;// different element type
这两个定义会实例化出两个不同的类。
- names的定义创建了一个Blob类,每个T都被替换为string。
- prices的定义生成了另一个Blob类,T被替换为double。
一个类模板的每个实例都形成一个独立的类。
类型Blob<string>与任何其他Blob类型都没有关联,
也不会对任何其他Blob类型的成员有特殊访问权限。
在模板作用域中引用模板类型¶
为了阅读模板类代码,应该记住类模板的名字不是一个类型名。
类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
可能令人迷惑的是,一个类模板中的代码如果使用了另外一个模板,
通常不将一个实际类型(或值)的名字用作其模板实参。
相反的,我们通常将模板自己的参数当作被使用模板的实参。
例如,我们的data成员使用了两个模板,vector和shared_ptr。
我们知道,无论何时使用模板都必须提供模板实参。
在本例中,我们提供的模板实参就是Blob的模板参数。因此,data的定义如下:
std::shared_ptr<std::vector<T>> data;
它使用了Blob的类型参数来声明data是一个shared_ptr的实例,
此shared_ptr指向一个保存类型为T的对象的vector实例。
当我们实例化一个特定类型的Blob,例如Blob<string>时,data会成为:
shared_ptr<vector<string>>
如果我们实例化Blob<int>,则data会成为shared_ptr<vector<int>>,依此类推。
类模板的成员函数¶
与其他任何类相同,我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,
且定义在类模板内的成员函数被隐式声明为内联函数。
类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。
因此,类模板的成员函数具有和模板相同的模板参数。
因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。
与往常一样,当我们在类外定义一个成员时,必须说明成员属于哪个类。
而且,从一个模板生成的类的名字中必须包含其模板实参。当我们定义一个成员函数时,
模板实参与模板形参相同。即,对于StrBlob的一个给定的成员函数
ret-type StrBlob::member-name(parm-list)
对应的Blob的成员应该是这样的:
template <typename T>
ret-type Blob<T>::member-name(parm-list)
check和元素访问成员¶
我们首先定义check成员,它检查一个给定的索引:
template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
throw std::out_of_range(msg);
}
除了类名中的不同之处以及使用了模板参数列表外,
此函数与原StrBlob类的check成员完全一样。
check和元素访问成员¶
下标运算符和back函数用模板参数指出返回类型,其他未变:
template <typename T>
T& Blob<T>::back()
{
check(0, "back on empty Blob");
return data->back();
}
template <typename T>
T& Blob<T>::operator[](size_type i)
{
// if i is too big, check will throw, preventing access to a nonexistent element
check(i, "subscript out of range");
return (*data)[i];
}
在原StrBlob类中,这些运算符返回string&。而模板版本则返回一个引用,
指向用来实例化Blob的类型。
check和元素访问成员¶
pop_back函数与原StrBlob的成员几乎相同:
template <typename T> void Blob<T>::pop_back()
{
check(0, "pop_back on empty Blob");
data->pop_back();
}
在原StrBlob类中,下标运算符和back成员都对const对象进行了重载。
我们将这些成员及front成员的定义留作练习。
Blob构造函数¶
与其他任何定义在类模板外的成员一样,构造函数的定义要以模板参数开始:
template <typename T>
Blob<T>::Blob(): data(std::make_shared<std::vector<T>>()) { }
这段代码在作用域Blob<T>中定义了名为Blob的成员函数。类似StrBlob的默认构造函数,
此构造函数分配一个空vector,并将指向vector的指针保存在data中。
如前所述,我们将类模板自己的类型参数作为vector的模板实参来分配vector。
Blob构造函数¶
类似的,接受一个initializer_list参数的构造函数将其类型参数T
作为initializer_list参数的元素类型:
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il):
data(std::make_shared<std::vector<T>>(il)) { }
类似默认构造函数,此构造函数分配一个新的vector。
在本例中,我们用参数il来初始化此vector。为了使用这个构造函数,
我们必须传递给它一个initializer_list,其中的元素必须与Blob的元素类型兼容:
Blob<string> articles = {"a", "an", "the"};
这条语句中,构造函数的参数类型为initializer_list<string>。
列表中的每个字符串字面常量隐式地转换为一个string。
类模板成员函数的实例化¶
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。
例如,下面代码
Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};
// instantiates Blob<int>::size() const
for (size_t i = 0; i != squares.size(); ++i)
squares[i] = i*i; // instantiates Blob<int>::operator[](size_t)
实例化了Blob<int>类和它的三个成员函数:
operator[]、size和接受initializer_list<int>的构造函数。
如果一个成员函数没有被使用,则它不会被实例化。成员函数只有在被用到时才进行实例化
这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
在类代码内简化模板类名的使用¶
当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。
在类模板自己的作用域中,我们可以直接使用模板名而不提供实参:
// BlobPtr throws an exception on attempts to access a nonexistent element
template <typename T> class BlobPtr
public:
BlobPtr(): curr(0) { }
BlobPtr(Blob<T> &a, size_t sz = 0):
wptr(a.data), curr(sz) { }
T& operator*() const
{ auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) is the vector to which this object points
}
// increment and decrement
BlobPtr& operator++(); // prefix operators
BlobPtr& operator--();
在类代码内简化模板类名的使用¶
private:
// check returns a shared_ptr to the vector if the check succeeds
std::shared_ptr<std::vector<T>>
check(std::size_t, const std::string&) const;
// store a weak_ptr, which means the underlying vector might be destroyed
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr; // current position within the array
};
细心的读者可能已经注意到,BlobPtr的前置递增和递减成员返回BlobPtr&,
而不是BlobPtr<T>&。当我们处于一个类模板的作用域中时,
编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。
即,就好像我们这样编写代码一样:
BlobPtr<T>& operator++();
BlobPtr<T>& operator--();
在类模板外使用类模板名¶
当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,
直到遇到类名才表示进入类的作用域:
// postfix: increment/decrement the object but return the unchanged value
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
// no check needed here; the call to prefix increment will do the check
BlobPtr ret = *this; // save the current value
++*this; // advance one element; prefix ++ checks the increment
return ret; // return the saved state
}
在类模板外使用类模板名¶
由于返回类型位于类的作用域之外,我们必须指出返回类型是一个实例化的BlobPtr,
它所用类型与类实例化所用类型一致。在函数体内,我们已经进入类的作用域,
因此在定义ret时无须重复模板实参。
如果不提供模板实参,则编译器将假定我们使用的类型与成员实例化所用类型一致。
因此,ret的定义与如下代码等价:
BlobPtr<T> ret = *this;
在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。
类模板和友元¶
当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。
如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系¶
类模板与另一个(类或函数)模板间友好关系的最常见的形式是
建立对应实例及其友元间的友好关系。
例如,我们的Blob类应该将BlobPtr类和一个模板版本的Blob相等运算符(练习中
为StrBlob定义的)定义为友元。
为了引用(类或函数)模板的一个特定实例,我们必须首先声明模板自身。
一个模板声明包括模板参数列表:
// forward declarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob; // needed for parameters in operator==
template <typename T>
bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
// each instantiation of Blob grants access to the version of
// BlobPtr and the equality operator instantiated with the same type
friend class BlobPtr<T>;
friend bool operator==<T>
(const Blob<T>&, const Blob<T>&);
// other members as in § 12.1.1
};
一对一友好关系¶
我们首先将Blob、BlobPtr和operator==声明为模板。
这些声明是operator==函数的参数声明以及Blob中的友元声明所需要的。
友元的声明用Blob的模板形参作为它们自己的模板实参。
因此,友好关系被限定在用相同类型实例化的Blob与BlobPtr相等运算符之间:
Blob<char> ca; // BlobPtr<char> and operator==<char> are friends
Blob<int> ia; // BlobPtr<int> and operator==<int> are friends
BlobPtr<char>的成员可以访问ca(或任何其他Blob<char>对象)的非public部分,
但ca对ia(或任何其他Blob<int>对象)或Blob的任何其他实例都没有特殊访问权限。
通用和特定的模板友好关系¶
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元
// forward declaration necessary to befriend a specific instantiation of a template
template <typename T> class Pal;
class C { // C is an ordinary, nontemplate class
friend class Pal<C>; // Pal instantiated with class C is a friend to C
// all instances of Pal2 are friends to C;
// no forward declaration required when we befriend all instantiations
template <typename T> friend class Pal2;
};
template <typename T> class C2 { // C2 is itself a class template
// each instantiation of C2 has the same instance of Pal as a friend
friend class Pal<T>; // a template declaration for Pal must be inscope
// all instances of Pal2 are friends of each instance of C2, prior declaration needed
template <typename X> friend class Pal2;
// Pal3 is a nontemplate class that is a friend of every instance of C2
friend class Pal3; // prior declaration for Pal3 not needed
};
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
令模板自己的类型参数成为友元¶
在新标准中,我们可以将模板类型参数声明为友元:
template <typename Type> class Bar {
friend Type; // grants access to the type used to instantiate Bar
// ...
};
此处我们将用来实例化Bar的类型声明为友元。
因此,对于某个类型名Foo,Foo将成为Bar<Foo>的友元,
Sales_data将成为Bar<Sales_data>的友元,依此类推。
值得注意的是,虽然友元通常来说应该是一个类或是一个函数,
但我们完全可以用一个内置类型来实例化Bar。
这种与内置类型的友好关系是允许的,以便我们能用内置类型来实例化Bar这样的类。
模板类型别名¶
类模板的一个实例定义了一个类类型,与任何其他类类型一样,
我们可以定义一个typedef来引用实例化的类:
typedef Blob<string> StrBlob;
这条typedef语句允许我们运行在12.1.1节中编写的代码,
而使用的却是用 string 实例化的模板版本的 Blob。
由于模板不是一个类型,我们不能定义一个typedef引用一个模板。
即,无法定义一个typedef引用Blob<T>。
但是,新标准允许我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors是一个pair<string, string>
在这段代码中,我们将twin定义为成员类型相同的pair的别名。
这样,twin的用户只需指定一次类型。
模板类型别名¶
一个模板类型别名是一族类的别名:
twin<int> win_loss; // win_loss是一个pair<int, int>
twin<double> area; // area是一个pair<double, double>
就像使用类模板一样,当我们使用twin时,需要指出希望使用哪种特定类型的twin。
当我们定义一个模板类型别名时,可以固定一个或多个模板参数:
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books; // books是一个pair<string, unsigned>
partNo<Vehicle> cars; // cars是一个pair<Vehicle, unsigned>
partNo<Student> kids; // kids是一个pair<Student, unsigned>
这段代码中我们将 partNo 定义为一族类型的别名,
这族类型是 second 成员为unsigned的pair。
partNo的用户需要指出pair的first成员的类型,但不能指定second成员的类型。
类模板的static成员¶
与任何其他类相同,类模板可以声明static成员
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
// other interface members
private:
static std::size_t ctr;
// other implementation members
};
Foo是一个类模板,它有一个名为count的public static成员函数
和一个名为ctr的private static数据成员。每个Foo的实例都有其自己的static成员实例
即,对任意给定类型 X,都有一个 Foo<X>::ctr 和一个 Foo<X>::count成员。
所有Foo<X>类型的对象共享相同的ctr对象和count函数。例如,
// instantiates static members Foo<string>::ctr and Foo<string>::count
Foo<string> fs;
// all three objects share the same Foo<int>::ctr and Foo<int>::count members
Foo<int> fi, fi2, fi3;
类模板的static成员¶
与任何其他static数据成员相同,模板类的每个static数据成员必须有且仅有一个定义。
但是,类模板的每个实例都有一个独有的static对象。
因此,与定义模板的成员函数类似,我们将static数据成员也定义为模板:
template <typename T>
size_t Foo<T>::ctr = 0; // define and initialize ctr
与类模板的其他任何成员类似,定义的开始部分是模板参数列表,
随后是我们定义的成员的类型和名字。
与往常一样,成员名包括成员的类名,对于从模板生成的类来说,类名包括模板实参。
因此,当使用一个特定的模板实参类型实例化 Foo 时,
将会为该类类型实例化一个独立的ctr,并将其初始化为0。
类模板的static成员¶
与非模板类的静态成员相同,我们可以通过类类型对象来访问一个类模板的static成员
也可以使用作用域运算符直接访问成员。
当然,为了通过类来直接访问static成员,我们必须引用一个特定的实例:
Foo<int> fi; // instantiates Foo<int> class
// and the static data member ctr
auto ct = Foo<int>::count(); // instantiates Foo<int>::count
ct = fi.count(); // uses Foo<int>::count
ct = Foo::count(); // error: which template instantiation?
类似任何其他成员函数,一个static成员函数只有在使用时才会实例化。
练习¶
什么是函数模版,什么是类模版?
一个函数模版就是一个公式,可用来生成针对特定类型的函数版本。
类模版是用来生成类的蓝图的,
与函数模版的不同之处是,编译器不能为类模版推断模版参数类型。
如果我们已经多次看到,为了使用类模版,我们必须在模版名后的尖括号中提供额外信息。
练习¶
当一个类模版被实例化时,会发生什么?
一个类模版的每个实例都形成一个独立的类。
练习¶
下面 List 的定义是错误的。应如何修改它?
template <typename elemType> class ListItem;
template <typename elemType> class List {
public:
List<elemType>();
List<elemType>(const List<elemType> &);
List<elemType>& operator=(const List<elemType> &);
~List();
void insert(ListItem *ptr, elemType value);
private:
ListItem *front, *end;
};
练习¶
模版需要模版参数,应该修改为如下:
template <typename elemType> class ListItem;
template <typename elemType> class List{
public:
List<elemType>();
List<elemType>(const List<elemType> &);
List<elemType>& operator=(const List<elemType> &);
~List();
void insert(ListItem<elemType> *ptr, elemType value);
private:
ListItem<elemType> *front, *end;
};
练习¶
编写你自己版本的 Blob 和 BlobPtr 模版,包含书中未定义的多个const成员。
//Blob:
#include <memory>
#include <vector>
template<typename T> class Blob
{
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// constructors
Blob();
Blob(std::initializer_list<T> il);
// number of elements in the Blob
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
练习¶
void push_back(const T& t) { data->push_back(t); }
void push_back(T&& t) { data->push_back(std::move(t)); }
void pop_back();
// element access
T& back();
T& operator[](size_type i);
const T& back() const;
const T& operator [](size_type i) const;
private:
std::shared_ptr<std::vector<T>> data;
// throw msg if data[i] isn't valid
void check(size_type i, const std::string &msg) const;
};
练习¶
// constructors
template<typename T>
Blob<T>::Blob() : data(std::make_shared<std::vector<T>>())
{}
template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) :
data(std::make_shared<std::vector<T>>(il))
{}
template<typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
throw std::out_of_range(msg);
}
练习¶
template<typename T>
T& Blob<T>::back()
{
check(0, "back on empty Blob");
return data->back();
}
template<typename T>
const T& Blob<T>::back() const
{
check(0, "back on empty Blob");
return data->back();
}
练习¶
template<typename T>
T& Blob<T>::operator [](size_type i)
{
// if i is too big, check function will throw, preventing access to a nonexistent element
check(i, "subscript out of range");
return (*data)[i];
}
template<typename T>
const T& Blob<T>::operator [](size_type i) const
{
// if i is too big, check function will throw, preventing access to a nonexistent element
check(i, "subscript out of range");
return (*data)[i];
}
template<typename T>
void Blob<T>::pop_back()
{
check(0, "pop_back on empty Blob");
data->pop_back();
}
练习¶
//BlobPtr:
#include "Blob.h"
#include <memory>
#include <vector>
template <typename> class BlobPtr;
template <typename T>
bool operator ==(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);
template <typename T>
bool operator < (const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);
练习¶
template<typename T> class BlobPtr
{
friend bool operator ==<T>
(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);
friend bool operator < <T>
(const BlobPtr<T>& lhs, const BlobPtr<T>& rhs);
public:
BlobPtr() : curr(0) {}
BlobPtr(Blob<T>& a, std::size_t sz = 0) :
wptr(a.data), curr(sz)
{}
T& operator*() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
练习¶
// prefix
BlobPtr& operator++();
BlobPtr& operator--();
// postfix
BlobPtr operator ++(int);
BlobPtr operator --(int);
private:
// returns a shared_ptr to the vector if the check succeeds
std::shared_ptr<std::vector<T>>
check(std::size_t, const std::string&) const;
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr;
};
练习¶
// prefix ++
template<typename T>
BlobPtr<T>& BlobPtr<T>::operator ++()
{
// if curr already points past the end of the container, can't increment it
check(curr, "increment past end of StrBlob");
++curr;
return *this;
}
// prefix --
template<typename T>
BlobPtr<T>& BlobPtr<T>::operator --()
{
--curr;
check(curr, "decrement past begin of BlobPtr");
return *this;
}
练习¶
// postfix ++
template<typename T>
BlobPtr<T> BlobPtr<T>::operator ++(int)
{
BlobPtr ret = *this;
++*this;
return ret;
}
// postfix --
template<typename T>
BlobPtr<T> BlobPtr<T>::operator --(int)
{
BlobPtr ret = *this;
--*this;
return ret;
}
练习¶
template<typename T> bool operator==(const BlobPtr<T> &lhs, const BlobPtr<T> &rhs)
{
if (lhs.wptr.lock() != rhs.wptr.lock())
{
throw runtime_error("ptrs to different Blobs!");
}
return lhs.i == rhs.i;
}
template<typename T> bool operator< (const BlobPtr<T> &lhs, const BlobPtr<T> &rhs)
{
if (lhs.wptr.lock() != rhs.wptr.lock())
{
throw runtime_error("ptrs to different Blobs!");
}
return lhs.i < rhs.i;
}
练习¶
解释你为 BlobPtr 的相等和关系运算符选择哪种类型的友好关系?
这里需要与类型一一对应,所以就选择一对一友好关系。
练习¶
编写 Screen 类模版,用非类型参数定义 Screen 的高和宽。
//Screen
#include <string>
#include <iostream>
template<unsigned H, unsigned W>
class Screen
{
public:
typedef std::string::size_type pos;
Screen() = default; // needed because Screen has another constructor
// cursor initialized to 0 by its in-class initializer
Screen(char c) :contents(H * W, c) {}
char get() const // get the character at the cursor
{
return contents[cursor];
} // implicitly inline
Screen &move(pos r, pos c); // can be made inline later
练习¶
friend std::ostream & operator<< (std::ostream &os, const Screen<H, W> & c)
{
unsigned int i, j;
for (i = 0; i<c.height; i++)
{
os << c.contents.substr(0, W) << std::endl;
}
return os;
}
friend std::istream & operator>> (std::istream &is, Screen & c)
{
char a;
is >> a;
std::string temp(H*W, a);
c.contents = temp;
return is;
}
练习¶
private:
pos cursor = 0;
pos height = H, width = W;
std::string contents;
};
template<unsigned H, unsigned W>
inline Screen<H, W>& Screen<H, W>::move(pos r, pos c)
{
pos row = r * width;
cursor = row + c;
return *this;
}
练习¶
为你的 Screen 模版实现输入和输出运算符。
Screen 类需要哪些友元(如果需要的话)来令输入和输出运算符正确工作?
解释每个友元声明(如果有的话)为什么是必要的。
类的 operator<< 和 operator>> 应该是类的友元。
练习¶
将 StrVec 类重写为模版,命名为 Vec。
//Vec:
#include <memory>
template<typename T>
class Vec
{
public:
Vec() :element(nullptr), first_free(nullptr), cap(nullptr) {}
Vec(std::initializer_list<T> l);
Vec(const Vec& v);
Vec& operator =(const Vec& rhs);
~Vec();
// memmbers
void push_back(const T& t);
练习¶
std::size_t size() const { return first_free - element; }
std::size_t capacity()const { return cap - element; }
T* begin() const { return element; }
T* end() const { return first_free; }
void reserve(std::size_t n);
void resize(std::size_t n);
void resize(std::size_t n, const T& t);
private:
// data members
T* element;
T* first_free;
T* cap;
std::allocator<T> alloc;
练习¶
// utillities
void reallocate();
void chk_n_alloc() { if (size() == capacity()) reallocate(); }
void free();
void wy_alloc_n_move(std::size_t n);
std::pair<T*, T*> alloc_n_copy(T* b, T* e);
};
练习¶
// copy constructor
template<typename T>
Vec<T>::Vec(const Vec &v)
{
std::pair<T*, T*> newData = alloc_n_copy(v.begin(), v.end());
element = newData.first;
first_free = cap = newData.second;
}
练习¶
// constructor that takes initializer_list<T>
template<typename T>
Vec<T>::Vec(std::initializer_list<T> l)
{
// allocate memory as large as l.size()
T* const newData = alloc.allocate(l.size());
// copy elements from l to the address allocated
T* p = newData;
for (const auto &t : l)
alloc.construct(p++, t);
// build data structure
element = newData;
first_free = cap = element + l.size();
}
练习¶
// operator =
template<typename T>
Vec<T>& Vec<T>::operator =(const Vec& rhs)
{
// allocate and copy first to protect against self_assignment
std::pair<T*, T*> newData = alloc_n_copy(rhs.begin(), rhs.end());
// destroy and deallocate
free();
// update data structure
element = newData.first;
first_free = cap = newData.second;
return *this;
}
练习¶
// destructor
template<typename T>
Vec<T>::~Vec()
{
free();
}
template<typename T>
void Vec<T>::push_back(const T &t)
{
chk_n_alloc();
alloc.construct(first_free++, t);
}
练习¶
template<typename T>
void Vec<T>::reserve(std::size_t n)
{
// if n too small, just return without doing anything
if (n <= capacity()) return;
// allocate new memory and move data from old address to the new one
wy_alloc_n_move(n);
}
template<typename T>
void Vec<T>::resize(std::size_t n)
{
resize(n, T());
}
练习¶
template<typename T>
void Vec<T>::resize(std::size_t n, const T &t)
{
if (n < size())
{
// destroy the range [element+n, first_free) using destructor
for (auto p = element + n; p != first_free;)
alloc.destroy(p++);
// update first_free to point to the new address
first_free = element + n;
}
else if (n > size())
{
for (auto i = size(); i != n; ++i)
push_back(t);
}
}
练习¶
template<typename T>
std::pair<T*, T*>
Vec<T>::alloc_n_copy(T *b, T *e)
{
// calculate the size needed and allocate space accordingly
T* data = alloc.allocate(e - b);
return{ data, std::uninitialized_copy(b, e, data) };
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// which copies the range[first, last) to the space to which
// the starting address data is pointing.
// This function returns a pointer to one past the last element
}
练习¶
template<typename T>
void Vec<T>::free()
{
// if not nullptr
if (element)
{
// destroy it in reverse order.
for (auto p = first_free; p != element;)
alloc.destroy(--p);
alloc.deallocate(element, capacity());
}
}
练习¶
template<typename T>
void Vec<T>::wy_alloc_n_move(std::size_t n)
{
// allocate as required.
std::size_t newCapacity = n;
T* newData = alloc.allocate(newCapacity);
// move the data from old place to the new one
T* dest = newData;
T* old = element;
for (std::size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*old++));
free();
// update data structure
element = newData;
first_free = dest;
cap = element + newCapacity;
}
练习¶
template<typename T>
void Vec<T>::reallocate()
{
// calculate the new capacity required
std::size_t newCapacity = size() ? 2 * size() : 1;
// allocate and move old data to the new space
wy_alloc_n_move(newCapacity);
}
模板参数¶
类似函数参数的名字,一个模板参数的名字也没有什么内在含义。
我们通常将类型参数命名为T,但实际上我们可以使用任何名字:
template <typename Foo> Foo calc(const Foo& a, const Foo& b)
{
Foo tmp = a; // tmp has the same type as the parameters and return type
// ...
return tmp; // return type and parameters have the same type
}
模板参数与作用域¶
模板参数遵循普通的作用域规则。
一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。
与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。
但是,与大多数其他上下文不同,在模板内不能重用模板参数名:
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a; // tmp has same type as the template parameter A, not double
double B; // error: redeclares template parameter B
}
//正常的名字隐藏规则决定了A的typedef被类型参数A隐藏。因此,tmp不是一个double,
//其类型是使用f时绑定到类型参数A的类型。
//由于我们不能重用模板参数名,声明名字为B的变量是错误的。
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次:
// error: illegal reuse of template parameter name V
template <typename V, typename V> // ...
模板声明¶
模板声明必须包含模板参数:
// declares but does not define compare and Blob
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
与函数参数相同,声明中的模板参数的名字不必与定义中相同:
// all three uses of calc refer to the same function template
template <typename T> T calc(const T&, const T&); // declaration
template <typename U> U calc(const U&, const U&); // declaration
// definition of the template
template <typename Type>
Type calc(const Type& a, const Type& b) { /* . . . */ }
当然,一个给定模板的每个声明和定义必须有相同数量和种类(即,类型或非类型)的参数。
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,
出现于任何使用这些模板的代码之前
使用类的类型成员¶
回忆一下,我们用作用域运算符(::)来访问static成员和类型成员。
在普通(非模板)代码中,编译器掌握类的定义。
- 因此,它知道通过作用域运算符访问的名字是类型还是static成员。
- 例如string::size_type,编译器有string的定义,从而知道size_type是一个类型。
但对于模板代码就存在困难。
- 例如,假定T是一个模板类型参数,当编译器遇到类似T::mem这样的代码时,
它不会知道mem是一个类型成员还是一个static数据成员,直至实例化时才会知道。
但是,为了处理模板,编译器必须知道名字是否表示一个类型。
例如,假定T是一个类型参数的名字,当编译器遇到如下形式的语句时:
T::size_type * p;
它需要知道
- 我们是正在定义一个名为p的变量?
- 还是将一个名为size_type的static数据成员与名为p的变量相乘?
使用类的类型成员¶
默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。
如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型
我们通过使用关键字typename来实现这一点:
template <typename T>
typename T::value_type top(const T& c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}
//我们的top函数期待一个容器类型的实参,
//它使用typename指明其返回类型并在c中没有元素时生成一个值初始化的元素返回给调用者
当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。
默认模板实参¶
就像能为函数参数提供默认实参一样,也可以提供默认模板实参(default template argument)。
在新标准中,可以为函数和类模板提供默认实参。
而更早的C++标准只允许为类模板提供默认实参。
//- 例如,我们重写compare,默认使用标准库的less函数对象模板
// compare has a default template argument, less<T>
// and a default function argument, F()
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
//在这段代码中,我们为模板添加了第二个类型参数,名为F,表示可调用对象的类型;
//并定义了一个新的函数参数f,绑定到一个可调用对象上。
//我们为此模板参数提供了默认实参,并为其对应的函数参数也提供了默认实参。
//默认模板实参指出compare将使用标准库的less函数对象类,
//它是使用与compare一样的类型参数实例化的。
//默认函数实参指出f将是类型F的一个默认初始化的对象。
默认模板实参¶
//当用户调用这个版本的compare时,可以提供自己的比较操作,但这并不是必需的:
bool i = compare(0, 42); // uses less; i is -1
// result depends on the isbns in item1 and item2
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn);
//第一个调用使用默认函数实参,即,类型less<T>的一个默认初始化对象。
//在此调用中,T为int,因此可调用对象的类型为less<int>。
//compare的这个实例化版本将使用less<int>进行比较操作。
//在第二个调用中,我们传递给compare三个实参:compareIsbn和两个Sales_data类
//型的对象。当传递给compare三个实参时,第三个实参的类型必须是一个可调用对象,
//该可调用对象的返回类型必须能转换为bool值,且接受的实参类型必须与compare的
//前两个实参的类型兼容。与往常一样,模板参数的类型从它们对应的函数实参推断
//而来。在此调用中,T的类型被推断为Sales_data,F被推断为compareIsbn的类型。
与函数默认实参一样,对于一个模板参数,
只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
模板默认实参与类模板¶
无论何时使用一个类模板,我们都必须在模板名之后接上尖括号。
尖括号指出类必须从一个模板实例化而来。
特别是,如果一个类模板为其所有模板参数都提供了默认实参,
且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:
template <class T = int>
class Numbers { // by default T is int
public:
Numbers(T v = 0): val(v) { }
// various operations on numbers
private:
T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; // empty <> says we want the default type
//此例中我们实例化了两个Numbers版本:average_precision是用int代替T实例化得到的
//lots_of_precision是用long double代替T实例化而得到的。
练习¶
声明为 typename 的类型参数和声明为 class 的类型参数有什么不同(如果有的话)?
什么时候必须使用typename?
没有什么不同。当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename,
而不能使用 class。
练习¶
解释下面每个函数模版声明并指出它们是否非法。更正你发现的每个错误。
(a) template <typename T, U, typename V> void f1(T, U, V);
(b) template <typename T> T f2(int &T);
(c) inline template <typename T> T foo(T, unsigned int *);
(d) template <typename T> f4(T, T);
(e) typedef char Ctype;
template <typename Ctype> Ctype f5(Ctype a);
(a) 非法。应该为 template <typename T, typename U, typename V> void f1(T, U, V);。
(b) 非法。应该为 template <typename T> T f2(int &t);
(c) 非法。应该为 template <typename T> inline T foo(T, unsigned int*);
(d) 非法。应该为 template <typename T> T f4(T, T);
(e) 非法。Ctype 被隐藏了。
练习¶
编写函数,接受一个容器的引用,打印容器中的元素。
使用容器的 size_type 和 size成员来控制打印元素的循环。
template<typename Container>
void print(const Container& c)
{
for (typename Container::size_type i = 0; i != c.size(); ++i)
std::cout << c[i] << " ";
}
练习¶
重写上一题的函数,使用begin 和 end 返回的迭代器来控制循环。
template<typename Container>
void print(const Container& c)
{
for (auto it = c.begin(); it != c.end(); ++it)
std::cout << *it << " ";
}
成员模板¶
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。
这种成员被称为成员模板(member template)。成员模板不能是虚函数。
普通(非模板)类的成员模板¶
作为普通类包含成员模板的例子,我们定义一个类,
类似unique_ptr所使用的默认删除器类型。
类似默认删除器,我们的类将包含一个重载的函数调用运算符,
它接受一个指针并对此指针执行delete。
与默认删除器不同,我们的类还将在删除器被执行时打印一条信息。
由于希望删除器适用于任何类型,所以我们将调用运算符定义为一个模板:
class DebugDelete {
public:
DebugDelete(std::ostream &s = std::cerr): os(s) { }
// as with any function template, the type of T is deduced by the compiler
template <typename T> void operator()(T *p) const
{ os << "deleting unique_ptr" << std::endl; delete p;
}
private:
std::ostream &os;
};
普通(非模板)类的成员模板¶
与任何其他模板相同,成员模板也是以模板参数列表开始的。
每个DebugDelete对象都有一个ostream成员,用于写入数据;
还包含一个自身是模板的成员函数。我们可以用这个类代替delete:
double* p = new double;
DebugDelete d; // an object that can act like a delete expression
d(p); // calls DebugDelete::operator()(double*), which deletes p
int* ip = new int;
// calls operator()(int*) on a temporary DebugDelete object
DebugDelete()(ip);
由于调用一个DebugDelete对象会delete其给定的指针,
我们也可以将DebugDelete用作unique_ptr的删除器。
为了重载unique_ptr的删除器,我们在尖括号内给出删除器类型,
并提供一个这种类型的对象给unique_ptr的构造函数
// destroying the the object to which p points
// instantiates DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// destroying the the object to which sp points
// instantiates DebugDelete::operator()<string>(string*)
unique_ptr<string,DebugDelete> sp(new string,DebugDelete());
普通(非模板)类的成员模板¶
在本例中,我们声明p的删除器的类型为DebugDelete,
并在p的构造函数中提供了该类型的一个未命名对象。
unique_ptr的析构函数会调用DebugDelete的调用运算符。
因此,无论何时unique_ptr的析构函数实例化时,DebugDelete的调用运算符都会实例化:
因此,上述定义会这样实例化.
// sample instantiations for member templates of DebugDelete
void DebugDelete::operator()(int *p) const { delete p; }
void DebugDelete::operator()(string *p) const { delete p; }
类模板的成员模板¶
对于类模板,我们也可以为其定义成员模板。
在此情况下,类和成员各自有自己的、独立的模板参数。
例如,我们将为Blob类定义一个构造函数,它接受两个迭代器,表示要拷贝的元素范围。
由于我们希望支持不同类型序列的迭代器,因此将构造函数定义为模板:
template <typename T> class Blob {
template <typename It> Blob(It b, It e);
// ...
};
此构造函数有自己的模板类型参数It,作为它的两个函数参数的类型。
与类模板的普通函数成员不同,成员模板是函数模板。
当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。
类模板的参数列表在前,后跟成员自己的模板参数列表:
template <typename T> // type parameter for the class
template <typename It> // type parameter for the constructor
Blob<T>::Blob(It b, It e):
data(std::make_shared<std::vector<T>>(b, e)) {
}
在此例中,我们定义了一个类模板的成员,类模板有一个模板类型参数,命名为T。
而成员自身是一个函数模板,它有一个名为It的类型参数。
实例化与成员模板¶
为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参。
我们在哪个对象上调用成员模板,编译器就根据该对象的类型来推断类模板参数的实参。
与普通函数模板相同,编译器通常根据传递给成员模板的函数实参来推断它的模板实参
实例化与成员模板¶
int ia[] = {0,1,2,3,4,5,6,7,8,9};
vector<long> vi = {0,1,2,3,4,5,6,7,8,9};
list<const char*> w = {"now", "is", "the", "time"};
// instantiates the Blob<int> class
// and the Blob<int> constructor that has two int* parameters
Blob<int> a1(begin(ia), end(ia));
// instantiates the Blob<int> constructor that has
// two vector<long>::iterator parameters
Blob<int> a2(vi.begin(), vi.end());
// instantiates the Blob<string> class and the Blob<string>
// constructor that has two (list<const char*>::iterator parameters
blob<string> a3(w.begin(), w.end());
当我们定义a1时,显式地指出编译器应该实例化一个int版本的blob。
构造函数自己的类型参数则通过begin(ia)和end(ia)的类型来推断,结果为int*。
因此,a1的定义实例化了如下版本:
blob<int>::blob(int*, int*);
a2的定义使用了已经实例化了的blob<int>类,
并用vector<short>::iterator替换it来实例化构造函数。
a3的定义(显式地)实例化了一个string版本的blob,
并(隐式地)实例化了该类的成员模板构造函数,其模板参数被绑定到list<const char*>。
练习¶
编写你自己的 debugdelete 版本。
//debugdelete
#include <iostream>
class debugdelete
{
public:
debugdelete(std::ostream& s = std::cerr) : os(s) {}
template<typename t>
void operator() (t* p) const
{
os << "deleting unique_ptr" << std::endl;
delete p;
}
private:
std::ostream& os;
};
练习¶
修改12.3节中你的 textquery 程序,
令 shared_ptr 成员使用 debugdelete 作为它们的删除器。
对shared_ptr成员file的初始化进行修改,创建一个debugdelete 对象作为第二个参数
textquery::textquery(ifstream &is):
file(new vector<string>,debugdelete("shared_ptr"))
练习¶
预测在你的查询主程序中何时会执行调用运算符。
如果你的预测和实际不符,确认你理解了原因。
当shared_ptr 的引用计数变为0,需要释放资源时,才会调用删除器进行资源释放。
练习¶
为你的 blob 模版添加一个构造函数,它接受两个迭代器。
template<typename t> //for class
template<typename it> //for this member
blob<t>::blob(it b, it e) :
data(std::make_shared<std::vector<t>>(b, e))
{ }
控制实例化¶
当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个对象文件中。
当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,
- 每个文件中就都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。
在新标准中,我们可以通过显式实例化(explicit instantiation)来避免这种开销。
一个显式实例化有如下形式:
extern template declaration; // instantiation declaration
template declaration; // instantiation definition
declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。例如,
// instantion declaration and definition
extern template class blob<string>; // declaration
template int compare(const int&, const int&); // definition
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。
将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)
对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
控制实例化¶
由于编译器在使用一个模板时自动对其实例化,
因此extern声明必须出现在任何使用此实例化版本的代码之前:
// application.cc
// these template types must be instantiated elsewhere in the program
extern template class blob<string>;
extern template int compare(const int&, const int&);
blob<string> sa1, sa2; // instantiation will appear elsewhere
// blob<int> and its initializer_list constructor instantiated in this file
blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
blob<int> a2(a1); // copy constructor instantiated in this file
int i = compare(a1[0], a2[0]); // instantiation will appear elsewhere
控制实例化¶
//文件application.o将包含blob<int>的实例及其接受initializer_list参数的
//构造函数和拷贝构造函数的实例。而compare<int>函数和blob<string>类将不在
//本文件中进行实例化。这些模板的定义必须出现在程序的其他文件中:
// templatebuild.cc
// instantiation file must provide a (nonextern) definition for every
// type and function that other files declare as extern
template int compare(const int&, const int&);
template class blob<string>; // instantiates all members of the class template
//当编译器遇到一个实例化定义(与声明相对)时,它为其生成代码。因此,文件
//templatebuild.o将会包含compare的int实例化版本的定义和blob<string>类的定义。
//当我们编译此应用程序时,必须将templatebuild.o和application.o链接到一起。
对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。
实例化定义会实例化所有成员¶
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。
当编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数。
因此,与处理类模板的普通实例化不同,编译器会实例化该类的所有成员。
即使我们不使用某个成员,它也会被实例化。
因此,我们用来显式实例化一个类模板的类型,必须能用于模板的所有成员。
在一个类模板的实例化定义中,所用类型必须能用于模板的所有成员函数。
练习¶
解释下面这些声明的含义。
extern template class vector<string>;
template class vector<sales_data>;
前者是模版声明,后者是实例化定义。
练习¶
假设 nodefault 是一个没有默认构造函数的类,
我们可以显式实例化 vector<nodefualt>吗?如果不可以,解释为什么。
不可以。如
std::vector<nodefault> vec(10);
会使用 nodefault 的默认构造函数,而 nodefault 没有默认构造函数,因此是不可以的。
练习¶
对下面每条带标签的语句,解释发生了什么样的实例化(如果有的话)。
如果一个模版被实例化,解释为什么;如果未实例化,解释为什么没有。
template <typename t> class stack { };
void f1(stack<char>); //(a)
class exercise {
stack<double> &rds; //(b)
stack<int> si; //(c)
};
int main() {
stack<char> *sc; //(d)
f1(*sc); //(e)
int iobj = sizeof(stack<string>); //(f)
}
(a)、(b)、(c)、(f) 都发生了实例化,(d)、(e) 没有实例化。
效率与灵活性¶
对模板设计者所面对的设计选择,标准库智能指针类型给出了一个很好的展示。
shared_ptr和unique_ptr之间的明显不同是它们管理所保存的指针的策略
- 前者给予我们共享指针所有权的能力;后者则独占指针。
- 这一差异对两个类的功能来说是至关重要的。
这两个类的另一个差异是它们允许用户重载默认删除器的方式。
我们可以很容易地重载一个shared_ptr的删除器,
只要在创建或reset指针时传递给它一个可调用对象即可。
与之相反,删除器的类型是一个unique_ptr对象的类型的一部分。
用户必须在定义unique_ptr时以显式模板实参的形式提供删除器的类型。
因此,对于unique_ptr的用户来说,提供自己的删除器就更为复杂。
如何处理删除器的差异实际上就是这两个类功能的差异。
但是,如我们将要看到的,这一实现策略上的差异可能对性能有重要影响。
在运行时绑定删除器¶
虽然我们不知道标准库类型是如何实现的,但可以推断出,
shared_ptr必须能直接访问其删除器。
即,删除器必须保存为一个指针或一个封装了指针的类
我们可以确定shared_ptr不是将删除器直接保存为一个成员,
因为删除器的类型直到运行时才会知道。
实际上,在一个shared_ptr的生存期中,我们可以随时改变其删除器的类型。
我们可以使用一种类型的删除器构造一个shared_ptr,
随后使用reset赋予此shared_ptr另一种类型的删除器。
通常,类成员的类型在运行时是不能改变的。因此,不能直接保存删除器。
为了考察删除器是如何正确工作的,让我们假定shared_ptr将它管理的指针保存在
一个成员p中,且删除器是通过一个名为del的成员来访问的。
则shared_ptr的析构函数必须包含类似下面这样的语句:
// value of del known only at run time; call through a pointer
del ? del(p) : delete p; // del(p) requires run-time jump to del's location
由于删除器是间接保存的,调用del(p)需要一次运行时的跳转操作,
转到del中保存的地址来执行对应的代码。
在编译时绑定删除器¶
现在,让我们来考察unique_ptr可能的工作方式。
在这个类中,删除器的类型是类类型的一部分。
即,unique_ptr有两个模板参数,一个表示它所管理的指针,另一个表示删除器的类型。
由于删除器的类型是unique_ptr类型的一部分,
因此删除器成员的类型在编译时是知道的,从而删除器可以直接保存在unique_ptr对象中。
unique_ptr的析构函数与shared_ptr的析构函数类似,
也是对其保存的指针调用用户提供的删除器或执行delete:
// del bound at compile time; direct call to the deleter is instantiated
del(p); // no run-time overhead
del的类型或者是默认删除器类型,或者是用户提供的类型。
到底是哪种情况没有关系,应该执行的代码在编译时肯定会知道。
实际上,如果删除器是类似DebugDelete之类的东西,这个调用甚至可能被编译为内联形式。
通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销。
通过在运行时绑定删除器,shared_ptr使用户重载删除器更为方便。
练习¶
编写你自己版本的 shared_ptr 和 unique_ptr。
见book/ch16/ex16.28
练习¶
修改你的 Blob 类,用你自己的 shared_ptr 代替标准库中的版本。
见book/ch16/ex16.29
练习¶
重新运行你的一些程序,验证你的 shared_ptr 类和修改后的 Blob 类。
(注意:实现 weak_ptr 类型超出了本书范围,
因此你不能将BlobPtr类与你修改后的Blob一起使用。)
参考之前
练习¶
如果我们将 DebugDelete 与 unique_ptr 一起使用,
解释编译器将删除器处理为内联形式的可能方式。
编译器可能会去掉DebugDelete类对象和相关函数,替换为对应的输出语句和释放操作
模板实参推断¶
对于函数模板,编译器利用调用中的函数实参来确定其模板参数。
从函数实参来确定模板实参的过程被称为模板实参推断(template argumentdeduction)。
在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,
用这些模板实参生成的函数版本与给定的函数调用最为匹配。
类型转换与模板类型参数¶
与非模板函数一样,我们在一次调用中传递给函数模板的实参被用来初始化函数的形参。
如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。
只有很有限的几种类型转换会自动地应用于这些实参。
编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
顶层const无论是在形参中还是在实参中,都会被忽略。
在其他类型转换中,能在调用中应用于函数模板的包括如下两项。
- const转换:可以将一个非const对象的引用(或指针)
传递给一个const的引用(或指针)形参
- 数组或函数指针转换:如果函数形参不是引用类型,
则可以对数组或函数类型的实参应用正常的指针转换。
一个数组实参可以转换为一个指向其首元素的指针。
类似的,一个函数实参可以转换为一个该函数类型的指针
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,
都不能应用于函数模板。
类型转换与模板类型参数¶
//作为一个例子,考虑对函数fobj和fref的调用。
//fobj函数拷贝它的参数,而fref的参数是引用类型:
template <typename T> T fobj(T, T); // arguments are copied
template <typename T> T fref(const T&, const T&); // references
string s1("a value");
const string s2("another value");
fobj(s1, s2); // calls fobj(string, string); const is ignored
fref(s1, s2); // calls fref(const string&, const string&)
// uses premissible conversion to const on s1
//在第一对调用中,我们传递了一个string和一个const string。
//虽然这些类型不严格匹配,但两个调用都是合法的。在fobj调用中,实参被拷贝,
//因此原对象是否是const没有关系。在fref调用中,参数类型是const的引用。
//对于一个引用参数来说,转换为const是允许的,因此这个调用也是合法的。
类型转换与模板类型参数¶
//作为一个例子,考虑对函数fobj和fref的调用。
//fobj函数拷贝它的参数,而fref的参数是引用类型:
template <typename T> T fobj(T, T); // arguments are copied
template <typename T> T fref(const T&, const T&); // references
int a[10], b[42];
fobj(a, b); // calls f(int*, int*)
fref(a, b); // error: array types don't match
//在下一对调用中,我们传递了数组实参,两个数组大小不同,因此是不同类型。
//在fobj调用中,数组大小不同无关紧要。两个数组都被转换为指针。
//fobj中的模板类型为int*。但是,fref调用是不合法的。如果形参是一个引用,
//则数组不会转换为指针。a和b的类型是不匹配的,因此调用是错误的。
将实参传递给带模板类型的函数形参时,
能够自动应用的类型转换只有const转换及数组或函数到指针的转换。
使用相同模板参数类型的函数形参¶
一个模板类型参数可以用作多个函数形参的类型。
由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。
如果推断出的类型不匹配,则调用就是错误的。
- 例如,我们的compare函数接受两个const T&参数,其实参必须是相同类型:
long lng;
compare(lng, 1024); // error: cannot instantiate compare(long, int)
//此调用是错误的,因为传递给compare的实参类型不同。
//从第一个函数实参推断出的模板实参为long,从第二个实参推断出的模板实参为int。
//这些类型不匹配,因此模板实参推断失败。
如果希望允许对函数实参进行正常的类型转换,我们可以将函数模板定义为两个类型参数:
// argument types can differ but must be compatible template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2)
{
if (v2 < v1) return 1;
if (v1 < v2) return -1; return 0;
}
现在用户可以提供不同类型的实参了:
long lng;
flexibleCompare(lng, 1024); // ok: calls flexibleCompare(long, int)
当然,必须定义了能比较这些类型的值的<运算符。
正常类型转换应用于普通函数实参¶
函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。
这种函数实参不进行特殊处理;它们正常转换为对应形参的类型。
例如,考虑下面的模板:
template <typename T> ostream &print(ostream &os, const T &obj)
{ return os << obj;}
//第一个函数参数是一个已知类型ostream&。第二个参数obj则是模板参数类型。
//由于os的类型是固定的,因此调用print时,传递给它的实参会进行正常的类型转换:
print(cout, 42); // instantiates print(ostream&, int)
ofstream f("output");
print(f, 10); // uses print(ostream&, int); converts f to ostream&
//在第一个调用中,第一个实参的类型严格匹配第一个参数的类型。
//此调用会实例化接受一个ostream&和一个int的print版本。
//在第二个调用中,第一个实参是一个ofstream,它可以转换为ostream&。
//由于此参数的类型不依赖于模板参数,因此编译器会将f隐式转换为ostream&。
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
练习¶
在模版实参推断过程中发生了什么?
在模版实参推断过程中,编译器使用函数调用中的实参类型来寻找模版实参,
用这些模版实参生成的函数版本与给定的函数调用最为匹配。
练习¶
指出在模版实参推断过程中允许对函数实参进行的两种类型转换。
- const 转换:
可以将一个非 const 对象的引用(或指针)
传递给一个 const 的引用(或指针)形参。
- 数组或函数指针转换:
如果函数形参不是引用类型,则可以对数组或函数类型的
实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。
类似的,一个函数实参可以转换为一个该函数类型的指针。
练习¶
对下面的代码解释每个调用是否合法。
如果合法,T 的类型是什么?如果不合法,为什么?
template <class T> int compare(const T&, const T&);
(a) compare("hi", "world");
(b) compare("bye", "dad");
(a) 不合法。compare(const char [3], const char [6]), 两个实参类型不一致。
(b) 合法。compare(const char [4], const char [4]).
练习¶
下面调用中哪些是错误的(如果有的话)?
如果调用合法,T 的类型是什么?如果调用不合法,问题何在?
template <typename T> T calc(T, int);
tempalte <typename T> T fcn(T, T);
double d; float f; char c;
(a) calc(c, 'c');
(b) calc(d, f);
(c) fcn(c, 'c');
(d) fcn(d, f);
(a) 合法,类型为char
(b) 合法,类型为double
(c) 合法,类型为char
(d) 不合法,这里无法确定T的类型是float还是double
练习¶
进行下面的调用会发生什么:
template <typename T> f1(T, T);
template <typename T1, typename T2) f2(T1, T2);
int i = 0, j = 42, *p1 = &i, *p2 = &j;
const int *cp1 = &i, *cp2 = &j;
(a) f1(p1, p2);
(b) f2(p1, p2);
(c) f1(cp1, cp2);
(d) f2(cp1, cp2);
(e) f1(p1, cp1);
(f) f2(p1, cp1);
(a) f1(int*, int*);
(b) f2(int*, int*);
(c) f1(const int*, const int*);
(d) f2(const int*, const int*);
(e) f1(int*, const int*); 这个使用就不合法
(f) f2(int*, const int*);
函数模板显式实参¶
在某些情况下,编译器无法推断出模板实参的类型。
其他一些情况下,我们希望允许用户控制模板实例化。
当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
指定显式模板实参¶
作为一个允许用户指定使用类型的例子,
我们将定义一个名为sum的函数模板,它接受两个不同类型的参数。
我们希望允许用户指定结果的类型。这样,用户就可以选择合适的精度。
我们可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型:
// T1 cannot be deduced: it doesn't appear in the function parameter list
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
在本例中,没有任何函数实参的类型可用来推断T1的类型。每次调用sum时
调用者都必须为T1提供一个显式模板实参(explicit template argument)。
我们提供显式模板实参的方式与定义类模板实例的方式相同。
显式模板实参在尖括号中给出,位于函数名之后,实参列表之前:
// T1 is explicitly specified; T2 and T3 are inferred from the argument types
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
此调用显式指定T1的类型。而T2和T3的类型则由编译器从i和lng的类型推断出来。
指定显式模板实参¶
显式模板实参按由左至右的顺序与对应的模板参数匹配;
- 第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,依此类推。
只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
如果我们的sum函数按照如下形式编写:
// poor design: users must explicitly specify all three template parameters
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
则我们总是必须为所有三个形参指定实参:
// error: can't infer initial template parameters
auto val3 = alternative_sum<long long>(i, lng);
// ok: all three parameters are explicitly specified
auto val2 = alternative_sum<long long, int, long>(i, lng);
正常类型转换应用于显式指定的实参¶
对于用普通类型定义的函数参数,允许进行正常的类型转换,
出于同样的原因,对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换:
long lng;
compare(lng, 1024); // error: template parameters don't match
compare<long>(lng, 1024); // ok: instantiates compare(long, long)
compare<int>(lng, 1024); // ok: instantiates compare(int, int)
//如我们所见,第一个调用是错误的,因为传递给compare的实参必须具有相同的类型。
//如果我们显式指定模板类型参数,就可以进行正常类型转换了。
//因此,调用compare<long>等价于调用一个接受两个const long&参数的函数。
//int类型的参数被自动转化为long。
//在第三个调用中,T被显式指定为int,因此lng被转换为int。
练习¶
标准库 max 函数有两个参数,它返回实参中的较大者。此函数有一个模版类型参数。
你能在调用 max 时传递给它一个 int 和一个 double 吗?
如果可以,如何做?如果不可以,为什么?
可以。提供显式的模版实参:
int a = 1;
double b = 2;
std::max<double>(a, b);
练习¶
当我们调用 make_shared 时,必须提供一个显示模版实参。
解释为什么需要显式模版实参以及它是如果使用的。
如果不显示提供模版实参,那么 make_shared 无法推断要分配多大内存空间。
练习¶
对16.1.1节 中的原始版本的 compare 函数,
使用一个显式模版实参,使得可以向函数传递两个字符串字面量。
compare<std::string>("hello", "world")
尾置返回类型与类型转换¶
当我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型是很有效的。
但在其他情况下,要求显式指定模板实参会给用户增添额外负担,而且不会带来什么好处。
我们可能希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:
template <typename It>
??? &fcn(It beg, It end)
{
// process the range
return *beg; // return a reference to an element from the range
}
我们并不知道返回结果的准确类型,但知道所需类型是所处理的序列的元素类型:
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = { "hi", "bye" };
auto &i = fcn(vi.begin(), vi.end()); // fcn should return int&
auto &s = fcn(ca.begin(), ca.end()); // fcn should return string&
尾置返回类型与类型转换¶
此例中,我们知道函数应该返回*beg,
而且知道我们可以用decltype(*beg)来获取此表达式的类型。
但是,在编译器遇到函数的参数列表之前,beg都是不存在的。
为了定义此函数,我们必须使用尾置返回类型。
由于尾置返回出现在参数列表之后,它可以使用函数的参数:
// a trailing return lets us declare the return type after the parameter list is seen
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
// process the range
return *beg; // return a reference to an element from the range
}
//此例中我们通知编译器fcn的返回类型与解引用beg参数的结果类型相同。
//解引用运算符返回一个左值,
//因此通过decltype推断的类型为beg表示的元素的类型的引用。
//因此,如果对一个string序列调用fcn,返回类型将是string&。
//如果是int序列,则返回类型是int&。
进行类型转换的标准库模板类¶
有时我们无法直接获得所需要的类型。
- 例如,我们可能希望编写一个类似fcn的函数,但返回一个元素的值而非引用。
在编写这个函数的过程中,我们面临一个问题:
- 对于传递的参数的类型,我们几乎一无所知。
在此函数中,我们知道唯一可以使用的操作是迭代器操作,
而所有迭代器操作都不会生成元素,只能生成元素的引用。
为了获得元素类型,我们可以使用标准库的类型转换(type transformation)模板。
这些模板定义在头文件type_traits中。
这个头文件中的类通常用于所谓的模板元程序设计。
但是,类型转换模板在普通编程中也很有用。表16.1列出了这些模板,
后续看到它们是如何实现的。
进行类型转换的标准库模板类¶
在本例中,我们可以使用remove_reference来获得元素类型。
remove_reference模板有一个模板类型参数和一个名为type的(public)类型成员。
如果我们用一个引用类型实例化remove_reference,则type将表示被引用的类型。
例如,如果我们实例化remove_reference<int&>,则type成员将是int。
类似的,如果我们实例化remove_reference<string&>,则type成员将是string,
依此类推。更一般的,给定一个迭代器beg:
remove_reference<decltype(*beg)>::type
将获得beg引用的元素的类型:decltype(*beg)返回元素类型的引用类型。
remove_reference::type脱去引用,剩下元素类型本身。
进行类型转换的标准库模板类¶
组合使用remove_reference、尾置返回及decltype,就可以在函数中返回元素值的拷贝:
// must use typename to use a type member of a template parameter; see § 16.1.3
template <typename It>
auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
// process the range
return *beg; // return a copy of an element from the range
}
注意,type是一个类的成员,而该类依赖于一个模板参数。
因此,我们必须在返回类型的声明中使用typename来告知编译器,type表示一个类型
进行类型转换的标准库模板类¶
表16.1中描述的每个类型转换模板的工作方式都与remove_reference类似。
每个模板都有一个名为type的public成员,表示一个类型。
此类型与模板自身的模板类型参数相关,其关系如模板名所示。
如果不可能(或者不必要)转换模板参数,则type成员就是模板参数类型本身。
例如,如果T是一个指针类型,则remove_pointer<T>::type是T指向的类型。
如果T不是一个指针,则无须进行任何转换,从而type具有与T相同的类型。
练习¶
下面的函数是否合法?如果不合法,为什么?
如果合法,对可以传递的实参类型有什么限制(如果有的话)?返回类型是什么?
template <typename It>
auto fcn3(It beg, It end) -> decltype(*beg + 0)
{
//处理序列
return *beg;
}
合法。该类型需要支持 + 操作。
练习¶
编写一个新的 sum 版本,它返回类型保证足够大,足以容纳加法结果。
template<typename T>
auto sum(T lhs, T rhs) -> decltype( lhs + rhs)
{
return lhs + rhs;
}
函数指针和实参推断¶
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,
编译器使用指针的类型来推断模板实参。
//例如,假定我们有一个函数指针,它指向的函数返回int,接受两个参数,
//每个参数都是指向const int的引用。我们可以使用该指针指向compare的一个实例:
template <typename T> int compare(const T&, const T&);
// pf1 points to the instantiation int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
//pf1中参数的类型决定了T的模板实参的类型。
//在本例中,T的模板实参类型为int。指针pf1指向compare的int版本实例。
如果不能从函数指针类型确定模板实参,则产生错误:
// overloaded versions of func; each takes a different function pointer type
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // error: which instantiation of compare?
//这段代码的问题在于,通过func的参数类型无法确定模板实参的唯一类型。
//对func的调用既可以实例化接受int的compare版本,也可以实例化接受string的版本。
//由于不能确定func的实参的唯一实例化版本,此调用将编译失败。
函数指针和实参推断¶
我们可以通过使用显式模板实参来消除func调用的歧义:
// ok: explicitly specify which version of compare to instantiate
func(compare<int>); // passing compare(const int&, const int&)
//此表达式调用的func版本接受一个函数指针,该指针指向的函数接受两个constint&参数。
当参数是一个函数模板实例的地址时,程序上下文必须满足:
对每个模板参数,能唯一确定其类型或值。
模板实参推断和引用¶
为了理解如何从函数调用进行类型推断,考虑下面的例子:
template <typename T> void f(T &p);
其中函数参数p是一个模板类型参数T的引用,非常重要的是记住两点:
编译器会应用正常的引用绑定规则;const是底层的,不是顶层的。
从左值引用函数参数推断类型¶
当一个函数参数是模板类型参数的一个普通(左值)引用时(即,形如T&),
绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。
实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型:
template <typename T> void f1(T&); // argument must be an lvalue
// calls to f1 use the referred-to type of the argument as the template parameter type
f1(i); // i is an int; template parameter T is int
f1(ci); // ci is a const int; template parameter T is const int
f1(5); // error: argument to a & parameter must be an lvalue
如果一个函数参数的类型是const T&,
正常的绑定规则告诉我们可以传递给它任何类型的实参
- 一个对象(const或非const)、一个临时对象或是一个字面常量值。
当函数参数本身是const时,T的类型推断的结果不会是一个const类型。
const已经是函数参数类型的一部分;因此,它不会也是模板参数类型的一部分:
template <typename T> void f2(const T&); // can take an rvalue
// parameter in f2 is const &; const in the argument is irrelevant
// in each of these three calls, f2's function parameter is inferred as const int&
f2(i); // i is an int; template parameter T is int
f2(ci); // ci is a const int, but template parameter T is int
f2(5); // a const & parameter can be bound to an rvalue; T is int
从右值引用函数参数推断类型¶
当一个函数参数是一个右值引用(即,形如T&&)时,
正常绑定规则告诉我们可以传递给它一个右值。
当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。
推断出的T的类型是该右值实参的类型:
template <typename T> void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int
引用折叠和右值引用参数¶
假定i是一个int对象,我们可能认为像f3(i)这样的调用是不合法的。
毕竟,i是一个左值,而通常我们不能将一个右值引用绑定到一个左值上。
但是,C++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。
- 这两个例外规则是move这种标准库设施正确工作的基础。
第一个例外规则影响右值引用参数的推断如何进行。
当我们将一个左值(如i)传递给函数的右值引用参数,且此右值引用
指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。
因此,当我们调用f3(i)时,编译器推断T的类型为int&,而非int。
T被推断为int&看起来好像意味着f3的函数参数应该是一个类型int&的右值引用。
通常,我们不能(直接)定义一个引用的引用。
但是,通过类型别名或通过模板类型参数间接定义是可以的。
引用折叠和右值引用参数¶
在这种情况下,我们可以使用第二个例外绑定规则:
如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。
- 在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。
在新标准中,折叠规则扩展到右值引用。
- 只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。
即,对于一个给定类型X:
- X&&、X&&&和X&&&都折叠成类型X&·
- 类型X&&&&折叠成X&&
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
如果将引用折叠规则和右值引用的特殊类型推断规则组合在一起,
则意味着我们可以对一个左值调用f3。当我们将一个左值传递给
f3的(右值引用)函数参数时,编译器推断T为一个左值引用类型:
f3(i); // argument is an lvalue; template parameter T is int&
f3(ci); // argument is an lvalue; template parameter T is const int&
引用折叠和右值引用参数¶
当一个模板参数T被推断为引用类型时,
折叠规则告诉我们函数参数T&&折叠为一个左值引用类型。
例如,f3(i)的实例化结果可能像下面这样:
// invalid code, for illustration purposes only
void f3<int&>(int& &&); // when T is int&, function parameter is int& &&
//f3的函数参数是T&&且T是int&,因此T&&是int&&&,会折叠成int&。
//因此,即使f3的函数参数形式是一个右值引用(即,T&&),
//此调用也会用一个左值引用类型(即,int&)实例化f3:
void f3<int&>(int&); // when T is int&, function parameter collapses to int&
引用折叠和右值引用参数¶
这两个规则导致了两个重要结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),
则它可以被绑定到一个左值;且
- 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,
且函数参数将被实例化为一个(普通)左值引用参数(T&)
这两个规则暗示,我们可以将任意类型的实参传递给T&&类型的函数参数。对于这种
类型的参数,(显然)可以传递给它右值,而如我们刚刚看到的,也可以传递给它左值。
如果一个函数参数是指向模板参数类型的右值引用(如,T&&),
则可以传递给它任意类型的实参。
如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。
编写接受右值引用参数的模板函数¶
模板参数可以推断为一个引用类型,这一特性对模板内的代码可能有令人惊讶的影响:
template <typename T> void f3(T&& val)
{
T t = val; // copy or binding a reference?
t = fcn(t); // does the assignment change only t or val and t?
if (val == t) { /* ... */ } // always true if T is a reference type
}
当我们对一个右值调用f3时,例如字面常量42,T为int。
在此情况下,局部变量t的类型为int,且通过拷贝参数val的值被初始化。
当我们对t赋值时,参数val保持不变。
另一方面,当我们对一个左值i调用f3时,则T为int&。
当我们定义并初始化局部变量t时,赋予它类型int&。
因此,对t的初始化将其绑定到val。
当我们对t赋值时,也同时改变了val的值。
在f3的这个实例化版本中,if判断永远得到true。
当代码中涉及的类型可能是普通(非引用)类型,也可能是引用类型时,
编写正确的代码就变得异常困难(虽然remove_reference这样的类型转换类可能会有帮助)
编写接受右值引用参数的模板函数¶
在实际中,右值引用通常用于两种情况:
- 模板转发其实参或模板被重载。
使用右值引用的函数模板通常使用在13.6.3节中看到的方式来进行重载:
template <typename T> void f(T&&); // binds to nonconst rvalues
template <typename T> void f(const T&); // lvalues and const rvalues
与非模板函数一样,
第一个版本将绑定到可修改的右值,而第二个版本将绑定到左值或const右值。
练习¶
对下面每个调用,确定 T 和 val 的类型:
template <typename T> void g(T&& val);
int i = 0; const int ci = i;
(a) g(i);
(b) g(ci);
(c) g(i * ci);
(a)T,val: int&
(b)T,val: const int&
(c)T:int
val: int&&
练习¶
使用上一题定义的函数,如果我们调用g(i = ci),g 的模版参数将是什么?
T : int &
val : int &
i = ci 返回的是左值,因此 g 的模版参数是 int&
练习¶
使用与第一题中相同的三个调用,如果 g 的函数参数声明为 T(而不是T&&),
确定T的类型。如果g的函数参数是 const T&呢?
template<typename T> void g(T val);
(a) T : int val : int
(b) T : int val : int
(c) T : int val : int
template<typename T> void g(const T & val);
(a) T : int val : const int &
(b) T : int val : const int &
(c) T : int val : const int &
练习¶
如果下面的模版,如果我们对一个像42这样的字面常量调用g,
解释会发生什么?如果我们对一个int 类型的变量调用g 呢?
template <typename T> void g(T&& val) { vector<T> v; }
当使用字面常量,T为int,val为int && 类型。
当使用int变量,T将为int&,val折叠为int&.
vector管理动态空间,需要维护指针,而不能定义指向int&的指针
编译的时候将会报错,因为没有办法对这种类型进行内存分配,无法创建vector<int&>。
理解std::move¶
标准库move函数是使用右值引用的模板的一个很好的例子。
幸运的是,我们不必理解move所使用的模板机制也可以直接使用它。
但是,研究move是如何工作的可以帮助我们巩固对模板的理解和使用。
虽然不能直接将一个右值引用绑定到一个左值上,
但可以用move获得一个绑定到左值上的右值引用。
由于move本质上可以接受任何类型的实参,因此我们不会惊讶于它是一个函数模板。
std::move是如何定义的¶
标准库是这样定义move的:
// for the use of typename in the return type and the cast see § 16.1.3
// remove_reference is covered in § 16.2.3
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
// static_cast covered in § 4.11.3
return static_cast<typename remove_reference<T>::type&&>(t);
}
这段代码很短,但其中有些微妙之处。
首先,move的函数参数T&&是一个指向模板类型参数的右值引用。
通过引用折叠,此参数可以与任何类型的实参匹配。
特别是,我们既可以传递给move一个左值,也可以传递给它一个右值:
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1); // ok: but after the assigment s1 has indeterminate value
std::move是如何工作的¶
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1); // ok: but after the assigment s1 has indeterminate value
在第一个赋值中,传递给move的实参是string的构造函数的右值结果——string("bye!")
当向一个右值引用函数参数传递一个右值时,由实参推断出的类型为被引用的类型
因此,在std::move(string("bye!"))中:
- 推断出的T的类型为string。·
- 因此,remove_reference用string进行实例化。·
- remove_reference<string>的type成员是string。·
- move的返回类型是string&&。·
- move的函数参数t的类型为string&&。
因此,这个调用实例化move<string>,即函数
string&& move(string &&t)
函数体返回static_cast<string&&>(t)。
t的类型已经是string&&,于是类型转换什么都不做。
因此,此调用的结果就是它所接受的右值引用。
std::move是如何工作的¶
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1); // ok: but after the assigment s1 has indeterminate value
现在考虑第二个赋值,它调用了std::move()。
在此调用中,传递给move的实参是一个左值。这样:·
- 推断出的T的类型为string&(string的引用,而非普通string)。·
- 因此,remove_reference用string&进行实例化。·
- remove_reference<string&>的type成员是string。·
- move的返回类型仍是string&&。·
- move的函数参数t实例化为string&&&,会折叠为string&。
因此,这个调用实例化move<string&>,即
这正是我们所寻求的——我们希望将一个右值引用绑定到一个左值。
这个实例的函数体返回static_cast<string&&>(t)。
在此情况下,t的类型为string&,cast将其转换为string&&。
从一个左值static_cast到一个右值引用是允许的¶
通常情况下,static_cast只能用于其他合法的类型转换。
但是,这里又有一条针对右值引用的特许规则:
- 虽然不能隐式地将一个左值转换为右值引用,
但我们可以用static_cast显式地将一个左值转换为一个右值引用。
对于操作右值引用的代码来说,将一个右值引用绑定到一个左值的特性允许它们截断左值
有时候,例如在我们的StrVec类的reallocate函数中,我们知道截断一个左值是安全的
- 一方面,通过允许进行这样的转换,C++语言认可了这种用法。
- 但另一方面,通过强制使用static_cast,C++语言试图阻止我们意外地进行这种转换。
虽然我们可以直接编写这种类型转换代码,但使用标准库move函数是容易得多的方式。
而且,统一使用std::move使得我们在程序中查找潜在的截断左值的代码变得很容易。
练习¶
解释下面的循环,它来自13.5节中的 StrVec::reallocate:
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
elem是string * 类型,*elem++是令elem指向下一个位置,
同时返回当前位置对象的左值引用。
然后用std::move函数令左值类型转换为右值引用,再传参给alloc.construct函数,
令其利用这个右值移动到新的内存中。
转发¶
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。
在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的
以及实参是左值还是右值。
作为一个例子,我们将编写一个函数,它接受一个可调用表达式和两个额外实参。
我们的函数将调用给定的可调用对象,将两个额外参数逆序传递给它。
下面是我们的翻转函数的初步模样:
// template that takes a callable and two parameters
// and calls the given callable with the parameters ''flipped''
// flip1 is an incomplete implementation: top-level const and references are lost
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
这个函数一般情况下工作得很好,
但当我们希望用它调用一个接受引用参数的函数时就会出现问题:
转发¶
void f(int v1, int &v2) // note v2 is a reference
{
cout << v1 << " " << ++v2 << endl;
}
在这段代码中,f改变了绑定到v2的实参的值。
但是,如果我们通过flip1调用f,f所做的改变就不会影响实参:
f(42, i); // f changes its argument i
flip1(f, j, 42); // f called through flip1 leaves j unchanged
问题在于j被传递给flip1的参数t1。
此参数是一个普通的、非引用的类型int,而非int&。因此,这个flip1调用会实例化为
void flip1(void(*fcn)(int, int&), int t1, int t2);
j的值被拷贝到t1中。f中的引用参数被绑定到t1,而非j,从而其改变不会影响j。
定义能保持类型信息的函数参数¶
为了通过翻转函数传递一个引用,我们需要重写函数,
使其参数能保持给定实参的“左值性”。
更进一步,可以想到我们也希望保持参数的const属性。
通过将一个函数参数定义为一个指向模板类型参数的右值引用,
我们可以保持其对应实参的所有类型信息。
而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,
因为在引用类型中的const是底层的。
如果我们将函数参数定义为T1&&和T2&&,
通过引用折叠就可以保持翻转实参的左值/右值属性:
定义能保持类型信息的函数参数¶
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
与较早的版本一样,如果我们调用flip2(f,j,42),将传递给参数t1一个左值j。
但是,在flip2中,推断出的T1的类型为int&,这意味着t1的类型会折叠为int&。
由于是引用类型,t1被绑定到j上。当flip2调用f时,f中的引用参数v2被绑定到t1,
也就是被绑定到j。当f递增v2时,它也同时改变了j的值。
如果一个函数参数是指向模板类型参数的右值引用(如T&&),
它对应的实参的const属性和左值/右值属性将得到保持。
定义能保持类型信息的函数参数¶
这个版本的flip2解决了一半问题。它对于接受一个左值引用的函数工作得很好,
但不能用于接受右值引用参数的函数。例如:
void g(int &&i, int& j)
{
cout << i << " " << j << endl;
}
如果我们试图通过flip2调用g,则参数t2将被传递给g的右值引用参数。
即使我们传递一个右值给flip2:
flip2(g, i, 42); // error: can't initialize int&& from an lvalue
传递给g的将是flip2中名为t2的参数。函数参数与其他任何变量一样,都是左值表达式
因此,flip2中对g的调用将传递给g的右值引用参数一个左值。
在调用中使用std::forward保持类型信息¶
我们可以使用一个名为forward的新标准库设施来传递flip2的参数,
它能保持原始实参的类型。类似move,forward定义在头文件utility中。
与move不同,forward必须通过显式模板实参来调用。
forward返回该显式实参类型的右值引用。即,forward<T>的返回类型是T&&。
通常情况下,我们使用forward传递那些定义为模板类型参数的右值引用的函数参数。
通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性:
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
// ...
}
本例中我们使用Type作为forward的显式模板实参类型,它是从arg推断出来的。
由于arg是一个模板类型参数的右值引用,Type将表示传递给arg的实参的所有类型信息。
- 如果实参是一个右值,则Type是一个普通(非引用)类型,forward<Type>将返回Type&&。
- 如果实参是一个左值,则通过引用折叠,Type本身是一个左值引用类型。
在此情况下,返回类型是一个指向左值引用类型的右值引用。
再次对forward<Type>的返回类型进行引用折叠,将返回一个左值引用类型。
在调用中使用std::forward保持类型信息¶
当用于一个指向模板参数类型的右值引用函数参数(T&&)时,
forward会保持实参类型的所有细节。
使用forward,我们可以再次重写翻转函数:
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
如果我们调用flip(g,i,42),i将以int&类型传递给g,42将以int&&类型传递给g。
与std::move相同,对std::forward不使用using声明是一个好主意
练习¶
编写你自己版本的翻转函数,通过调用接受左值和右值引用参数的函数来测试它。
template<typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
重载与模板¶
函数模板可以被另一个模板或一个普通非模板函数重载。
与往常一样,名字相同的函数必须具有不同数量或类型的参数。
如果涉及函数模板,则函数匹配规则会在以下几方面受到影响:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。·
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。·
- 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序
当然,可以用于函数模板调用的类型转换是非常有限的。·
- 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。
但是,如果有多个函数提供同样好的匹配,则
- 如果同样好的函数中只有一个是非模板函数,则选择此函数。
- 如果同样好的函数中没有非模板函数,而有多个函数模板,
且其中一个模板比其他模板更特例化,则选择此模板。
- 否则,此调用有歧义。
正确定义一组重载的函数模板
需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。
编写重载模板¶
作为一个例子,我们将构造一组函数,它们在调试中可能很有用。
我们将这些调试函数命名为debug_rep,每个函数都返回一个给定对象的string表示。
我们首先编写此函数的最通用版本,将它定义为一个模板,接受一个const对象的引用:
// print any type we don't otherwise handle
template <typename T> string debug_rep(const T &t)
{
ostringstream ret; // see § 8.3
ret << t; // uses T's output operator to print a representation of t
return ret.str(); // return a copy of the string to which ret is bound
}
此函数可以用来生成一个对象对应的string表示,
该对象可以是任意具备输出运算符的类型。
编写重载模板¶
接下来,我们将定义打印指针的debug_rep版本:
// print pointers as their pointer value,
//followed by the object to which the pointer points
// NB: this function will not work properly with char*; see § 16.3 (p. 698)
template <typename T> string debug_rep(T *p)
{
ostringstream ret;
ret << "pointer: " << p; // print the pointer's own value
if (p)
ret << " " << debug_rep(*p); // print the value to which p points
else
ret << " null pointer"; // or indicate that the p is null
return ret.str(); // return a copy of the string to which ret is bound
}
此版本生成一个string,包含指针本身的值和调用debug_rep获得的指针指向的值。
注意此函数不能用于打印字符指针,因为IO库为char*值定义了一个<<版本。
此<<版本假定指针表示一个空字符结尾的字符数组,并打印数组的内容而非地址值。
编写重载模板¶
我们可以这样使用这些函数:
string s("hi");
cout << debug_rep(s) << endl;
对于这个调用,只有第一个版本的debug_rep是可行的。
第二个debug_rep版本要求一个指针参数,但在此调用中我们传递的是一个非指针对象。
因此编译器无法从一个非指针实参实例化一个期望指针类型参数的函数模板,
因此实参推断失败。由于只有一个可行函数,所以此函数被调用。
如果我们用一个指针调用debug_rep:
cout << debug_rep(&s) << endl;
两个函数都生成可行的实例:·
- debug_rep(const string*&),
由第一个版本的debug_rep实例化而来,T被绑定到string*。·
- debug_rep(string*),
由第二个版本的debug_rep实例化而来,T被绑定到string。
第二个版本的debug_rep的实例是此调用的精确匹配。
第一个版本的实例需要进行普通指针到const指针的转换。
正常函数匹配规则告诉我们应该选择第二个模板,实际上编译器确实选择了这个版本。
多个可行模板¶
作为另外一个例子,考虑下面的调用:
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的指针的调用永远是有歧义的。
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。
非模板和模板重载¶
作为下一个例子,
我们将定义一个普通非模板版本的debug_rep来打印双引号包围的string:
// print strings inside double quotes
string debug_rep(const string &s)
{
return '"' + s + '"';
}
现在,当我们对一个string调用debug_rep时:
string s("hi");
cout << debug_rep(s) << endl;
有两个同样好的可行函数:·
- debug_rep<string>(const string&),第一个模板,T被绑定到string*。·
- debug_rep(const string&),普通非模板函数。
在本例中,两个函数具有相同的参数列表,因此显然两者提供同样好的匹配。
但是,编译器会选择非模板版本。当存在多个同样好的函数模板时,
编译器选择最特例化的版本,出于相同的原因,一个非模板函数比一个函数模板更好。
对于一个调用,
如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
重载模板和类型转换¶
还有一种情况我们到目前为止尚未讨论:C风格字符串指针和字符串字面常量。
现在有了一个接受string的debug_rep版本,我们可能期望一个传递字符串的调用会
匹配这个版本。但是,考虑这个调用:
cout << debug_rep("hi world!") << endl; // calls 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处理,可以定义另外两个非模板重载版本:
// convert the character pointers to string and call the string version of debug_rep
string debug_rep(char *p)
{
return debug_rep(string(p));
}
string debug_rep(const char *p)
{
return debug_rep(string(p));
}
缺少声明可能导致程序行为异常¶
值得注意的是,为了使char*版本的debug_rep正确工作,在定义此版本时,
debug_rep(const string&)的声明必须在作用域中。
否则,就可能调用错误的debug_rep版本
template <typename T> string debug_rep(const T &t);
template <typename T> string debug_rep(T *p);
// the following declaration must be in scope
// for the definition of debug_rep(char*) to do the right thing
string debug_rep(const string &);
string debug_rep(char *p)
{
// if the declaration for the version that takes a const string& is not in scope
// the return will call debug_rep(const T&) with T instantiated to string
return debug_rep(string(p));
}
通常,如果使用了一个忘记声明的函数,代码将编译失败。
但对于重载函数模板的函数而言,则不是这样。
如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不重要了。
在本例中,如果忘记了声明接受string参数的debug_rep版本,
编译器会默默地实例化接受const T&的模板版本。
在定义任何函数之前,记得声明所有重载的函数版本。
这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。
练习¶
编写你自己版本的 debug_rep 函数。
template<typename T> std::string debug_rep(const T& t)
{
std::ostringstream ret;
ret << t;
return ret.str();
}
template<typename T> std::string debug_rep(T* p)
{
std::ostringstream ret;
ret << "pointer: " << p;
if(p)
ret << " " << debug_rep(*p);
else
ret << " null pointer";
return ret.str();
}
练习¶
解释下面每个调用会发生什么:
template <typename T> void f(T);
template <typename T> void f(const T*);
template <typename T> void g(T);
template <typename T> void g(T*);
int i = 42, *p = &i;
const int ci = 0, *p2 = &ci;
g(42); g(p); g(ci); g(p2);
f(42); f(p); f(ci); f(p2);
g(42); //g(T) ,T:int
g(p); //g(T*),T:int
g(ci); //g(T) ,T:int
g(p2); //g(T*),T:const int
f(42); //f(T) ,T:int
f(p); //f(T) ,T:int*
f(ci); //f(T) ,T:int
f(p2); //f(const T*),T:int
练习¶
定义上一个练习中的函数,令它们打印一条身份信息。
运行该练习中的代码。如果函数调用的行为与你预期不符,确定你理解了原因。
#include<iostream>
#include<string>
template <typename T>
void f(T t)
{
std::cout << "void f(T t)" << std::endl;
}
template <typename T>
void f(const T * t)
{
std::cout << "void f(const T * t)" << std::endl;
}
练习¶
template <typename T>
void g(T t)
{
std::cout << "void g(T t)" << std::endl;
}
template <typename T>
void g(T * t)
{
std::cout << "void g(T * t)" << std::endl;
}
int main()
{
int i = 42, *p = &i;
const int ci = 0, *p2 = &ci;
g(42); g(p); g(ci); g(p2);
f(42); f(p); f(ci); f(p2);
return 0;
}
练习¶
输出:
void g(T t)
void g(T * t)
void g(T t)
void g(T * t)
void f(T t)
void f(T t)
void f(T t)
void f(const T * t)
可变参数模板¶
一个可变参数模板(variadic template)就是一个
接受可变数目参数的模板函数或模板类。
可变数目的参数被称为参数包(parameter packet)。存在两种参数包:
- 模板参数包(template parameter packet),表示零个或多个模板参数;
- 函数参数包(function parameter packet),表示零个或多个函数参数。
可变参数模板¶
我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个
模板参数列表中,class…或typename…指出接下来的参数表示零个或多个类型的列表;
一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。
在函数参数列表中,
如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。例如:
// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
声明了foo是一个可变参数函数模板,
它有一个名为T的类型参数,和一个名为Args的模板参数包。
这个包表示零个或多个额外的类型参数。
foo的函数参数列表包含一个const &类型的参数,指向T的类型,
还包含一个名为rest的函数参数包,此包表示零个或多个函数参数。
可变参数模板¶
与往常一样,编译器从函数的实参推断模板参数类型。
对于一个可变参数模板,编译器还会推断包中参数的数目。例如,给定下面的调用:
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d); // three parameters in the pack
foo(s, 42, "hi"); // two parameters in the pack
foo(d, s); // one parameter in the pack
foo("hi"); // empty pack
编译器会为foo实例化出四个不同的版本:
void foo(const int&, const string&, const int&, double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
在每个实例中,T的类型都是从第一个实参的类型推断出来的。
剩下的实参(如果有的话)提供函数额外实参的数目和类型。
sizeof…运算符¶
当我们需要知道包中有多少元素时,可以使用sizeof…运算符。
类似sizeof,sizeof…也返回一个常量表达式,而且不会对其实参求值:
template<typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // number of type parameters
cout << sizeof...(args) << endl; // number of function parameters
}
练习¶
调用本节中的每个 foo,确定 sizeof...(Args) 和 sizeof...(rest)分别返回什么。
#include <iostream>
using namespace std;
template <typename T, typename ... Args>
void foo(const T &t, const Args& ... rest){
cout << "sizeof...(Args): " << sizeof...(Args) << endl;
cout << "sizeof...(rest): " << sizeof...(rest) << endl;
};
void test_param_packet(){
int i = 0;
double d = 3.14;
string s = "how now brown cow";
foo(i, s, 42, d);
foo(s, 42, "hi");
foo(d, s);
foo("hi");
}
练习¶
int main(){
test_param_packet();
return 0;
}
结果:
sizeof...(Args): 3
sizeof...(rest): 3
sizeof...(Args): 2
sizeof...(rest): 2
sizeof...(Args): 1
sizeof...(rest): 1
sizeof...(Args): 0
sizeof...(rest): 0
练习¶
编写一个程序验证上一题的答案。
参考上题
编写可变参数函数模板¶
我们可以使用一个initializer_list来定义一个可接受可变数目实参的函数。
但是,所有实参必须具有相同的类型(或它们的类型可以转换为同一个公共类型)。
当我们既不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数是很有用的
作为一个例子,我们将定义一个函数,它类似较早的error_msg函数,
差别仅在于新函数实参的类型也是可变的。
我们首先定义一个名为print的函数,它在一个给定流上打印给定实参列表的内容。
编写可变参数函数模板¶
可变参数函数通常是递归的。
第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
我们的print函数也是这样的模式,
每次递归调用将第二个实参打印到第一个实参表示的流中。
为了终止递归,我们还需要定义一个非可变参数的print函数,
它接受一个流和一个对象:
// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template<typename T>
ostream &print(ostream &os, const T &t)
{
return os << t; // no separator after the last element in the pack
}
// this version of print will be called for all but the last element in the pack
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
os << t << ", "; // print the first argument
return print(os, rest...); // recursive call; print the other arguments
}
编写可变参数函数模板¶
第一个版本的print负责终止递归并打印初始调用中的最后一个实参。
第二个版本的print是可变参数版本,它打印绑定到t的实参,
并调用自身来打印函数参数包中的剩余值。
这段程序的关键部分是可变参数函数中对print的调用:
return print(os, rest...); // recursive call; print the other arguments
我们的可变参数版本的print函数接受三个参数:
一个ostream&,一个const T&和一个参数包。而此调用只传递了两个实参。
其结果是rest中的第一个实参被绑定到t,剩余实参形成下一个print调用的参数包。
因此,在每个调用中,包中的第一个实参被移除,成为绑定到t的实参。即,给定:
print(cout, i, s, 42); // two parameters in the pack
递归会执行如下:
调用t rest...
print(cout, i, s, 42) i s,42
print(cout, s, 42) s 42
print(cout, 42)调用非可变参数版本print
前两个调用只能与可变参数版本的print匹配,非可变参数版本是不可行的,
因为这两个调用分别传递四个和三个实参,而非可变参数print只接受两个实参。
编写可变参数函数模板¶
对于最后一次递归调用print(cout,42),两个print版本都是可行的。
这个调用传递两个实参,第一个实参的类型为ostream&。
因此,可变参数版本的print可以实例化为只接受两个参数:
一个是ostream&参数,另一个是const T&参数。
对于最后一个调用,两个函数提供同样好的匹配。
但是,非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数版本
当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。
否则,可变参数版本会无限递归。
练习¶
编写你自己版本的 print 函数,并打印一个、两个及五个实参来测试它,
要打印的每个实参都应有不同的类型。
#include<iostream>
#include<string>
template <typename T>
std::ostream & print(std::ostream & os, const T & t)
{
return os << t;
}
template <typename T, typename ... Args>
std::ostream & print(std::ostream & os, const T & t, const Args & ... rest)
{
os << t << ", ";
return print(os, rest...);
}
练习¶
int main()
{
print(std::cout, 1) << std::endl;
print(std::cout, 1, "123") << std::endl;
print(std::cout, 1, "123", 3.123, std::string("qwert"), -5) << std::endl;
return 0;
}
练习¶
如果我们对一个没 << 运算符的类型调用 print,会发生什么?
无法通过编译。
练习¶
如果我们的可变参数版本 print 的定义之后声明非可变参数版本,
解释可变参数的版本会如何执行。
无限递归,最终最后一步
error: no matching function for call to 'print(std::ostream&)'
包扩展¶
对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展(expand)它
当扩展一个包时,我们还要提供用于每个扩展元素的模式(pattern)。
扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。
我们通过在模式右边放一个省略号(…)来触发扩展操作。
例如,我们的print函数包含两个扩展:
template <typename T, typename... Args>
ostream &
print(ostream &os, const T &t, const Args&... rest)// expand Args
{
os << t << ", ";
return print(os, rest...); // expand rest
}
第一个扩展操作扩展模板参数包,为print生成函数参数列表。
第二个扩展操作出现在对print的调用中。此模式为print调用生成实参列表。
包扩展¶
对Args的扩展中,编译器将模式const Arg&应用到模板参数包Args中的每个元素。
因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,
每个类型都形如const type&。例如:
print(cout, i, s, 42); // two parameters in the pack
最后两个实参的类型和模式一起确定了尾置参数的类型。此调用被实例化为:
ostream&
print(ostream&, const int&, const string&, const int&);
第二个扩展发生在对print的(递归)调用中。
在此情况下,模式是函数参数包的名字(即rest)。
此模式扩展出一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于:
print(os, s, 42);
理解包扩展¶
print中的函数参数包扩展仅仅将包扩展为其构成元素,C++语言还允许更复杂的扩展模式
我们可以编写第二个可变参数函数,对其每个实参调用debug_rep,
然后调用print打印结果string:
// call debug_rep on each argument in the call to print
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
// print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an)
return print(os, debug_rep(rest)...);
}
这个print调用使用了模式debug_reg(rest)。
此模式表示我们希望对函数参数包rest中的每个元素调用debug_rep。
扩展结果将是一个逗号分隔的debug_rep调用列表。即,下面调用:
errormsg(cerr,fcnname, code.num(),otherdata,"other",item);
就好像我们这样编写代码一样
print(cerr, debug_rep(fcnname), debug_rep(code.num()),
debug_rep(otherdata), debug_rep("otherdata"),
debug_rep(item));
理解包扩展¶
与之相对,下面的模式会编译失败
// passes the pack to debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // error: no matching function to call
这段代码的问题是我们在debug_rep调用中扩展了rest,它等价于
print(cerr, debug_rep(fcnname, code.num(),
otherdata, "otherdata", item));
在这个扩展中,我们试图用一个五个实参的列表来调用debug_rep,
但并不存在与此调用匹配的debug_rep版本。debug_rep函数不是可变参数的,
而且没有哪个debug_rep版本接受五个参数。
扩展中的模式会独立地应用于包中的每个元素。
练习¶
编写并测试可变参数版本的 errormsg。
template<typename... args>
std::ostream& errormsg(std::ostream& os, const args... rest)
{
return print(os, debug_rep(rest)...);
}
练习¶
比较你的可变参数版本的 errormsg 和6.2.6节中的 error_msg函数。
两种方法的优点和缺点各是什么?
可变参数版本有更好的灵活性。
转发参数包¶
在新标准下,我们可以组合使用可变参数模板与forward机制来编写函数,
实现将其实参不变地传递给其他函数。
作为例子,我们将为strvec类添加一个emplace_back成员。
标准库容器的emplace_back成员是一个可变参数成员模板,
它用其实参在容器管理的内存空间中直接构造一个元素。
我们为strvec设计的emplace_back版本也应该是可变参数的,
因为string有多个构造函数,参数各不相同。
由于我们希望能使用string的移动构造函数,
因此还需要保持传递给emplace_back的实参的所有类型信息。
转发参数包¶
如我们所见,保持类型信息是一个两阶段的过程。
首先,为了保持实参中的类型信息,必须将emplace_back的函数参数定义为
模板类型参数的右值引用:
class strvec {
public:
template <class... args> void emplace_back(args&&...);
// remaining members as in § 13.5
};
模板参数包扩展中的模式是&&,
意味着每个函数参数将是一个指向其对应实参的右值引用。
转发参数包¶
其次,当emplace_back将这些实参传递给construct时,
我们必须使用forward来保持实参的原始类型
template <class... args>
inline
void strvec::emplace_back(args&&... args)
{
chk_n_alloc(); // reallocates the strvec if necessary
alloc.construct(first_free++, std::forward<args>(args)...);
}
转发参数包¶
emplace_back的函数体调用了chk_n_alloc来确保有足够的空间容纳一个新元素,
然后调用了construct在first_free指向的位置中创建了一个元素。
construct调用中的扩展为
std::forward<args>(args)...
它既扩展了模板参数包args,也扩展了函数参数包args。此模式生成如下形式的元素
std::forward<T_i>(t_i)
其中T_i表示模板参数包中第i个元素的类型,t_i表示函数参数包中第i个元素。
例如,假定svec是一个strvec,如果我们调用
svec.emplace_back(10, 'c'); // adds cccccccccc as a new last element
construct调用中的模式会扩展出
std::forward<int>(10), std::forward<char>(c)
通过在此调用中使用forward,我们保证如果用一个右值调用emplace_back,
则construct也会得到一个右值。例如,在下面的调用中:
svec.emplace_back(s1 + s2); // uses the move constructor
传递给emplace_back的实参是一个右值,它将以如下形式传递给construct
std::forward<string>(string("the end"))
forward<string>的结果类型是string&&,因此construct将得到一个右值引用实参。
construct会继续将此实参传递给string的移动构造函数来创建新元素。
建议:转发和可变参数模板¶
可变参数函数通常将它们的参数转发给其他函数。
这种函数通常具有与我们的emplace_back函数一样的形式:
// fun has zero or more parameters each of which is
// an rvalue reference to a template parameter type
template<typename... args>
void fun(args&&... args) // expands args as a list of rvalue references
{
// the argument to work expands both args and args
work(std::forward<args>(args)...);
}
这里我们希望将fun的所有实参转发给另一个名为work的函数,
假定由它完成函数的实际工作。类似emplace_back中对construct的调用,
work调用中的扩展既扩展了模板参数包也扩展了函数参数包。
由于fun的参数是右值引用,因此我们可以传递给它任意类型的实参;
由于我们使用std::forward传递这些实参,
因此它们的所有类型信息在调用work时都会得到保持。
练习¶
为你的 strvec 类及你为16.1.2节练习中编写的 vec 类添加 emplace_back 函数。
template<typename t> //for the class template
template<typename... args> //for the member template
inline void
vec<t>::emplace_back(args&&...args)
{
chk_n_alloc();
alloc.construct(first_free++, std::forward<args>(args)...);
}
练习¶
假定 s 是一个 string,解释调用 svec.emplace_back(s)会发生什么。
s是一个左值,经过包扩展,以如下形式传给construct
std::forward<string>(s)
forward<string>结果类型是string&,因此construct将得到一个左值引用实参
它继续将此参数传递给string的拷贝构造函数来创建新元素
练习¶
解释 make_shared 是如何工作的。
make_shared是一个可变参数模板的函数,它的第一个模板类型是需要手动指定的,
表示了构造的类型。后面的是可变参数模板。函数形参是可变参数模板类型。
函数内容是new一个对象,用std::forward保持参数类型,
然后用指针来构造一个shared_ptr,并返回。
练习¶
定义你自己版本的 make_shared。
template <typename t, typename ... args>
auto make_shared(args&&... args) -> std::shared_ptr<t>
{
return std::shared_ptr<t>(new t(std::forward<args>(args)...));
}
模板特例化¶
编写单一模板,使之对任何可能的模板实参都是最适合的,都能实例化,
这并不总是能办到。在某些情况下,通用模板的定义对特定类型是不适合的:
通用定义可能编译失败或做得不正确。其他时候,
我们也可以利用某些特定知识来编写更高效的代码,而不是从通用模板实例化。
当我们不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本。
模板特例化¶
我们的compare函数是一个很好的例子,
它展示了函数模板的通用定义不适合一个特定类型(即字符指针)的情况。
我们希望compare通过调用strcmp比较两个字符指针而非比较指针值。
实际上,我们已经重载了compare函数来处理字符串字面常量
// first version; can compare any two types
template <typename t> int compare(const t&, const t&);
// second version to handle string literals
template<size_t n, size_t m>
int compare(const char (&)[n], const char (&)[m]);
模板特例化¶
但是,只有当我们传递给compare一个字符串字面常量或者一个数组时,
编译器才会调用接受两个非类型模板参数的版本。
如果我们传递给它字符指针,就会调用第一个版本:
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); // calls the first template
compare("hi", "mom"); // calls the template with two nontype parameters
我们无法将一个指针转换为一个数组的引用,
因此当参数是p1和p2时,第二个版本的compare是不可行的。
为了处理字符指针(而不是数组),
可以为第一个版本的compare定义一个模板特例化(template specialization)版本。
一个特例化版本就是模板的一个独立的定义,
在其中一个或多个模板参数被指定为特定的类型。
定义函数模板特例化¶
当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。
为了指出我们正在实例化一个模板,应使用关键字template后跟一个空尖括号对(<>)。
空尖括号指出我们将为原模板的所有模板参数提供实参:
// special version of compare to handle pointers to character arrays
template <>
int compare(const char* const &p1, const char* const &p2)
{
return strcmp(p1, p2);
}
理解此特例化版本的困难之处是函数参数类型。当我们定义一个特例化版本时,
函数参数类型必须与一个先前声明的模板中对应的类型匹配。本例中我们特例化:
template <typename t> int compare(const t&, const t&);
其中函数参数为一个const类型的引用。
类似类型别名,模板参数类型、指针及const之间的相互作用会令人惊讶。
我们希望定义此函数的一个特例化版本,其中t为const char*。
我们的函数要求一个指向此类型const版本的引用。
一个指针类型的const版本是一个常量指针而不是指向const类型的指针。
我们需要在特例化版本中使用的类型是const char * const &,
即一个指向const char的const指针的引用。
函数重载与模板特例化¶
当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。
即,我们为原模板的一个特殊实例提供了定义。
重要的是要弄清:一个特例化版本本质上是一个实例,而非函数名的一个重载版本。
特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
我们将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,
会影响到函数匹配。例如,我们已经定义了两个版本的compare函数模板,
一个接受数组引用参数,另一个接受const t&。
我们还定义了一个特例化版本来处理字符指针,这对函数匹配没有影响。
当我们对字符串字面常量调用compare时
compare("hi", "mom")
对此调用,两个函数模板都是可行的,且提供同样好的(即精确的)匹配。
但是,接受字符数组参数的版本更特例化,因此编译器会选择它。
如果我们将接受字符指针的compare版本定义为一个普通的非模板函数
(而不是模板的一个特例化版本),此调用的解析就会不同。
在此情况下,将会有三个可行的函数:两个模板和非模板的字符指针版本。
所有三个函数都提供同样好的匹配。如前所述,
当一个非模板函数提供与函数模板同样好的匹配时,编译器会选择非模板版本。
关键概念:普通作用域规则应用于特例化¶
为了特例化一个模板,原模板的声明必须在作用域中。
而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中。
对于普通类和函数,丢失声明的情况(通常)很容易发现
- 编译器将不能继续处理我们的代码。
但是,如果丢失了一个特例化版本的声明,编译器通常可以用原模板生成代码。
由于在丢失特例化版本时编译器通常会实例化原模板,
很容易产生模板及其特例化版本声明顺序导致的错误,而这种错误又很难查找。
如果一个程序使用一个特例化版本,
而同时原模板的一个实例具有相同的模板实参集合,就会产生错误。
但是,这种错误编译器又无法发现。
模板及其特例化版本应该声明在同一个头文件中。
所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
类模板特例化¶
除了特例化函数模板,我们还可以特例化类模板。
作为一个例子,我们将为标准库hash模板定义一个特例化版本,
可以用它来将sales_data对象保存在无序容器中。
默认情况下,无序容器使用hash<key_type>来组织其元素。
为了让我们自己的数据类型也能使用这种默认组织方式,
必须定义hash模板的一个特例化版本。一个特例化hash类必须定义:
- 一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个size_t。·
- 两个类型成员,result_type和argument_type,
分别调用运算符的返回类型和参数类型。·
- 默认构造函数和拷贝赋值运算符.可以隐式定义
在定义此特例化版本的hash时,唯一复杂的地方是:
必须在原模板定义所在的命名空间中特例化它。
类模板特例化¶
我们可以向命名空间添加成员。为了达到这一目的,首先必须打开命名空间:
// open the std namespace so we can specialize std::hash
namespace std {
} // close the std namespace; note: no semicolon after the close curly
花括号对之间的任何定义都将成为命名空间std的一部分。
下面的代码定义了一个能处理sales_data的特例化hash版本:
namespace std {
template <> // we're defining a specialization with
struct hash<sales_data> // the template parameter of sales_data
{
// the type used to hash an unordered container must define these types
typedef size_t result_type;
typedef sales_data argument_type; // by default, this type needs ==
size_t operator()(const sales_data& s) const;
// our class uses synthesized copy control and default constructor
};
类模板特例化¶
size_t
hash<sales_data>::operator()(const sales_data& s) const
{
return hash<string>()(s.bookno) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
} // close the std namespace; note: no semicolon after the close curly
我们的hash<sales_data>定义以template<>开始,指出我们正在定义一个全特例化的模板。
我们正在特例化的模板名为hash,而特例化版本为hash<sales_data>。
接下来的类成员是按照特例化hash的要求而定义的。
类似其他任何类,我们可以在类内或类外定义特例化版本的成员,
本例中就是在类外定义的。重载的调用运算符必须为给定类型的值定义一个哈希函数。
对于一个给定值,任何时候调用此函数都应该返回相同的结果。
一个好的哈希函数对不相等的对象(几乎总是)应该产生不同的结果。
类模板特例化¶
在本例中,我们将定义一个好的哈希函数的复杂任务交给了标准库。
标准库为内置类型和很多标准库类型定义了hash类的特例化版本。
我们使用一个(未命名的)hash<string>对象来生成bookno的哈希值,
用一个hash<unsigned>对象来生成units_sold的哈希值,
用一个hash<double>对象来生成revenue的哈希值。
我们将这些结果进行异或运算,形成给定sales_data对象的完整的哈希值。
值得注意的是,我们的hash函数计算所有三个数据成员的哈希值,
从而与我们为sales_data定义的operator==是兼容的。默认情况下,
为了处理特定关键字类型,无序容器会组合使用key_type对应的特例化hash版本
和key_type上的相等运算符。
类模板特例化¶
假定我们的特例化版本在作用域中,当将sales_data作为容器的关键字类型时,
编译器就会自动使用此特例化版本:
// uses hash<sales_data> and sales_data operator==from § 14.3.1
unordered_multiset<sales_data> sdset;
由于hash<sales_data>使用sales_data的私有成员,
我们必须将它声明为sales_data的友元:
template <class t> class std::hash; // needed for the friend declaration
class sales_data {
friend class std::hash<sales_data>;
// other members as before
};
这段代码指出特殊实例hash<sales_data>是sales_data的友元。
由于此实例定义在std命名空间中,我们必须记得在friend声明中应使用std::hash。
为了让sales_data的用户能使用hash的特例化版本,
我们应该在sales_data的头文件中定义该特例化版本。
类模板部分特例化¶
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。
我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。
一个类模板的部分特例化(partial specialization)本身是一个模板,
使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。
我们只能部分特例化类模板,而不能部分特例化函数模板。
在16.2.3节中我们介绍了标准库remove_reference类型。
该模板是通过一系列的特例化版本来完成其功能的:
// original, most general template
template <class t> struct remove_reference {
typedef t type;
};
// partial specializations that will be used for lvalue and rvalue references
template <class t> struct remove_reference<t&> //references
{ typedef t type; };
template <class t> struct remove_reference<t&&> //references
{ typedef t type; };
第一个模板定义了最通用的模板。它可以用任意类型实例化;
它将模板实参作为type成员的类型。接下来的两个类是原始模板的部分特例化版本。
类模板部分特例化¶
由于一个部分特例化版本本质是一个模板,与往常一样,我们首先定义模板参数。
类似任何其他特例化版本,部分特例化版本的名字与原模板的名字相同。
对每个未完全确定类型的模板参数,在特例化版本的模板参数列表中都有一项与之对应。
在类名之后,我们为要特例化的模板参数指定实参,这些实参列于模板名之后的尖括号中。
这些实参与原始模板中的参数按位置对应。
部分特例化版本的模板参数列表是
- 原始模板的参数列表的一个子集或者是一个特例化版本。
在本例中,特例化版本的模板参数的数目与原始模板相同,但是类型不同。
两个特例化版本分别用于左值引用和右值引用类型:
int i;
// decltype(42) is int, uses the original template
remove_reference<decltype(42)>::type a;
// decltype(i) is int&, uses first (T&) partial specialization
remove_reference<decltype(i)>::type b;
// decltype(std::move(i)) is int&&, uses second (i.e., T&&) partial specialization
remove_reference<decltype(std::move(i))>::type c;
三个变量a、b和c均为int类型。
特例化成员而不是类¶
我们可以只特例化特定成员函数而不是特例化整个模板。
例如,如果Foo是一个模板类,包含一个成员Bar,我们可以只特例化该成员:
template <typename T> struct Foo {
Foo(const T &t = T()): mem(t) { }
void Bar() { /* ... */ }
T mem;
// other members of Foo
};
template<> // we're specializing a template
void Foo<int>::Bar(){ // we're specializing the Bar member of Foo<int>
// do whatever specialized processing that applies to ints
}
本例中我们只特例化Foo<int>类的一个成员,其他成员将由Foo模板提供:
Foo<string> fs; //instantiates Foo<string>::Foo()
fs.Bar(); //instantiates Foo<string>::Bar()
Foo<int> fi; //instantiates Foo<int>::Foo()
fi.Bar(); //uses our specialization of Foo<int>::Bar()
当我们用int之外的任何类型使用Foo时,其成员像往常一样进行实例化。
当我们用int使用Foo时,Bar之外的成员像往常一样进行实例化。
如果我们使用Foo<int>的成员Bar,则会使用我们定义的特例化版本。
练习¶
定义你自己版本的 hash<Sales_data>,
并定义一个 Sales_data 对象的 unorder_multise。
将多条交易记录保存到容器中,并打印其内容。
见book/ch16/ex16.62
编译命令 g++ main.cpp Sales_data.cpp && ./a.out
练习¶
定义一个函数模版,统计一个给定值在一个vecor中出现的次数。测试你的函数,
分别传递给它一个double的vector,一个int的vector以及一个string的vector。
#include <iostream>
#include <vector>
#include <cstring>
using std::string;
// template
template<typename T>
std::size_t count(const std::vector<T> & vec, T value)
{
auto count = 0;
for(auto const& elem : vec)
if(value == elem) ++count;
return count;
}
练习¶
// template specialization
template<>
std::size_t count (const std::vector<const char*> & vec, const char* value)
{
auto count = 0;
for(auto const& elem : vec)
if(0 == strcmp(value, elem)) ++count;
return count;
}
练习¶
int main()
{
// for ex16.63
std::vector<double> vd = { 1.1, 1.1, 2.3, 4 };
std::cout << count(vd, 1.1) << std::endl;
std::vector<int> vi = {1,2,3,1};
std::cout << count(vi, 1) << std::endl;
std::vector<string> vs = {"abc","ab","abc"};
std::cout << count(vs, std::string("abc") )<< std::endl;
// for ex16.64
std::vector<const char*> vcc = { "alan", "alan", "alan", "alan", "moophy" };
std::cout << count(vcc, "alan") << std::endl;
return 0;
}
练习¶
为上一题的模版编写特例化版本来处理vector<const char*>。编写程序使用这个特例化版本。
参考之前
练习¶
在16.3节中我们定义了两个重载的 debug_rep 版本
一个接受 const char* 参数,另一个接受 char * 参数。
将这两个函数重写为特例化版本。
#include<iostream>
#include<string>
#include<sstream>
template<typename T> std::string debug_rep(const T& t)
{
std::ostringstream ret;
ret << t;
return ret.str();
}
练习¶
template<typename T> std::string debug_rep(T* p)
{
std::ostringstream ret;
ret << "pointer: " << p;
if(p)
ret << " " << debug_rep(*p);
else
ret << " null pointer";
return ret.str();
}
练习¶
template <>
std::string debug_rep(char * p)
{
std::cout<<"debug_rep(char * p)";
return debug_rep(std::string(p));
}
template <>
std::string debug_rep(const char * p)
{
std::cout<<"debug_rep(const char * p)";
return debug_rep(std::string(p));
}
int main()
{
char p[] = "alan";
std::cout << debug_rep(p) << "\n";
std::cout << debug_rep("abc") << "\n";
return 0;
}
练习¶
重载debug_rep 函数与特例化它相比,有何优点和缺点?
重载函数会改变函数匹配。
特例化不影响函数匹配。
优点:
特例化不会影响重载函数的匹配规则
缺点
不能实现重载函数的匹配规则优先级的提升
需要严格遵守模板的定义规则
练习¶
定义特例化版本会影响 debug_rep 的函数匹配吗?如果不影响,为什么?
不影响,特例化是模板的一个实例,并没有重载函数。
小结¶
模板是C++语言与众不同的特性,也是标准库的基础。
一个模板就是一个编译器用来生成特定类类型或函数的蓝图。
生成特定类或函数的过程称为实例化。我们只编写一次模板,
就可以将其用于多种类型和值,编译器会为每种类型和值进行模板实例化。
我们既可以定义函数模板,也可以定义类模板。
标准库算法都是函数模板,标准库容器都是类模板。
显式模板实参允许我们固定一个或多个模板参数的类型或值。
对于指定了显式模板实参的模板参数,可以应用正常的类型转换。
一个模板特例化就是一个用户提供的模板实例,
它将一个或多个模板参数绑定到特定类型或值上。
当我们不能(或不希望)将模板定义用于某些特定类型时,特例化非常有用。
最新C++标准的一个主要部分是可变参数模板。
一个可变参数模板可以接受数目和类型可变的参数。
可变参数模板允许我们编写像容器的emplace成员和
标准库make_shared函数这样的函数,实现将实参传递给对象的构造函数。
实践课¶
- 从课程主页 cpp.njuer.org 打开 《面向对象编程基础》 实验课 九 模板 https://developer.aliyun.com/adc/scenario/35d4a17e964b479ebffe7cc4e5d7eae3
- 使用g++编译代码
- 编辑一个 readme.md 文档,键入本次实验心得.
- 使用git进行版本控制 可使用之前的gitee代码仓库
- 云服务器(elastic compute service,简称ecs) - aliyun linux 2是阿里云推出的 linux 发行版 - vim是从vi发展出来的一个文本编辑器. - g++ 是c++编译器
习题
用类模板定义一个类属的队列类
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekn: mkdir -p weekn ; mv 文件名 weekn;)
vim test.cpp
g++ ./test.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "your name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN
git push 到你的git仓库
git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md
提交¶
- 截图或复制文字,提交到群作业.
- 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
- 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo
关于使用tmux¶
sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a,
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0
vim 共分为三种模式¶
- 命令模式
- 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
- i 切换到输入模式,以输入字符.
- x 删除当前光标所在处的字符.
- : 切换到底线命令模式,可输入命令.
- 输入模式
- 命令模式下按下i就进入了输入模式.
- ESC,退出输入模式,切换到命令模式
- 底线命令模式
- 命令模式下按下:(英文冒号)就进入了底线命令模式.
- wq 保存退出
vim 常用按键说明¶
除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下:
- 光标移动
- j下 k上 h左 l右
- w前进一个词 b后退一个词
- Ctrl+d 向下半屏 ctrl+u 向上半屏
- G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
- dd 删一行 ndd 删n行
- yy 复制一行 nyy复制n行
- p将复制的数据粘贴在下一行 P粘贴到上一行
- u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
- /word 向下找word ?word 向上找
- n重复搜索 N反向搜索
- :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
取代为 word2
vim 常用按键说明¶
底线命令模式下:
- :set nu 显示行号
- :set nonu 取消行号
- :set paste 粘贴代码不乱序
【注:把caps lock按键映射为ctrl,能提高编辑效率.】
Markdown 文档语法¶
# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
- 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)
|表格标题1|表格标题2|
|-|-|
|内容1|内容2|