通用引用、引用折叠与完美转发问题

一、通用引用:
通用引用(universal reference)是Scott Meyers在C++ and Beyond 2012演讲中自创的一个词,用来特指一种引用的类型。这种引用在源代码中(“T&&”)看起来像右值引用,但是它们可以表现左值引用(即“T&”)的行为。它们的双重性质允许它们绑定右值(就像右值引用那样)和左值(就像左值引用那样)。而且,它们可以绑定const或者非const对象,可以绑定volatile和非volatile对象,还可以绑定const和volatile同时作用的对象。它们实际上可以绑定任何东西。构成通用引用有两个条件:

  • 必须精确满足T&&这种形式(即使加上const也不行)
  • 类型T必须是通过推断得到的(最常见的就是模板函数参数)

(第一个例外)比如将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断,模板类型参数为:实参的左值引用类型

template<typename T> void f(T&&);
int i = 1;
f(i);//编译器推导出T为int&类型
f(10);//推导出T为普通类型int

二、引用折叠:
(第二个例外)通常不能直接定义引用的引用(引用非实体),但是通过类型别名或模板类型参数间接定义是可以的,但这时引用会形成“折叠”,例如上例中,当T被推导为int&,f的参数类型变为int& &&(无效代码,用于演示),引用折叠规则:

  • X& &, X& &&, X&& &折叠为:X&
  • X&& &&折叠为:X&&

于是最后我们得到的f的形参类型是int&类型,即当我们用左值对f进行调用时,实际是用传引用的方式在调用函数。

三、std::move的工作原理:
由以上知识,就可以解释一下std::move的工作原理了:

template<typename T>
typename remove_reference<T>::type&& move(T&& t)
{
  return static_cast<
         typename remove_reference<T>::type&&>(t);
}

首先,move函数的参数类型是通用引用T&&,可以绑定任意类型参数,其次,返回值是remove_reference<T>的type成员类型的右值引用,比如T被推导为int或者int&,则remove_reference<int&>::type为int类型,返回值类型为int&&,最后,函数体中static_cast内的转换过程类似,虽然不能隐式的将一个左值转换为一个右值引用,但是通过static_cast显式转换时允许的(把左值截断问题缩小在使用std::move代码的范围内)。

四、完美转发:
有时候,某些函数需要将其实参连同类型(const、左值、右值等属性)不变的转发给其他函数。

template<typename F,typename T>
void sender(F receiver, T t) //sender函数,接受一个可调用对象和一个模板参数类型的参数
{
  receiver(t); //sender需要将自己的参数t转发给receiver函数
}

一般情况下这个函数能工作,但是当它调用一个接受引用类型参数的函数时就会有问题:

void rec(int& i) { ++i;}
int j = 1;
rec(j); 
cout << j << endl; //输出j为2;

//但是通过sender调用时:
template<typename F,typename T>
void sender(F receiver, T t) 
{
    receiver(t); 
}
sender(rec, j);
cout << j << endl; //输出j为1

其原因是j传递给sender函数,推断出T为int类型而非引用,j的值是被拷贝到形参t中的,因此对t值的改变不会反应到j中。

这时联想到我们讲的通用引用,将模板参数类型定义为T&&,接受左值时,T会被推断为左值引用类型,经过一次引用折叠,得参数t的类型为左值引用,它对应实参的const属性和左值、右值属性都将得到保持:

template<typename F, typename T>
void sender(F receiver, T&& t) //通用引用
{
    receiver(t);
}

void rec(int& i)
{
    ++i;
}
sender(rec, j);
cout << j << endl; //OK!输出j为2

但是这儿又会遇到另一个问题:

template<typename F, typename T>
void sender(F receiver, T&& t)
{
    receiver(t);
}

void rec(int&& i) //现在rec接受一个右值引用参数
{
  ++i;
}

int j = 1;
sender(rec,j);//错误:无法从一个左值实例化int&&
sender(rec,1);//错误:无法从一个左值实例化int&&

当我们试图对一个接受右值引用的函数转发参数时,会报以上错误,不论我们传递给sender函数的是一个左值还是右值。原因是传递给rec函数中形参i的是sender函数中的参数t函数参数和其他变量一样都是左值表达式!所以会出现将左值绑定到右值引用的错误。

这时需要用到forward函数来保证:当sender函数接受一个右值实参,转发给rec函数时仍然能保持其右值属性。forward函数定义在<utility>头文件中。

//forward函数
//lvalue (1)    
template <typename Type> Type&& forward (typename remove_reference<Type>::type& arg) noexcept
{   
    return (static_cast<Type&&>(arg));
}
//rvalue (2)    
template <typename Type> Type&& forward (typename remove_reference<Type>::type&& arg) noexcept
{   
    return (static_cast<Type&&>(arg));
}

forward函数的工作原理:由arg接受的实参类型推断出Type类型。

forward函数必须通过显式模板实参来调用,它跟通用引用配合可以保存原始实参的所有特性,回到我们的例子:

template<typename F, typename T>
void sender(F receiver, T&& t)
{
    receiver(std::forward<T>(t));
}

当传递给sender的是一个右值——比如10时,推断出来的T是一个普通类型即int,传给forward的形参arg的实参就是一个int,从而推出Type是int,这时调用std::forward<int>(t)返回的是int&&,保存了原实参的右值属性。
当传递给sender的是一个左值——比如j时,这时我们推断出来的T应该是一个int&了,调用std::forward<int&>(t)返回int & &&(无效代码,演示)折叠成为int&,保存了原实参的左值属性。

template<typename F, typename T>
void sender(F receiver, T&& t)
{
    receiver(std::forward<T>(t));
}

void rec1(int i)
{
    ++i;
}
void rec2(int& i)
{
    ++i;
}
void rec3(int&& i)
{
    ++i;
}
int main()
{
    int i = 1, j = 1, k = 1;
    /*
    rec1(i);
    cout << i << endl; //输出1
    */

    /*
    rec2(j);
    cout << j << endl; //输出2
    */

    /*
    rec3(std::move(k));
    cout << k << endl; //输出2
    */

    sender(rec1, i);
    cout << i << endl;//输出1
    sender(rec2, j); 
    cout << j << endl;//输出2
    sender(rec3, std::move(k));
    cout << k << endl;//输出2
}
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值