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
再调用f
,f
的参数v2
被绑定到t1
,就能改变传入参数的值了。
这个版本的flip
仍然只解决了一半问题。他对于接受一个左值引用的函数(就是f
这样的)工作做得很好,换句话说,flip2
可以完美的保持参数的左值属性,但如果要他保持参数的右值属性,他则完全不能工作。例如:
void g(int&& v1, int& v2)
{
cout << i << " " << j << endl;
}
flip2(g, i, 42); 会报错,完全不能工作
为什么会报错???不至于吧??
看flip2
的第三个参数,是个右值,传进flip2
的函数参数t2
,编译器推断出T2
为int
类型(引用折叠原理),所以t2
就是int&&
类型。
直到这里,都完全符合我们的预期。但是当把t2
传入函数g
时,就出现饿了问题。t2
是作为第一个参数传入g
的,也就是说:要向g
的右值引用参数 传递一个左值,这就错了啊!不能把右值引用绑定到一个左值上。要用实实在在的右值 给函数g
中的右值引用参数传参。
这该怎么办?
1.2 使用std::forward
保持类型信息
forward
可以保持传入实参的左右值属性。
类似move
,forward
也定义在头文件utility
中。与move
不同,forward
必须通过显式模板实参调用(forward<T>
)。
对于任意给定的显式模板实参T
,forward<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&
类型传递给g
,42
将以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...
获取参数包中元素的数量
类似sizeof
,sizeof...
也返回一个常量表达式,且不会对其实参求职:
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
}
第一个扩展操作
第一个扩展操作扩展模板参数包Args
,为print
生成函数参数列表。编译器将模式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
确保有足够空间容纳一个新元素,然后调用construct
在first_free
指向的位置创建一个新元素。construct
中的扩展模式为:
forward<Args>(args)...
它既扩展了模板参数包Args
,也扩展了函数参数包args
。此模式生成了如下形式的元素:
通过在此调用中使用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)...));
}