第十六章(模板和泛型编程)
1).泛型编程。例如,容器,迭代器,算法。提供一些信息进行实例化。可以在编译时就知道类型。
2).OOP,动态绑定,可以在运行时才知道类型。 3).以上两者都可以处理编写程序时,不知道类型的情况。
/1.定义模板
1).比较所有类型的数据大小,
- 需要定义很多的重载函数,(我们所能想到的)
- 而且如果是用户自定义的类型,这种策略就不可行。
2).比较函数,除了数据类型,函数体,函数名,参数名,返回值类型完全一样。
//1.函数模板
1).定义一个函数模板,而不是定义很多的重载函数。 2).template<typename/class T> (模板参数列表,里面就是模板参数分为类型参数和非类型参数,非类型参数需要是常量表达式,注意每一个类型参数前 都需要 typename/class (类类型,但其实不只是类类型);或者非类型参数前使用unsigned ,
- (可以交叉使用)且它们以逗号隔开)
- (一旦有了上述定义,T可以看成类型说明符。和内置数据类型,类类型没有什么两样。可以定义变量等。需要用的时候就把它看成是类型名称即可。)
- 然后书写正常的完整函数即可。这样就完成了一个函数模板。
{
template<typename T>
int compare(const T &v1,const T &v2) {
if (v1 < v2) return -1;
if (v1 > v2) return 1;
return 0;
}
}
3).注意事项。
- 模板定义中,模板参数列表不能为空
- 将模板参数列表和函数的参数列表相比较。(我们可以隐式或者显式地执行模板实参)注意使用时(和普通函数一样地调用)是编译器通过识别我们的函数实参,(推断出模板的实参,绑定到模板参数中)判断出类型然后将T用实际的类型代入,这就是实例化的过程。
{
vector<int> vec1{1,2,3,4};
vector<int> vec2{2,3,4};
compare(vec1,vec2);
}
4).当编译器使用模板实参代替对应的模板参数创建出模板的一个实例。(就是实例化一个模板) 5).非类型参数的典型就是数组的引用。比较字符串。因为我们不知道大小。我们需要定义数组的引用。 6).注意非类型参数可以是以下类型。
- 整型,
- 指针,
- 引用。
- 绑定到非类型整型参数的实参必须是一个常量表达式。而对于指针和引用要求是,实参必须具有静态的生存期(
static 变量)。当然指针可以用nullptr 或者0的常量表达式来实例化。
7).由于非类型参数是一个常量,所以我们可以,在需要使用常量的地方就可以使用它们。例如,指定数组的大小。 8).例如,比较两个字符串数组。就是两个const char *
{
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]) {
return strcmp(p1,p2);
}
compare("hi", "mom");
int compare(const char (&p1)[3], const char (&p2)[4]);
}
9).函数模板也可以是inline 或者constexpr 的。注意声明位置。
{
template <...>
inline int ...;
template <...>
constexpr int ...;
}
10).模板程序需要尽量减少对类型的依赖。
- 设为
const 的引用。避免一些类无法进行拷贝。 - 使用
less<T> 来实例化,而不是使用<> 。可以在函数体里面使用类型模板。
{
if (less<T>()(v1,v2)) return -1;
if (less<T>()(v2,v1)) return 1;
}
11).关于模板的定义和函数,类的区别。
- 当编译器运到一个模板定义时,它并不生成代码。只有当我们实例化出一个特定版本时,编译器才会生成代码。
- 对于函数,我们调用时,编译器只需要掌握声明,也就是我们需要先声明再使用;
- 对于一个类类型的对象,我们使用时,类的定义必须是可用的;但是成员函数的定义不必已经出现。
- 因此,函数的声明和类的定义放在头文件;普通函数的定义和类的成员函数的定义
放在源文件。 - 而对于模板,为了实例化一个版本,编译器需要掌握函数模板,或者类模板成员函数的定义。因此,这些函数的定义需要放在头文件中。即保证函数模板实例化时,函数的定义是可见。
- 其他就是,模板设计者,保证用在模板中的可见的类型都是可见的,例如,类类型是已经定义好的等。这一点,用户和设计者的要求一致。
12).编译错误的报告(包含三个阶段)。
- 第一个阶段,编译模板本身。一般只是检查语法错误;例如变量名字,分号等。
- 第二个阶段,遇到模板的使用时,参数的数目,类型是否匹配。
- 第三个阶段,模板的实例化,会发现类型相关的错误。依赖于编译器如何管理实例化,这类错误又可能在链接时才报告。
13).例子。
- 例如,我们在使用
compare 的<> 进行比较的版本的时候,编译器只有当进行第三个阶段时,才会知道错误。
练习,
{
template <typename I, class T>
I find(I b, I e, const T &v) {
while (b != e && *b != v) {
++b;
}
return b;
}
}
{
template <typename T, size_t N>
void print(const T (&a)[N]) {
for (auto iter = begin(a); iter != end(a); ++iter) {
cout << *iter << " ";
}
}
}
{
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;
}
}
- 16.7,设计
constexpr 函数模板,返回的是一个给定的数组的大小。利用函数模板直接可以给出。
{
template <typename T, size_t N>
constexpr size_t arr_size(const T (&a)[N]) {
return N;
}
}
//2.类模板
1).与函数模板不一样的是,它不能用推断的方式来得到模板参数。和我们以前使用模板一样,我们都需要显式地在尖括号中加入额外的信息————模板实参列表(显式模板实参。),来代替模板参数。 2).定义类模板Blob 。
{
template <typename T>
class Blob {
public:
typedef T value_type;
typedef typename vector<T>::size_type size_type;
Blob();
Blob(initializer_list<T> il);
size_type size() const {
return data->size();
}
bool enpty() const {
return data->empty();
}
void push_back(const T &t) {
data->push_back(t);
}
void push_back(T &&t) {
data->push_back(std::move(t));
}
void pop_back();
T& back();
T& operator[](size_type i);
private:
Shared_ptr<vector<T>> data;
void check(size_type i, const string &msg) const;
};
Blob<int> ia;
Blob<int> ia2 = {0,1,2,3,4};
template<>
class Blob<int> {
...
};
}
3).类模板不是一个类型名字。 4).定义在模板类之外的成员函数,也应该以模板的形式。
- 和类模板一样的模板参数。
- 类的名字必须包含模板实参,当我们定义成员函数时,模板实参和模板参数一致。
{
template<typename T>
void Blob<T>::check(size_t i, const string &smg) {
if (i >= data->size()) {
throw out_of_range(msg);
}
}
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 t) {
check(t, "out of range");
return (*data)[t];
}
template<class T>
void Blob<int>::pop_back() {
check(0, "pop_back on empty Blob");
data->pop_back();
}
template<class T>
Blob<T>::Blob() : data(make_shared<vector<T>>()) {}
template<class T>
Blob<T>::Blob(initializer_list<T> il) : data(make_shared<vector<T>>(il)) {}
Blob<string> articles = {"1","2"};
}
5).模板类的成员函数,只有当被使用时,才会别实例化。因此即便是有一些类型不完全符合模板的操作的要求。但是我们依然可以用该类型来实例化类。 6).当我们使用一个类模板时必须提供模板实参,但是有一个例外。在类模板自己的作用域中时,我们可以直接使用模板名字,而不用提供实参。这样就会默认是我们的类型参数。
{
BlobPtr& operator++();
BlobPtr& operator--();
BlobPtr<T>& operator++();
BlobPtr<T>& operator--();
}
7).在类外时,必须注意,当遇到类名字之后才表示进入类的作用域。
{
template<typename T>
BlobPtr<T> BlobPtr<T>::operator++() {
BlobPtr ret = *this;
++*this;
return ret;
}
}
8).类模板和友元
- 一个类模板中包含一个非模板友元。则友元被授权可以访问所有模板实例。
- 如果友元是模板,类可以授权给所有友元实例,也可以授权给部分实例。
9).例子。普通类的友元各种状态。
{
template <typename T> class BlobPtr;
template <typename T> class Blob;
template <typename T>
bool operator==(const Blob<T> &, const Blob<T> &);
template<typename T>
class Blob {
friend class BlobPtr<T>;
friend bool operator==(const Blob<T> &,const Blob<T> &);
};
}
10).模板类的友元的各中状态。
{
template<typename T> class Pal;
class C {
friend class Pal<C>;
template <typename T> friend class Pal2;
};
template <typename T> class C2 {
friend class Pal<T>;
template <typename X> friend class Pal2;
friend class Pal3;
};
}
11).令模板自己的类型参数成为友元。
{
template <typename T> class Bar {
friend T;
};
}
12).类型别名。
typedef Blob<string> StrBlob; 这样就得到一个StrBlob 表示一个string 实例化的Blob 。- 为类模板定义一个类型别名。(类模板不是一个类型,但是新标准允许我们为一个类模板定义一个类型别名。形式比较特殊。)
{
template<typename T> using twin = pair<T, T>;
twin<int> win_loss;
twin<string> area;
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books;
partNo<Student> kids;
}
13).类模板的static 成员
{
template <class T> class Foo {
public:
static size_t count() {
return ctr;
}
private:
static size_t ctr;
};
}
- 每一个
Foo 的实例化都有其自己的static 成员。即,对于给定的类型X,都有一个Foo<X>::ctr ,和一个Foo<T>::count 成员实例。所有的Foo<X> 共享这个ctr 和count 函数。 - 每一个模板类的
static 成员,和其他类一样,都是有且仅有一个定义。但是由于,每一个实例类都有一个独有的对象。因此,对于static 成员于定义模板函数一致。template<typename T> size_t Foo<T>::ctr = 0; 与定义别名,类模板成员一样,以一个模板参数列表开始,然后是定义的类型和名字。 - 访问类模板的
static 成员,也必须指定哪一个实例的static 成员,或者使用对象。
{
Foo<int> f;
auto ct = Foo<int>::count();
ct = f.count();
ct = Foo::count();
}
练习,
- 16.11,
List<T>();List<T>(const List<T> &); 这样的构造函数也是合法的,但是没有必要。
//3.模板参数
1).模板参数的名字可以是任意的。它的作用域就是声明之后到模板声明或者定义结束之前。它也会隐藏外层作用域中声明想同的名字。但是,不能使用模板参数名,这和特殊标识符号是一样的。
{
typedef double A;
template <typename A, typename B>
void f(A a, B b) {
A temp = a;
double B;
}
template <typename T, typename T>....
}
2).模板声明
{
template <typename> class Blob;
template <typename T> int compare(const T&, const T&);
template <typename T> T calc(const T&, const T&);
template <typename T> U calc(const U&, const U&);
template <typename Type>
Type calc(const Type &a, const Type &b) {...}
}
3).当我们希望通知编译器一个名字表示类型时,必须使用关键字typename ,而不能是class 。在类型参数列表中可以用class?
- 如果没有显式地指出,c++假定通过作用域运算符号访问的名字是一个变量而不是一个类型。
- 在普通的时候,不需要这样地指出,因为编译器对类型的定义知晓,当我们使用时,它知道是哪一种情况(是类型成员还是
static 成员。)。 - 而当使用参数类型时,编译器只有当实例化时,才能知道。但是处理模板,编译器就必须知道名字是否表示一个类型。
T::size_type * p;//否则编译器不知道这是定义一个变量还是一个乘法。 - 所以,显式地指出。
{
template <typename T>
typename T::value_type top(const T &c) {
if (!c.empty())
return c.back();
else
return typename T::value_type();
}
}
4).默认模板实参
- 与函数默认实参一样,一个模板参数,只有它的右侧所有的参数都有默认实参时,它才可以有默认实参。
{
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;
}
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, isbnCompare);
}
5).模板实参和类模版
- 不管是否需要使用默认实参,使用类模板都需要加上尖括号。尖括号指出类必须从一个模板实例化而来。
{
template <typename T = int> class Numbers {
public:
Numbers(T v = 0) : val(v) {}
private:
T val;
};
Numbers<long double> lots_of_precious;
Numbers<> average_precision;
}
//4.成员模板
1).模板类和普通类,都可以包含一个模板成员函数。这样成员称为成员模板。成员模板不可以是虚函数。 2).普通类的成员模板。
{
class DebugDelete {
public:
DebugDelete(ostream &s = cerr) : os(s) {}
template <typename T>
void operator()(T *p) const {
os << "deleting unique_ptr" << endl;
delete p;
}
private:
ostream &os;
};
double *p = new double;
DebugDelete d;
d(p);
int *ip = new int;
DebugDelete() (ip);
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
}
3).类模板的成员模板
{
template <typename T> class Blob {
template <typename It> Blob(It b, It e);
};
}
- 在类外定义一个成员模板时,必须同时提供类模板和成员模板参数列表。类模板的参数列表在前,函数的模板在数列表在后,
{
template <typename T>
template <typename It>
Blob<T>::Blob(It b, It e) :
data(make_shared<vector<T>>)(b, e) {}
}
4).实例化一个类模板的成员模板。
- 实例化类,还是需要显式地指出;实例化函数还是由编译器隐式地从实参中推断得到。
{
int ia[] = {0, 1, 2, 3, 4};
vector<long> vi = {1, 2, 3, 4};
list<const chat *> w = {"wow", "is", "the", "time"};
Blob<int> a1(begin(ia), end(ia));
Blob<long> a2(vi.begin(), vi.end());
Blob<string> a3(w.begin(), w.end());
}
//5.控制实例化
1).由于模板是使用时才会进行实例化,如果我们在多个独立编译的源文件中都使用了相同的模板,并且提供了一样的模板参数时,每一个源文件就都会有一个该模板的实例。这样无疑是额外的开销。 2).解决办法,显式实例化。
{
extern template declaration;
template declaration;
extern template class Blob<string>;
template int compare(const int &, const int &);
}
- 当编译器遇到
extern 模板声明时,它不会在本文件中生成实例化代码;将一个实例化声明为extern 就表示承诺在程序其他位置有该实例化的一个非extern 声明(定义)。对于给定的一个实例化版本,可能有多个extern 声明,但是必须只有一个定义。 - 编译器使用哦一个模板时会自动对其实例化,因此
extern 声明必须出现在任何使用此实例化版本代码之前。
{
extern template class Blob<string>;
extern template int compare(const int &, const int &);
Blob<string> sa1, sa2;
Blob<int> a1 = {1, 2, 3, 4};
Blob<int> a2(a1);
int i = compare(a1[0], a2[0]);
template int compare(const int &, const int &);
template class Blob<string>;
}
3).实例化定义会实例化所有成员
- 一个类实例化定义会实例化该模板的所有成员,包括内联的成员函数,当编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数。因此,如果我们显式实例化一个类模板的类型,必须能用于模板的所有成员。
练习,
//6.效率和灵活性
1).unique_ptr 和shared_ptr 的设计。
- 都是模板,可以在运行时改变存储的类型。
- 对于
unique_ptr 删除器类型也是它的类型的一部分。 - 在运行时候绑定删除器,
shared_ptr 的重载比较方便;在编译器时绑定删除器,避免了间接调用的额外开销,但是重载起来比较麻烦。
练习,
{
#ifndef SP_H
#define SP_H
#include <iostream>
using namespace std;
template <typename T>
class SP {
public:
SP() : p(nullptr), use(nullptr) {}
explicit SP(T *pt) :
p(pt), use(new int(1)) {}
SP(const SP &sp) :
p(sp.p), use(sp.use) {
if(use) ++*use;
}
SP& operator=(const SP&);
~SP();
T& operator*() {return *p;}
T& operator*() const {return *p;}
private:
T *p;
size_t *use;
};
template <typename T>
SP<T>::~SP() {
if(use && --*use == 0) {
delete p;
delete use;
}
}
...
template <typename T>
class UP {
public:
UP() : p(nullptr) {}
UP(const UP &) delete;
explicit UP(T *up) :
p(up) {}
UP& operator=(const UP&) = delete;
~UP();
T* release();
void reset(T* new_p);
T& operator*() {return *p;}
T& operator*() const {return *p;}
private:
T *p;
};
}
/2.模板实参的推断
1).推断规则,
- 顶层
const 无论是在形参还是实参中,都会被忽略。(对于实参而言) - 应用于函数模板的类型转换包括以下两项(对实参进行转换)
const 转换。可以将一个非const 对象的指针或者引用传递给一个const 指针或者引用形参。- 数组或函数指针转换,如果函数形参不是引用类型,则可以对数组或者函数类型的实参应用正常的指针转换。
- 其他的类型转换,如算术转换,派生类向基类的转换,以及用户自定义的转换,都不能应用于函数模板的实参。
{
template <typename T> T fobj(T, T);
template <typename T> T fref(const T &, const T &);
string s1("a value");
const string s2("another value");
fobl(s1, s2);
fref(s1, s2);
int a[10], b[30];
fobj(a, b);
fref(a, b);
}
2).使用相同模板参数类型的函数形参
- 由于只能有以上有限的类型转换,当一个模板类型参数用作多个函数形参的类型时,实参必须有相同的类型。如果推断出来的类型不匹配,调用iu是错误的。
{
long lhg;
compare(lng, 1024);
template <typename A, typename B>
int flexibleCompare(const A &v1, const B &v2) {
if...
}
long lng;
flexibleCompare(lng, 1024);
}
练习,
{
template <class T>
int compare(const T &v1, const T &v2);
compare("hi", "hello");
compare("world", "error");
}
//2.函数模板的显式实参(显式实例化)
1).用户控制函数模板的实例化
{
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
auto val3 = sum<long long>(i, lng);
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2,T1);
auto val3 = alternative_sum<long long>(i, lng);
auto val2 = alternative_sum<long long, int, long>(i, lng);
}
2).正常类型转换可以应用于显式指定的实参。
{
long lng;
compare(lng, 1024);
compare<long>(lng, 1024);
compare<int>(lng, 1024);
}
练习,
- 16.38,为什么
make_shared 需要显式地实参。有时候我们需要的是默认初始化,有时候会用make_shared<string>(10,'4'); 的形式进行初始化。而以上的形式编译器都没有办法得到实参的类型。 - 16.39,比较i啷个字符串的大小,可以这样做,
compare<string>("hello", "hi"); 如果没有显式地指定,编译器自动识别的将会是,char [] ,不仅仅如此,而且得到的很可能是类型不匹配。
//3.位置返回类型和类型转换
1).显示地指出返回类型虽然有一定的好处。但是也有问题。例如,当我们需要返回迭代器范围中的指定元素时,需要返回的类型就是元素的类型,可是我们不知道元素的类型。我们可以使用decltype(*b) 但是,在函数参数列表之前,b是不存在的。
- 为了定义上面的函数,我们必须使用位置返回类型。由于位置返回类型在参数列表之后,它可以使用函数的参数。
{
template <typename It>
auto fcn(It b, It e) -> decltype(*b) {
return *b;
}
}
2).进行类型转换的标准库模板类
- 问题,我们有时候需要返回一个元素的值而不是它的引用。但是我们传入的是迭代器,对元素的类型一无所知,并且迭代器解引用只能返回元素的引用。
- 解决,使用标准库的类型转换模板。
- 它们定义在头文件
type_traits 。这个头文件中的类通常用于所谓的模板元程序设计,但是对于普通的编程也很有用。 - 使用
remove_reference 来得到元素类型。它有一个模板类型参数,和一个名为type 的public 类型成员。使用,remove_reference<int &> 则type 成员将会是int 。即如果我们用一个引用类型实例化它,它的type 成员将会表示被引用的类型。 - 如果给定的是一个迭代器。
remove_reference<decltype(*b)>::type 将会得到元素的类型。
{
template <typename T>
auto fcn2(It b, It e) ->
typename remove_reference(*b)::type {
return *b;
}
}
3).标准库类型转换模板。p606
练习
- 16.40,注意
decltype(*b + 0);
- 元素类型必须支持加法的
- 由于
*b + 0 返回的是右值,返回的将会是,const 右值引用
//4.函数指针和实参推断
1).用函数模板初始化一个函数指针或为一个函数指针赋值。
{
template <typename T>
int compare(const T&, const T&);
int (*pf1)(const int&, const int&) = compare;
void func(int (*)(const string&, const string&));
void func(int (*)(const int&, const int&));
func(compare);
func(compare<int>);
}
- 当参数是一个函数模板的实例地址时,程序上下位必须满足,对每一个模板参数,能唯一确定其类型或者值。
//5.模板实参推断和引用
1).左值引用函数参数推断类型
{
template <typename T>
void f1(T &);
f1(i);
f1(ci);
f1(2);
template <typename T>
void f2(const T&);
f2(i);
f2(ci);
f2(5);
}
2).右值引用函数参数推断类型
template <typename T> void f3(T &&); f3(12); 推断出来T为int 3).两个左值赋值给右值引用变量的例外- 这两个例外时
move 标准库正常工作的基础
- 第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数时,且此右值引用指向模板类型参数(T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此
f3(i); 编译器推断出T的类型,int& 而不是int 。这样推断,意味着f3的函数参数应该是一个类型为int& 的右值引用。通常我们不能(直接)定义一个引用的引用。但是通过类型别名或通过模板类型参数间接定义是可以的 - 第二个例外的绑定规则,入股哦我们间接创建一个引用的引用,则这些引用形成了折叠。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只有一种特殊的情况下引用折叠会成右值引用:右值引用的右值引用。
{
X& &, X&& &, X& &&;
X&& &&;
}
3).利用引用折叠规则和右值引用的特殊类型推断规则结合在一起。我们可以对一个左值调用f3.
{
f3(i);
f3(ci);
void f3(int& &&);
void f3<int &>(int &);
}
4).编写接受右值引用参数的模板函数
{
template <typename T>
void f3(T &&val) {
T t = val;
t = fcn(t);
if (val == t) {
}
}
template <typename T> void f(T &&);
template <typename T> void f(const T &);
}
练习,
- 16.45,引用不是对象,它没有地址,指针不能指向它,容器
vector 也不能容纳它。因为vector 本质上就是用指针进行的动态内存的管理。
//6.理解std::move
1).它是使用右值引用模板的一个很好的例子。它可以接受任何实参,它是一个函数模板。 2).如何定义std::move
{
template <typename T>
typename remove_reference<T>::type&& move(T &&t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
string s1("hi!"), s2;
s2 = std::move(string("bye!"));
s2 = std::move(s1);
}
3).以上说明从一个左值到右值的转换是允许的。
- 虽然我们不可以隐式地将左值转换为一个右值引用;但是我们可以使用
static_cast 显式地将一个左值转换为一个右值引用。 - 这样显式地指出,可以方式我们意外地进行这样的左值到右值的截断转换;迫使我们嫩保证这样的转换是安全的。
- 尽管这样的方式是允许的,我们还是统一使用
std::move ,方便排错。
//7.转发
1).某一些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括是否是const 是左值还是右值。
{
template <typename F, typename T1, typename T2>
void flipl(F f, T1 t1, T2 t2) {
f(t2, t1);
}
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
f(42, i);
flipl(f, 42, i);
}
2).以上的调用不能达到我们想要的目的。
- 通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。(左值性,
const 性质。)
- 使用引用参数,使得我们可以保持底层的
const 性质 - 使用右值引用可以通过引用折叠保留实参的左值,右值性质。
{
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2) {
f(t2, t1);
}
}
3).在调用中使用std::forward保持类型信息。
- 它是一个标准库设施。它和
move 一样定义在头文件utility 中;与move 不一样,它必须通过显式模板实参来调用。forward 返回该显示实参的乐星的左值引用。即,forward<T> 的返回值是一个T&& - 通常情况下,我们使用
forward 传递那些定义为模板类型参数的右值引用的函数参数。通过返回类型上的引用折叠,forward 可以保持给定实参的右值或者左值属性。
{
template <typename T>
intermediary(Type &&arg) {
finalFcn(std::forward<Type>(arg));
}
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2) {
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
}
/3.重载与函数模板
1).函数模板可以被另一个模板或一个普通非模板函数重载。名字相同的函数必须又不同数量或者类型的参数。 2).如果涉及到函数模板,函数匹配的规则会在以下方面受到影响。
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的模板总是可行的,因为模板实参推断会会排除不可行的模板。
- 可行函数(模板或者非模板),按照类型转换来排序。但是可用于函数模板的类型转换非常有限。
- 如果且有一个函数提供比其他任何函数都更好的匹配,则选择该函数。但是
- 如果同样好的函数中,只有一个非模板函数,则选择此函数。
- 如果同样好的函数中,没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更加特例化,则选择该模板。
- 否则该调用有歧义。
3).编写重载函数模板
{
template <typename T>
string debug_rep(const T &t) {
ostringstream ret;
ret << t;
return ret.str();
}
template <typename T>
string debug_rep(T *p) {
ostringstream ret;
ret << "pointer: " << p;
if (p)
ret << " " << debug_ret(*p);
else
ret << "null pointer."
return ret.str();
}
string s("hi");
cout << debug_rep(s) << endl;
cout << debug_ret(&s) << endl;
debug_ret(const string* &);
debug_ret(string *);
}
4).多个可行的版本
{
const string *sp = &s;
cout << debug_ret(sp) << endl;
debug_ret(const string * &);
}
5).非模板和模板的重载
{
string debug_ret(const string &s) {
return '"' + s + '"';
}
string s("hello");
cout << debug_ret(s) << endl;
debug_ret<string>(const string &);
debug_ret(const string &);
}
6).重载模板和类型转换
{
cout << debug_ret("hello") << endl;
debug_ret(const T&);
debug_ret(T *);
debug_ret(const string &);
string debug_ret(char *p) {
return debug_ret(string(p));
}
string debug_ret(const char*) {
return debug_ret(string(p));
}
}
7).缺少声明可能导致程序异常
- 在定义任何一个函数之前,记得声明你所需要的所有重载的版本。这样就不必担心,编译器由于未遇到你希望的版本,导致实例化一个你并不需要的版本。
练习
/4.可变参数模板
1).一个可变参数模板就是一个接受可变数目参数的模板函数或者类。可变参数称为参数包。有两个中参数包
- 模板参数包,表示零个或者多个模板参数
- 函数参数包,表示零个或者多个函数参数
2).我们使用省略号来指出一个模板参数或者函数参数的包。在一个模板参数列中,class…或者typename…指出接下来的参数表示零个或者多个类型的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
{
template <typename T, typename... Args>
void foo(const T &t, const Args&... rest);
int i = 0;
double d = 2.22;
string s = "how new";
foo(i, s, 24, d);
foo(s, 42, "hi");
foo(s, d);
foo("hi");
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const string&, const double&);
void foo(const char[3]&);
}
2).sizeof... 运算符
- 当我们需要知道包里面有多少个元素时,可以使用
sizeof... 运算符。类似于,sizeof ,返回一个常量值,而且不会对实参进行求值。
{
template <typename...Args>
void g(Args...args) {
cout << sizeof...(Args) << endl;
cout << sizeof...(args) << endl;
}
}
//1.编写可变参数函数模板
1).解决实参的数目和类型均未知的情况。
- 使用
initializer_list 的所有实参必须有一样的类型或者可以转换为一样的类型。 2).一个例子。 - 可变参数函数通常是递归的,第一步调用,处理包中的第一个实参,然后用剩余实参调用自身。设计的
print 也是这样的模式每一个递归调用将第二个实参打印到第一个实参表示的流中。为了终止递归,我们还需要定义一个非可变参数的print ,他接受一个流和一个对象。
{
template <typename T>
ostream& print(ostream &os, const T &t) {
return os << t;
}
template <typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args &rest) {
os << t << ", "
return print(os, rest...);
print(cout, i, s, 42);
}
}
//2.包扩展
1).对于一个参数包,我们可以获取其大小,还能对他做的唯一一件事情就是**扩展。**当我们扩展一个包时,我们还有提供用于每一个扩展元素的模式。扩展一个包就是将他分解为构成的元素,对每一个元素应用模式,获取扩展后的元素。
{
template <typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args&... rest) {
os...
return print(os, rest...);
}
print(cout, i, s, 42);
ostream&
print(stream &, const int &, const string &, const int &);
print(cout, s, 42);
}
2).理解包扩展
- 以上的
print 例子就仅仅是将包中的元素扩展为其构成的元素,还可以有更加复杂的扩展。
{
template <typename... Args>
ostream& errorMsg(ostream &os, const Args&... rest) {
return print(os, debug_ret(rest)...);
}
errorMsg(cerr, fcnName, code,num(),。。。。。)
print(cerr, debug_ret(rest...));
}
//3.转发参数包
1).在新标准下,我们可以使用可变参数模板于forward 机制来编写函数。 2).以StrVec 中的成员函数emplace_back 作为例子,他是一个可变参数函数,在容器管理内存空间中构造一个元素。
emplace 中参数都是参数模板的右值引用。
{
class StrVec {
public:
template <typename... Args>
void emplace_back(Args&&...);
};
}
- 当
emplace_back 传递参数给construct 时,我们还需要保证,还原参数的类型。
{
template <typename...Args>
inline
void StrVec::emplace_back(Args&&... args) {
chk_n_alloc();
alloc.construct(first_free++, std::forward<Args>(args)...)
}
std::forward<Ti>(ti);
sevc.emplace_back(10, 'c');
std::forward<int>(10), std::forward<char>('c')
svec.emplace_back(s1 + s2);
std::forward<string>(string("the end"));
}
3).所有的可变参数转发,都有和emplace_back 类似的形式。
- 参数是右值引用,可以接受任意的类型参数。
- 配合
forward 可以将参数的类型信息完整的转发。
练习,
- 16.59,是
forward<string&> 结果是string& 答案解析错误。 - 16.61,编写
make_shared
{
template <typename T, typename... Args>
SP<T> make_SP(Args&&... args) {
return SP<T>(new T(std::forward<Args>(args)...));
}
}
/5.模板特例化
1).当我们不能(或者不希望)使用模板版本时,可以定义类或者函数模板的一个版本特例化。
- 编写更加高效的代码
- 解决通用模板无法正确解决或者正确编译的问题。
2).例如,我们希望比较两个字符指针的内容而不是指针值,通用的版本不能做到这一点。
{
template <typename T>
int compare (const T&, const T&);
template <size_t N, size_t M>
int compare(const char (&v1)[N], const char (&v2)[M]);
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);
compare("hi", "mom");
template <>
int compare(const char* const &p1, const char* const &p2) {
return strcmp(p1, p2);
}
template <typename T>
int compare(const T&, const T&);
}
3).函数重载和模板实例化
- 函数重载也可以实现模板实例化一样的功能,为什么要进行特例化???
- 当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原模版的一个特殊实例提供了定义。一个特例化版本本质上是一个实例,而不是一个函数名的一个重载版本。所以特例化不会影响函数匹配。
- 我们将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,会影响到函数匹配。
- 例如,我们定义了两个版本的
compare 函数模板,
{
template <size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);
template <typename T>
int compare(const T*, const T*);
compare("hi", "mom");
}
4).函数模板的特例化不会影响函数匹配过程,但是**它相当于帮助编译器进行了模板的显式实例化过程,当我们使用到该模板的该实例情况时,会调用我们已经特例化的版本,而编译器不会再实例化一个版本。**例如,我们已经定义了一个特例化的接受const char* const & 的版本,当我们传入如上面的字符串时,假如函数匹配到这个接受const T& 的函数模板时,会使用我们特例化的版本,而不是再进行实例化。 5).如果我们没有再使用之前进行特例化的声明,那么不同于普通的类或者函数,有可能不会报错,编译器可以使用原模版进行实例化。这种错误要避免。具体情况待验证。 6).所有的模板和模板特例化声明应该放在同一个头文件中,所有同名的模板声明放在前面,然后是该模板特例化的版本。保证特例化时,函数的模板是可见的。 7).类模板的特例化。
- 特例化标准库
hash ,默认情况下它是使用hash<key_type> 来组织元素。 - 为了让我们自己的数据类型也能使用这样的默认组织形式,必须定义
hash 模板的一个特例化版本。一个特例化版本的hash 必须定义
- 一个重载调用运算符,它接受一个容器关键字类型对象,返回一个
size_t ; - 两个类型成员,
result_type 和argument_type 分别表示调用运算符的返回类型和参数类型 - 默认构造函数和拷贝赋值运算符(可以是隐式定义的)
{
namespace std {
}
namespace std {
template <>
struct hash<Sales_data> {
typedef size_t result_type;
typedef Sales_data argument_type;
size_t operator()(const Sales_data &s) const;
};
size_t
operator()(const Sales_data &s) const {
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
}
}
8).hash 特例化版本的使用。
{
unorder_multiset<Sales_data> SDset;
template<class T> std::hash;
class Sales_data {
friend class std::hash<Sales_data>;
};
}
9).类模板的部分特例化
- 与函数模板不同的是,我们可以为类模板只提供一部分的而不是所有的模板参数,或者是参数的一部分而不是全部的特性。
- 一个类模板的部分特例化本身也是一个模板,使用它的用户还必须为那么特例化中未指定的模板参数提供实参。
{
template <class T> struct remove_reference {
typedef T type;
};
template <class T> struct remove_reference <T&> {
typedef T type;
};
template <class T> struct remove_reference <T&&> {
typedef T type;
};
int i;
remove_reference<decltype(42)>::type a;
remove_reference<decltype(i)>::type b;
remove_reference<decltype(std::move(i))>::type c;
}
10).特例化成员而不是类
{
template <typename T> struct Foo {
Foo(const T &t = T()) : men(t) {}
void Bar() {}
T mem;
};
template <>
void Bar<int>::bar() {
}
Foo<string> fs;
fs.Bar();
Foo<int> fi;
fi.Bar();
}
练习
- 16.63,
string 传递时候,会转换为char* ??
|