【模板进阶】模板的万能引用
一、类型的区别和基本定义
1.1类型的基本定义
首先先看一个最简单例子:
//类型的区别和基本定义
void func(const int& abc) { } //abc的类型为const int&
这里的 a b c abc abc被推导为什么类型? 显然可见,为 c o n s t i n t & const \ int\& const int&类型,上面不都写着呢!
然后,我们在看看一个简单的模板:
template<typename T>
void func(const T& abc) {}
void Test1() {
func(10); //T为int,abc为const int&
}
这里传入的 10 10 10显然是转换为了 i n t int int类型,那么 T = i n t , a b c = c o n s t i n t & T = int,abc = const\ int\& T=int,abc=const int&,这是容易推导出来的。
二、万能引用的基本认识
U n i v e r s a l R e f e r e n c e Universal \ Reference Universal Reference后来被称为 F o r w a r d i n g R e f e r e n c e ( 转发引用 ) Forwarding \ Reference \ (转发引用) Forwarding Reference (转发引用),翻译成中文有好几种方式,这里我们采用一种最常用的名称:“万能引用”。
万能引用这个概念是许多概念的基础,所以这里率先将这个概念阐述清楚。首先记住一个结论:
万能引用是一种类型。就跟int是一种类型一个道理,再次强调,万能引用,是一种类型。
2.1右值引用
万能引用和右值引用形式上长得相似,但实际上是两种类型,这里先介绍一下右值引用。
我们知道,右值是绑定在用 & & \&\& &&符号表示,主要是用于绑定右值,如:
//右值引用
void myfunc(int&& tmpr) {
std::cout << tmpr << "\n";
}
测试一下:
void Test2() {
myfunc(10); //正确,右值
int i = 100;
myfunc(i); //错误,左值无法绑定到右值引用上
}
这里,容易发现, m y f u n c ( i ) myfunc(i) myfunc(i)这一行编译失败了,原因就是右值引用无法绑定在左值上。
2.2 万能引用
这里,我们改造一下 m y f u n c myfunc myfunc函数,把它写成一个函数模板:
template<typename T>
void myfunc(T&& tmpr) {
std::cout << "调用了函数模板:" << tmpr << "\n";
}
然后我们再同样调用相同的函数:
void Test2() {
myfunc(10); //正确,右值
int i = 100;
myfunc(i); //编译成功
}
可以发现,此时编译成功了。
现在,我们看到的事实有两条:
(
1
)
(1)
(1)这里的函数模板中的
t
m
p
r
v
tmprv
tmprv参数能接受左值(作为实参),也能接受右值;
(
2
)
(2)
(2)
t
m
p
r
v
tmprv
tmprv的类型是
T
&
&
T\&\&
T&&(这两个地址符是属于
t
m
p
r
v
tmprv
tmprv的),编译都没报错。
实际上,这里使用到的就是万能引用,这里的 T T T类型在传入左值或者右值的时候,会被推断为不同的类型,通用编译期的折叠引用最终推断出正确的引用类型来接受参数。
同时,这里的引用类型是能够被修改的,如下:
//万能引用
template<typename T>
void myfunc(T&& tmpr) {
std::cout << "调用了函数模板:" << tmpr << "\n";
tmpr = 0; //tmpr是左值,可以被修改
}
void Test1(){
myfunc(i); //正确,万能引用
std::cout << i << "\n";
i = 200;
myfunc(std::move(i));
std::cout << i << "\n";
}
我们调用一下,可以发现无论是左值还是右值,都可以被修改,因为接收实参的形参本身是一个左值,它可以被修改:
万能引用 ( U n i v e r s a l R e f e r e n c e ) (Universal Reference) (UniversalReference)又称为未定义引用,这种引用离不开上面提到的两种语境,这两种语境必须同时存在:
(
1
)
(1)
(1)必须是函数模板;
(
2
)
(2)
(2)必须是发生了模板类型推断并且函数模板形参形如
T
&
&
T\&\&
T&&。
万能引用的格式为&T&&&。它也用两个地址符号 & & \&\& &&表示,所以,万能引用的形式就与右值引用一模一样。但是解释起来却不一样(注意语境,只有在语境满足的条件下,才能把 & & \&\& &&解释为万能引用而不是往右值引用)。
( 1 ) (1) (1)右值引用作为函数形参时,实参必须传递右值进去,不然编译器报错,上面已经看到了。
(
2
)
(2)
(2)而万能引用作为函数形参时,实参可以传递左值进去,也可以传递右值进去。所以,万
能引用叫作未定义引用。如果传递左值进去,那么这个万能引用就是一个左值引用;如果传递右值进去,那么这个万能引用就是一个右值引用。从这个角度来讲,万能引用更厉害,是一种中性的引用,可以摇身一变,变成左值引用;也可以摇身一变,变成右值引用。
三、万能引用的辨认
万能引用和右值引用通常很相似,我们下面举几个例子来讨论一下:
3.1 例子1
这是右值引用,万用引用必须在模板的条件下:
//右值引用
void myfunc(int&& tmpr) {
std::cout << tmpr << "\n";
}
3.2 例子2
这是右值引用,因为在类模板实例化的同时, T T T也就被确认了,因此是右值引用
template<typename T>
class myclass {
public:
void func(T&& tmpr) { //不是万能引用
std::cout << tmpr << "\n";
}
};
3.3 例子3
这个是万能引用,这里的 U U U类型是独立的,不受类模板的影响
template<typename T>
class myclass {
public:
template<typename U>
void func2(U&& tmpr) { //万能引用
std::cout << tmpr << "\n";
}
};
3.4 例子4
形如 a u t o & & auto \&\& auto&&的也是万能引用:
//万能引用
template<typename T>
void myfunc(T&& tmpr) {
auto&& tmpr2 = tmpr; //万能引用
std::cout << "调用了函数模板:" << tmpr << "\n";
}
四、万能引用资格的剥夺
4.1剥夺
如果在万能引用中使用 c o n s t const const修饰,那么这个万能引用资格就会被剥夺,从而退化为右值引用,例如:
//剥夺
template<typename T>
void myfunc_const(const T&& tmpr) { //退化为右值引用
std::cout << tmpr << "\n";
//tmpr = 0; //const修饰不能被修改
}
void Test4() {
int i = 100;
//myfunc_const(i); //错误,只能传入右值
myfunc_const(std::move(i)); //std::move转为右值
std::cout << i << "\n";
}
这时,我们 m y f u n c _ c o n s t ( i ) myfunc\_const(i) myfunc_const(i)就会编译错误,因为此时已经是右值引用了。并且由于 c o n s t const const的修饰,我们无法在函数内修改参数。