C11新特性右值引用&&

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C11新特性右值引用&&相关的知识,希望对你有一定的参考价值。

参考技术A c中,左值其实就是有名字的变量,而运算操作(加减乘除,函数返回值等)就是右值,右值不允许被修改
c++中,相对于右值引入了一个新的概念,基础类型右值不允许被修改,但用户自定义的类型,右值可以通过它的成员函数进行修改

类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。

&是c++里的左值引用
&&是c11里的右值引用
左值引用就不多说了,现在解释一下右值引用(来自知乎[hggg ggg])

使用std::move()接受一个参数,返回该参数对应的右值引用
move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它在调用move后,我们不能对移后源对象的值做任何假设。我们可以销毁一个移后源对象,也可以赋予它新值,但是不能使用一个移后源对象的值。

static_cast是一个强制类型转换符,强制类型转换会告诉编译器:我们知道并且不会在意潜在的精度损失。
如果没有使用强制类型转换符,那么编译器会产生警告
static_cast最神奇的地方在于它可以找回存在于void * t 中的值,如下

forward()接收一个参数,返回该参数本来所对应的类型的引用。(即完美转发)

转载右值引用

【本文转自】:
作者: 苏丙榅
链接: https://subingwen.cn/cpp/rvalue-reference/
来源: 爱编程的大丙

1. 右值引用

1.1 右值

C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。在介绍右值引用类型之前先要了解什么是左值和右值:

  • 左值(l-value - locator value)是指存储在内存中、有明确存储地址(可取地址)的数据;
  • 右值(r -value - read value)是指可以提供数据值的数据(不可取地址);

通过描述可以看出,区分左值与右值的便捷方法是:可以对其取地址(&)就是左值,否则为右值 。所有有名字的变量或对象都是左值,而右值是匿名的。

int a = 520;
int b = 1314;
a = b;

一般情况下,位于 = 前的为左值,位于 = 后边的为右值。也就是说例子中的 a, b 为左值,520、1314为右值。a=b 是一种特殊情况,在这个例子中 a, b 都是左值,因为变量 b 是可以被取地址的,不能视为右值。

C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):

  • 纯右值:非引用返回的临时变量、运算产生的临时变量、原始字面量和 lambda 等
  • 将亡值:与右值引用相关的,比如,T&& 类型函数的返回值、 std::move 的返回值等。

1.2 右值引用

右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

关于右值引用的使用,参考代码如下:

#include <iostream>

int&& value = 520;
class Test
{
public:
    Test()
    {
        std::cout << "construct: my name is jerry" << std::endl;
    }
    Test(const Test& a)
    {
        std::cout << "copy construct: my name is tom" << std::endl;
    }
};

Test getObj()
{
    return Test();
}

int main()
{
    int a1;
    int&& a2 = a1;        // ERROR: \'initializing\': cannot convert from \'int\' to \'int &&\', message : You cannot bind an lvalue to an rvalue reference
    Test& t = getObj();   // ERROR: \'initializing\': cannot convert from \'Test\' to \'Test &\', message : A non-const reference may only be bound to an lvalue
    Test&& t1 = getObj();
    const Test& t2 = getObj();
    return 0;
}
  • 在上面的例子中 int&& value = 520; 里面 520 是纯右值,value 是对字面量 520 这个右值的引用。
  • 在 int &&a2 = a1; 中 a1 虽然写在了 = 右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。
  • 在 Test& t = getObj() 这句代码中语法是错误的,右值不能给普通的左值引用赋值。
  • 在 Test && t = getObj(); 中 getObj() 返回的临时对象被称之为将亡值,t 是这个将亡值的右值引用。
  • const Test& t = getObj() 这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。

2. 性能优化

在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。例如:

#include <iostream>

class Test
{
public:
    Test() : m_num(new int(100))
    {
        std::cout << "construct: my name is jerry" << std::endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        std::cout << "copy construct: my name is tom" << std::endl;
    }

    ~Test()
    {
        delete m_num;
        std::cout << "delete m_num" << std::endl;
    }

    int* m_num{ nullptr };
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    std::cout << "t.m_num: " << *t.m_num << std::endl;
    return 0;
};

程序执行结果如下:

construct: my name is jerry
copy construct: my name is tom
delete m_num
t.m_num: 100
delete m_num

通过输出的结果可以看到调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象 t,在 getObj() 函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了。如果能够使用临时对象已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了,右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高 C++ 应用程序的性能。

使用移动构造函数例子:

#include <iostream>

class Test
{
public:
    Test() : m_num(new int(100))
    {
        std::cout << "construct: my name is jerry" << std::endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        std::cout << "copy construct: my name is tom" << std::endl;
    }

    // 添加移动构造函数
    Test(Test&& a) : m_num(a.m_num)
    {
        a.m_num = nullptr;
        cout << "move construct: my name is sunny" << endl;
    }

    ~Test()
    {
        if (m_num != nullptr)
        {
            delete m_num;
            m_num = nullptr;
            std::cout << "delete m_num" << std::endl;
        }
    }

    int* m_num{ nullptr };
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    std::cout << "t.m_num: " << *t.m_num << std::endl;
    return 0;
};

程序运行结果如下:

construct: my name is jerry
move construct: my name is sunny
t.m_num: 100
delete m_num

通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。

如果不使用移动构造或拷贝构造,在执行 Test t = getObj() 的时候也是进行了浅拷贝,但是当临时对象被析构的时候,类成员指针 int* m_num; 指向的内存也就被析构了,对象 t 也就无法访问这块内存地址了。对象 t 再去释放该内存时,程序就会崩溃,双重释放。

在测试程序中 getObj() 的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果 = 右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。

对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。

3. && 的特性

在 C++ 中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为 T&&,如果是自动类型推导需要指定为 auto &&,在这两种场景下 && 被称作未定的引用类型。另外还有一点需要额外注意 const T&& 表示一个右值引用,不是未定引用类型。

先来看第一个例子,在函数模板中使用 &&:

template<typename T>
void f(T&& param);
void f1(const T&& param);
f(10);
int x = 10;
f(x);
f1(10);

在上面的例子中函数模板进行了自动类型推导,需要通过传入的实参来确定参数 param 的实际类型。

  • 第 4 行中,对于 f(10) 来说传入的实参 10 是右值,因此 T&& 表示右值引用
  • 第 6 行中,对于 f(x) 来说传入的实参是 x 是左值,因此 T&& 表示左值引用
  • 第 7 行中,f1(10) 的参数是 const T&& 不是未定引用类型,不需要推导,本身就表示一个右值引用
int main()
{
    int x = 520, y = 1314;
    auto&& v1 = x;
    auto&& v2 = 250;
    decltype(x)&& v3 = y;   // error
    std::cout << "v1: " << v1 << ", v2: " << v2 << std::endl;
    return 0;
};
  • 第 4 行中 auto&& 表示一个整形的左值引用
  • 第 5 行中 auto&& 表示一个整形的右值引用
  • 第 6 行中 decltype(x)&& 等价于 int&& 是一个右值引用不是未定引用类型,y 是一个左值,不能使用左值初始化一个右值引用类型。

以上是关于C11新特性右值引用&&的主要内容,如果未能解决你的问题,请参考以下文章

右值引用&&

C/C++C++11新特性:初探右值引用与转移语义

重用“&&”标记以进行右值引用的基本原理?

转载右值引用

C++ 11 右值引用以及std::move

[C++11]右值和右值引用