C++11中的universal引用和右值引用

stackoverflow上有个问题:Why “universal references” have the same syntax as rvalue references? 就是说为什么这俩的形式都是T&&(T表示一个类型)。其中有一个回答很好,回答如下:

I think it happened the other way around. The initial idea was to introduce rvalue-references into the language, meaning that “the code providing the double-ampersand reference does not care about what will happen to the referred-to object”. This permits move semantics. This is nice.
Now. The standard forbids constructing a reference to a reference, but this was always possible. Consider:

    template<typename T>
    void my_func(T,T&){...}
    ...
    my_func<int&>(a,b);

In this case the type of the second parameter should be int & &, but this is explicitly forbidden in the standard. So the references have to be collapsed, even in C++98. In C++98, there was only one kind of reference, so the collapsing rule was simple:
& & -> &
Now, we have two kinds of references, where && means “I don’t care about what may happen to the object”, and & meaning “I may care about what may happen to the object, so you better watch what you’re doing”. With this in mind, the collapsing rules flow naturally: C++ should collapse referecnces to && only if no one cares about what happens to the object:
& & -> &
& && -> &
&& & -> &
&& && -> &&
With these rules in place, I think it’s Scott Meyers who noticed that this subset of rules:
& && -> &
&& && -> &&
Shows that && is right-neutral with regards to reference collapsing, and, when type deduction occurs, the T&& construct can be used to match any type of reference, and coined the term “Universal reference” for these references. It is not something that has been invented by the Committee. It is only a side-effect of other rules, not a Committee design.
And the term has therefore been introduced to distinguish between REAL rvalue-references, when no type deduction occurs, which are guaranteed to be &&, and those type-deduced UNIVERSAL references, which are not guaranteed to remain && at template specialization time.

也就说,其实并没有什么universal引用,它只是C++引用折叠规则的一个副产物罢了。而且这个一般universal引用只能在模板中发挥作用,一旦一个非模板函数的参数类型声明成T&&的形式,那个T已经是固定的了,所以T&&就是固定的,也就只能表示右值引用。而在模板中,那个T是不确定的,如果T是一个左值引用类型,即T = S&,那么T&&就等价于S& &&,根据上面的引用折叠规则,& && -> &,那么S& &&类型就是S&,也就是左值引用,其他同理。

下面看一个例子:

void f(int&& a) {
    std::cout << "rvalue" << std::endl;
}

void f(int& a) {
    std::cout << "lvalue" << std::endl;
}

template<typename T>
void test(T&& a) {
    f(forward<T>(a));
}

template<typename T>
T&& forward(std::remove_reference_t<T>& arg)
{
    return static_cast<T&&>(arg);
}

int main()
{

    int a = 10;
    test(a);
    test(std::move(a));

    //system("pause"); //for windows VS to pause to see the output
    return 0;
}

这里实现了一个非常非常简单的完美转发(perfect forwarding)。我们下面一步一步分析一下代码的运行过程:

  • 首先声明一个int变量,这是一个左值
  • 然后调用test(a)。a的类型是int,你可能认为test函数模板应该实例化成如下,毕竟传递进去的是一个int类型:
//typename T 被推断成 int
void test(int&& a){
    f(forward<int>(a));     
}
  • 但是如果是这样,就不对了啊,明明传递进去一个左值,而test模板对其实例化的结果居然接收一个右值引用类型,这肯定不对啊!所以,当参数是一个左值时,在模板类型推断的时候,左值会被推断成左值引用,不管传递的参数是左值类型还是左值引用,通通变成左值引用。于是正确的模板实例应该是这样:
//typename T 被推断成 int&
void test(int&  a){   //int& && -> int&
    f(forward<int&>(a));        
}
  • 这样的话,就进入了forward函数,其模板参数被指定成int&,forward被实例化成这样:
//typename T 是int&,
int& forward(int& arg)  //std::remove_reference_t<int&> 就是 int
{
    return static_cast<int&>(arg);   //int& && -> int&,返回类型也是一样的原理
}
  • 于是将a类型转换成一个左值引用,传递给f,所以调用的是接收左值引用类型的f重载,打印lvalue
  • 然后再调用main函数里面的test(std::move(a))。std::move的作用就是把一个变量转换成一个右值,里面做的工作其实就是一个类型转换。test和forward模板实例化成这样:
//typename T 被推断成 int
void test(int&&  a){  
    f(forward<int>(a));     
}
//typename T 是int,
int&& forward(int& arg)  //std::remove_reference_t<int> 就是 int
{
    return static_cast<int&&>(arg);  
}
  • 所以返回一个右值引用,从而调用接收右值引用的f重载,打印出rvalue。

这里简要说明一下为什么需要完美转发。因为在test函数里面,其参数a是一个左值,不管它前面的类型是右值类型还是左值类型,它都是一个左值。而如果用这个a调用f的话,只能调用f的左值重载版本,永远调用不到其右值重载版本。至于为什么a是一个左值,简单说就是只要有名字的变量都是左值,具体可以参考右值引用。而完美转发则可以实现传递进来什么类型,我就返回什么类型,所以就可以利用其实现对不同重载版本的f的调用。

下面我简单修改一下mian函数:

...     //as before
int main()
{
    int a = 10;
    int &b = a;
    int&& c = std::move(a);
    test(b);//这里b是一个左值引用
    test(c);//c是一个右值引用

    //system("pause"); //for windows VS to pause to see the output
    return 0;
}

这里,有两点想说明:

  1. b是一个左值引用,但是正如前面所说,只要是左值,test模板的推断类型都是int&,所以test(b)调用的是f的左值版本,打印lvalue
  2. c虽然是一个右值引用,但是它的的确确是一个有名字的变量,那么它实际上就是一个左值,所以test(c)调用的也是f的左值版本,打印lvalue
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值