C++ 模板与泛型编程 《C++Primer》第16章(中下)———— 读书笔记

1、转发

某些函数需要将其一个或多个实参连同实参的类型完全不变的转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const,以及实参是左值还是右值

作为一个例子,下面将编写一个函数,共三个参数,一个可调用对象 和 两个实参。我们的函数把后两个实参,逆序传递给可调用对象。函数的雏形如下:

template<typename F, typename T1, typename T2> void flip1(F f, T1 t1, T2 t2)
{
	f(t2, t1);
}

该函数一般情况下可正常工作,但用它调用一个接受引用参数的f 就会出现问题:

void f(int v1, int& v2)
{
	cout << v1 << " " << ++v2 << endl;		f 要改变其中一个参数
}

f(42, i);			f 改变了i
flip1(f, i, 42);	通过 flip1,f就不能改变 i 了
					这实例化的其实是 void flip1(void(*)(int, int&), int, int);
					最后两个参数是普通的非引用int,这不能改变它们的值

上面的flip1模板中,函数的形参都是实参拷贝得到的,根本不可能改变传入的参数。换言之,flip1根本没有做到,转发时保持被转发实参的左右值属性。

1.1 定义能保持类型信息的函数参数

flip的函数参数,要能保持给定实参的左右值性,为了应对所有情况,const属性也应该保持。
那么怎么做呢?

通过把函数参数定义为绑定到模板类型参数的右值引用(T&&),就可以保持其对应实参的所有类型信息。
这是利用了引用折叠。

template<typename F, typename T1, typename T2> void flip2(F f, T1&& t1, T2&& t2)
{
	f(t2, t1);
}

flip2(f, i, 42);

这回再调用flip2编译器推断出T1的类型为int&flip2再调用ff的参数v2被绑定到t1,就能改变传入参数的值了。
在这里插入图片描述
这个版本的flip仍然只解决了一半问题。他对于接受一个左值引用的函数(就是f这样的)工作做得很好,换句话说,flip2可以完美的保持参数的左值属性,但如果要他保持参数的右值属性,他则完全不能工作。例如:

void g(int&& v1, int& v2)
{
	cout << i << " " << j << endl;		
}

flip2(g, i, 42);	会报错,完全不能工作

为什么会报错???不至于吧??
flip2的第三个参数,是个右值,传进flip2的函数参数t2,编译器推断出T2int类型(引用折叠原理),所以t2就是int&&类型。
直到这里,都完全符合我们的预期。但是当把t2传入函数g时,就出现饿了问题。t2是作为第一个参数传入g的,也就是说:要向g的右值引用参数 传递一个左值,这就错了啊!不能把右值引用绑定到一个左值上。要用实实在在的右值 给函数g中的右值引用参数传参。

这该怎么办?

1.2 使用std::forward保持类型信息

forward可以保持传入实参的左右值属性。
类似moveforward也定义在头文件utility中。与move不同,forward必须通过显式模板实参调用(forward<T>)。
对于任意给定的显式模板实参Tforward<T>总返回形如T&&的类型。实际的返回类型,根据推断出的T的类型进行引用折叠

现有一个函数模板:
template<typename T> void func(T&& t)
{
	std::forward<T>(t)	// ......
	// .........
}
  • 若传入模板的实参是左值,那么T本身被推断为左值引用类型,所以t是左值引用类型(引用折叠)。此情况下forward返回T&&,经过引用折叠,返回的是左值引用类型。
  • 若传入的模板实参是右值,那么T本身被推断为普通(非引用)类型,所以t是右值引用类型。此情况下forward返回T&&,所以返回的就是右值引用,而且也确实是个右值。

在这里插入图片描述
有了forward,再次重写flip函数:

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&类型传递给g42将以int&&类型传递给g


综上,进行函数转发时,保持类型信息是一个两阶段的过程。

  • 第一阶段:保持实参中的类型信息,要把函数的实参定义为绑定到模板参数类型的右值引用
  • 第二阶段:函数体内,把函数参数传递给其他函数时,必须用forward保持实参的原始类型。

2、重载与模板

函数模板可以被其他函数模板普通函数重载。名字相同的函数,必须具有不同数量或类型的参数。
涉及函数模板的函数匹配规则:

  • 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例
  • 与往常一样,所有可行函数按类型转换来排序。当然,可用于函数模板调用的类型转换很少
  • 如果有一个函数提供比其他函数都好的匹配,那就选他。
    但是,如果有多个函数提供同样好的匹配,则:
  • ——同样好的函数中,只一个是非模板函数,就选这个非模板的。(非模板比模板更特例化
  • ——同样好的函数中,全是模板函数,选择最特例化的那个模板实例
  • ——否则,二义性,报错。

什么是最特例化的?看下面的例子。

2.1 模板之间的重载

作为例子,下面定义一组函数(他们在调试中可能很有用),把这些函数命名为debug_rep,每个函数都返回给定对象的string表示。
首先编写最通用最不特例化)的版本,他接受一个const对象的引用:

template<typename T> string debug_rep(const T& t)
{
	ostringstream ret;	定义一个字符流
	ret << t;			使用 T 的输出运算符把 t 的一个表示形式写入字符流中
	return ret.str();	返回字符流绑定的 string 的一个副本
}
此函数用于生成一个对象的 string 表示,对象可以是任意具备 << 运算符的类型

下面,咱再定义一个debug_rep,让他们构成重载。这回,我们定义返回存有指针内容string的版本:

template<typename T> string debug_rep(T* p)
{
	ostringstream ret;
	ret << "pointer: " << p;			把指针本身的值写入字符流
	if (p)
		ret << " " << debug_rep(*p);	把指针指向的值写入字符流
	else
		ret << " null pointer ";		空,就写入表示空的语句
	return ret.str();
}
此版本生成的string,包含指针本身的值,以及调用debug_rep()获得的指针指向的值

注:此函数模板不能用于打印字符指针因为IO库为char*值定义的<<版本,假定指针指向的是空字符结尾的字符数组,并打印数组的内容,而非地址值。

好了,两个模板都定义好了,调用他们试试吧:

string s("hi");
cout << debug_rep(s) << endl;

很显然,对于上面的调用,只有第一个版本是可行的,因为第二个版本要求指针参数,这里传入的是对象,实参推断失败。
那么,再考虑如下调用呢:

cout << debug_rep(&s) << endl;

对于这个调用,两个版本都能生成可行的实例:

  • 第一个版本实例化了 string debug_rep(const string*&);T被推断为string*
  • 第二个版本实例化了 string debug_rep(string*);T被推断为string

第一个版本要进行普通指针到const指针的转换,所以,第二个版本是此调用的精确匹配

如果两个都能精确匹配呢?

考虑下面的调用:

const string* sp = &s;
cout << debug_rep(sp) << endl;

对于这个调用,两个版本都能生成可行实例,且都是精确匹配

  • 第一个版本生成 string debug_rep(const string*&);T被推断为string,无需类型转换
  • 第二个版本生成 string debug_rep(const string*);T被推断为const string,也无需类型转换

此时,正常的匹配规则无法区分调用哪个。但是,根据重载函数模板的特殊规则,这时应该调用第二个版本,即,更特例化的版本
debug(const T&)本质上可以用于任何类型包括指针类型。此模板debug_rep(T*)更通用后者只能用于指针类型,更加特例化。最终,根据规则,选择更特例化的版本。
在这里插入图片描述

2.2 非模板 和 模板的重载

作为例子,下面定义一个普通非模板的·debug_rep,他返回双引号包围的string

string debug_rep(const string& s)
{
	return '"' + s + '"';
}
进行如下调用:
string s("hi");
cout << debug_rep(s) << endl;

仍然有两个可行的版本:

  • 第一个函数模板实例化了 string debug_rep<string>(const string&);T被绑定到string
  • string debug_rep(const string&); ,非模板函数

仍然是根据选择最特例化的版本的规则,应该选择调用非模板函数
在这里插入图片描述

2.3 重载模板 和 类型转换

目前为止,还有一种情况没有处理,那就是当传入一个C风格字符串和字符串字面常量时,会调用哪个版本?

cout << debug_rep("hi world!") << endl;

上面的三个版本其实都是可行的:
首先明确,我们传入的字符串常量是const char[10]字符串数组类型

  • 第一个版本实例化为 string debug_rep(const char[10]&)T被推断为 char[10] ,就是字符串数组类型
  • 第二个版本实例化为 string debug_rep(const char*)T被推断为 const char
  • 第三个版本中,实参要从 const char[10] 转换为 string

——第一个版本可以精确匹配,无需任何类型转换。
——第二个版本需要进行一次数组到指针的转换但是,对于函数匹配来说,这种转换被认为是精确匹配。所以说相当于没有发生类型转换。
——对于第三个非模板函数,要发生const char[10]string的转换。

显然,这回非模板函数不是精确匹配,不可能调用它。而前两个函数模板都可以精确匹配,最终会选择第二个版本,因为它更特例化

如果我们本来希望字符串数组按照string处理,可以定义另外的非模板重载版本,并且他们必须是精确匹配

string debug_rep(char* p)
{
	return debug_rep(string(p));
}

string debug_rep(const char* p)
{
	return debug_rep(string(p));
}

在这两个重载版本里,我们手动使用传入的字符串数组构造string对象,然后调用string版本的debug_rep。这方法简直是太妙了。

2.4 缺少声明可能导致编译器实际使用的重载版本不是我们想用的

使用刚刚的例子就可以说明。
为了使最后定义的char*版本的debug_rep正常工作,在定义这两个版本之前,必须有debug_rep(const string&的声明或定义。否则,实际调用的debug_rep可能不是我们所想的版本:

template<typename T> string debug_rep(const T&);
string debug_rep(const string&);		如果没有本行声明,且这个函数被定义在debug_rep(const char* p)的下面会怎样??
string debug_rep(const char* p)
{
	return debug_rep(string(p));
}

上面代码中,如果没有那行声明,则debug_rep(const char* p)函数体中调用的将是,debug_rep(const T&)T被实例化为string的版本。
在这里插入图片描述

3、可变参数模板

3.1 什么是可变参数模板

可变参数模板就是一个接收可变数目参数的模板函数或模板类。可变数目的参数被称为参数包
有两种参数包:

  • 模板参数包:放在尖括号里,表示零个或多个模板参数
  • 函数参数包:放在函数参数列表中,表示零个或多个函数参数

使用 ... 指出一个模板参数或函数参数表示一个包。在模板参数列表中class...typename... 指出接下来的参数是模板参数包;函数参数列表中,如果一个参数的类型是模板参数包,则此参数是个函数参数包。例如:

template<typename T, typename... Args> void foo(const T&, const Args& ... rest);

Args是一个模板参数包,rest是一个函数参数包
在这里插入图片描述

3.2 sizeof...获取参数包中元素的数量

类似sizeofsizeof...也返回一个常量表达式,且不会对其实参求职:

template<typename... Args> void func(const Args&... rest)
{
	cout << sizeof...(Args) << endl;	类型参数的数目
	cout << sizeof...(rest) << endl;	函数参数的数目
}

3.3 编写可变参数函数模板

之前我们用initializer_list定义接受可变数量实参的函数,但要求所有元素类型一致。当我们及不知道元素数量,也不知道元素类型时,就使用参数包吧。
下面将定义一个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...);		rest... 这是包扩展,下一节讲
}
给定调用:
print(cout, i, s, 42);

在这里插入图片描述
前两个调用只能与可变参数版本的print版本匹配。对于最后一次递归调用print(cout, 42),两个版本都可精准匹配,但是非可变参数版本更特例化,因此调用非可变参数版本。
在这里插入图片描述
在这里插入图片描述

3.4 包扩展

我们能对参数包执行的操作只有两个:

  • sizeof...获取包中元素个数
  • 扩展

扩展就是打开参数包,把包中的每个元素都一个一个列出来。
当扩展一个包时,还要提供应用于每个扩展元素的模式对每个元素应用模式,才是最终扩展后的列表
在这里插入图片描述
通过在模式右边放一个省略号(...)来触发扩展操作
上面定义的print函数其实就包含两个扩展操作

template<typename T, typename... Args> vostream& print(ostream& os, const T& t, const Args&... rest)	扩展 Args
{
	os << t << " ";
	return print((os, rest...);			扩展 rest
} 
第一个扩展操作

第一个扩展操作扩展模板参数包Argsprint生成函数参数列表。编译器将模式const Args& 应用到模板参数包 Args中的每个元素。因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的(形参)列表,每个类型都形如const type& 。例如:

print(cout, i, s, 42);		包中有两个参数

最后两个实参的类型和模式一起确定了后两个参数的类型,该调用实例化了:

ostream& print(ostream&, const int&, const string&, const int&);
第二个扩展操作

第二个扩展操作发生在对print的递归调用中。此时,模式是函数参数包的名字rest,此模式扩展出一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于:

print(os, s, 42);
深入理解包扩展

print中的函数参数包扩展仅仅是将包扩展为其参数列表的构成元素。C++还允许更复杂的扩展模式。例如,下面编写第二个可变参数函数,对每个实参调用debug_rep,然后调用print打印结果string

template<typename... Args> ostream& ErrorMsg(ostream& os, const Args&... rest)
{
	return print(os, debug_rep(rest)...);
}

这个print调用使用了模式debug_rep(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("other"), debug_rep(item));

注意,下面的调用,会报错:

print(os, debug_rep(rest...));		这样的调用没有匹配函数

该调用没有匹配函数!为什么???
因为,你在debug_rep参数列表里面按照rest模式扩展了参数包,它等价于:

print(os, debug_rep(fcnName, code,num(), otherData, "other", item));	debug_rep()哪能接收这老些参数啊??

debug_rep的参数是不可变的,而且没有哪个debug_rep能接受五个参数。
在这里插入图片描述

3.5 转发参数包

把之前的转发机制 和 刚刚学的参数包扩展结合起来使用,可以完成很了不得事情。
我们组合使用可变参数模板与forward机制编写函数,实现吧函数实参不变的传递给其他函数。作为例子,下面将为StrVec类添加一个emplace_back成员。
(标准库容器的emplace_back将其参数复制移动到当前容器的末尾,如果容量不够了,则它会调用allocator::construct在内存空间中新分配一片空间,且标准库的emplace_back含有可变参数)

参考上对面标准库的emplace_back成员的描述,我们自定义的版本也应实现类似的功能。首先,我们的emplace_back版本应该含有可变实参,其次,我们希望使用拷贝构造,移动构造等多种构造方式,还需要保持传递给emplace_back的实参的所有类型信息。

如之前所学的,保持类型信息是一个两阶段的过程。首先,为了保持实参中的类型信息,必须把emplace_back函数的参数定义为 绑定到模板类型参数的右值引用

class StrVec
{
public:
	template<typename... Args> void emplace_back(Args&&... args);
};

其次,emplace_back把这些实参传递给construct必须用forward 保持实参的原始类型:

template<typename... Args> inline void StrVec::emplace_back(Args&&... args)
{
	chk_n_alloc();		如果需要的话,就重新分配内存空间
	alloc.construct(first_free++, std::forward<Args>(args)...);
}

emplace_back先调用chk_n_alloc确保有足够空间容纳一个新元素,然后调用constructfirst_free指向的位置创建一个新元素。construct中的扩展模式为:

forward<Args>(args)...

既扩展了模板参数包Args也扩展了函数参数包args 。此模式生成了如下形式的元素:
去word里整个公式,截图
通过在此调用中使用forward,就能保证当传入emplace_back的参数是一个右值时construct也会得到一个右值,这样就能保证传入右值时使用移动构造函数

在这里插入图片描述
实际上,标准库中vector::emplace_back的声明 形式上和我们自定义的emplace_back确实差不多。标准库的声明如下:

template <class... Args>
  void emplace_back(Args&&... args);

例题一
在这里插入图片描述
make_shred的工作过程类似emplace_back。他接受参数包,经过扩展,转发给new,作为vector的初始化参数。下面给出标准库make_shared的声明:

template <class T, class... Args>
  shared_ptr<T> make_shared(Args&&... args);

例题二
在这里插入图片描述

template<typename T, typename... Args> sp<T> make_sp(Args&&... args)
{
	return sp<T>(new T(std::forward<Args>(args)...));
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值