万能引用和完美转发

1.前置知识:

        在了解万能引用和完美转发之前需要了解左值,右值,左值引用,右值引用和引用折叠。下面简单概括一下这些名词,关于每个名词的详细解释网上有很多非常好的文章,故不在本文赘述。

1.1 左值和右值:

        简单的说可以取地址的就是左值,如变量。反之就是右值,如常数。仅通过是否能出现在等号左边并不能完全区分左值,如"a=b"这种。此外有些类禁用了'='操作符,然而它们仍是左值。

1.2 左值引用和右值引用:

        右值引用是C++11提出的,因此之前的引用均为左值引用。C++11采用“&&”操作符来进行右值引用,如“int && a=10”。绑定到右值以后右值的生存期会延长至与绑定到它的右值引用的生存期。

1.3 引用折叠:

        引用折叠简单的说就是多个引用叠加的规则,C++不允许对引用复引用,因此当多个引用叠加具有一下规则:

& + & -> &
& + && -> &
&& + & -> &
&& + && -> &&

进而可以得出多个引用叠加只要有一个左值引用,那么最终的结果就是左值引用。

2.万能引用:

        万能引用是一种写法,使得一个函数可以同时接受左值引用或右值引用为参数。

2.1 万能引用之前:

        在引入万能引用之前,我们写接收引用的函数需要如下代码:

void func(int& a) {
    std::cout << "left" << '\n';
}

void func(int&& a) {
    std::cout << "right" << '\n';
}

        我们需要写两个函数,以便可以使用func(a)和func(1)。而万能引用的存在使得仅需写一个函数即可同时支持这两种写法。这也是为什么需要万能引用。

2.2 万能引用:

        上面的函数进行以下写法即为万能引用:

template<typename T>
void func1(T&& a)
{
    
}

        为了验证这个函数是否能够接收左值和右值我们可以测试一下:

#include <iostream>

void func(int& a) {
    std::cout << "left" << '\n';
}

void func(int&& a) {
    std::cout << "right" << '\n';
}

template<typename T>
void func1(T&& a)
{
    func(a);
}

int main()
{
    int a = 3;
    func1(3);
    func1(a);

    return 0;
}

        结果如下:

a4ae9806760b47f4b9e8597b6752df72.png

         可见这左值和右值都能够成功接收。而为什么输入了一个右值和一个左值,结果却都调用了左值的函数,这便是为什么要有完美转发。简单的说就是func1里面将a作为左值传递给了func,因此输出的都是左值。解决方式可以在第三章完美转发查看,本章先实现万能引用。

2.3 万能引用原理:

        万能引用原理就是因为引用折叠,以上述代码为例,当调用func1(3)时,由于接受的是常数,因此模板类型T被推导为int,因此func1等价于void func1(int&& a),可以接收右值引用。当调用func1(a)时,由于a是左值,因此T被推导为int&,函数等价于void func1(int& && a)。由于1.3的引用折叠得知函数等价于void func1(int& a),可以接收左值引用。

2.4 万能引用的注意事项:

2.4.1 只有T被推导时才是万能引用:

        由于2.3不难得出万能引用的实现原理是类型推导,因此非模板函数或者实例化时无需推导则无法成为万能引用。下面例子可以看出

//func和func1仍与2.2相同

int main()
{
    int a=3;
    func1(a);//正确
    func1(3);//正确
    func1<int>(a);//编译报错
    return 0;
}

        func1<int>(a)错误的原因很简单,在a传递前模板类型T已经被实例化为void func1(int && a),成为了只能接收右值引用的函数,而a是左值,因此毫无疑问会产生报错。

2.4.2 看到&&注意万能引用:

        根据上面可以看出C++11及其之后标准的代码中看到&&修饰的参数并不一定是右值引用,同样的有T&&的万能引用也要在实例化时候注意不要让它变成右值引用。如果不注意前者有可能在写重载函数时出现问题,下面举一个简单例子

#include <iostream>

template<typename T>
void func(T& a) {
    std::cout << "left" << '\n';
}

template<typename T>
void func(T&& a) {
    std::cout << "right" << '\n';
}

int main()
{
    int a = 3;
    func(3);//编译通过
    func(a);//编译报错,因为上述重载的两个函数均可以接收左值
    func<int>(a);
    //编译通过,在2.4.1中得出此时第二个func由万能引用变为仅接收右值引用的函数,不会产生歧义

    return 0;
}

        在此例子,本身只是想通过模板类实现func,但是由于万能引用的存在,使得func(a)时产生歧义。在复杂的程序中,这种错误并不一定会被很容易的发现且可能产生其它问题。

3.完美转发:

        在2.2中我们已经发现了当函数接收右值时也会变成左值,这是因为任何的函数内部,对形参的直接使用,都是按照左值进行的。因此func1(a)传递的一定是左值。为了解决这个问题引用了完美转发。

3.1 完美转发的使用:

        完美转发使用std::forward函数,以修改后2.2的程序为例

#include <iostream>

void func(int& a) {
    std::cout << "left" << '\n';
}

void func(int&& a) {
    std::cout << "right" << '\n';
}

template<typename T>
void func1(T&& a)
{
    func(std::forward<T>(a));
}

int main()
{
    int a = 3;
    func1(3);
    func1(a);

    return 0;
}

        结果如下:

c1bc1dfa18b040adadddc365bfeeea5c.png

        可以看到成功将’3‘以右值形式传给了func。至于可不可以使用std::forward<int>(a)我们在3.2中分析完源码即可分析。

3.2 forward源码分析:

3.2.1 源码简化

        forward源码如下

  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

        核心内容简化伪代码如下

  template<typename _Tp>
  _Tp&& forward(_Tp& __t)
    { return static_cast<_Tp&&>(__t); }

  template<typename _Tp>
   _Tp&& forward(_Tp&& __t) 
    {
      return static_cast<_Tp&&>(__t);
    }

3.2.2 正确设置forward的模板参数        

        首先重点强调以下forward(a)这种未传递模板参数的用法会报错。因为任何的函数内部,对形参的直接使用,都是按照左值进行的。因此a无论是什么,在使用时始终是左值,因此失去了完美转发的意义。所以正确使用必须得使用下列写法:

template<typename T>
void func1(T&& a)
{
    func(std::forward<T>(a));
}

        此时T是a的原始类型,forward里面的参数a为左值引用。forward函数就是通过T来推导的。

3.2.3 正确设置模板参数的情况

        正确设置模板参数后将会出现以下情况:

        a.原始类型为左值引用,传入参数为左值引用。

        b.原始类型为左值引用,传入参数为右值引用。

        c.原始类型为右值引用,传入参数为左值引用。

        d.原始类型为右值引用,传入参数为右值引用。

        但是实际上分析,这两个forward函数的返回值仅由_Tp决定,因此我们只需要关注传入的T的属性是左值引用还是右值引用。而且根据引用折叠传入的属性一定和返回的属性一致。因此完美转发除了返回正确的属性外,我们也可以传递指定的_Tp来传递想要的属性。如forward<int>(a),这样返回的一定是右值。不过move也有强制右值的作用。因此还是尽量传递原始属性来实现这个函数特有的意义。

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值