C++右值引用

来源:《深入应用C++11 代码优化与工程级应用》

右值引用可以避免无谓的复制,提高程序性能。

相应的,C++11的容器中还添加了一些右值版本的插入函数。

1.&&的特性

右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。

无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。

通过右值引用的声明,该右值又重获新生,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活在,该右值临时量就会一直存活下去。


cpp 复制代码
#include <iostream>
using namespace std;

int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
struct A
{
    A()
    {
        cout << "construct:" << ++g_constructCount << endl;
    }
    A(const A& a)
    {
        cout << "copy construct:" << ++g_copyConstructCount << endl;
    }
    ~A()
    {
        cout << "destruct:"<<++g_destructCount << endl;
    }
};
A GetA()
{
    return A();
}

int main()
{
    A a = GetA();
    cout << "---" << endl;
    return 0;
}
在GCC下编译时设置编译选项-fno-elide-constructors来关闭返回值优化效果。

g++ -fno-elide-constructors main.cpp -o demo.exe

在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是GetA() 函数内部创建的对象返回后构造一个临时对象产生的。

在C++中,当函数返回一个对象时,如果这个对象是通过值传递的,则会经历复制构造过程

即:copy construct
当GetA()调用时,它创建了一个局部对象A,调用构造函数,输出construct:1

当GetA()返回时,这个局部对象的生命周期即将结束,因此需要创建一个临时对象来保存返回值,此时调用复制构造函数,输出copy construct:1

局部对象在GetA()函数作用域结束时被销毁,调用析构函数,输出destruct:1

使用临时对象对a进行初始化,调用了拷贝构造函数,输出copy construct:2

执行完A a = GetA();然后临时对象被销毁,输出destruct:2

return 0;a被销毁,输出destruct:3


cpp 复制代码
A a;     //调用构造函数
A b = a; //调用拷贝构造函数
b = a;   //调用赋值运算符 

拷贝构造函数与赋值运算符的区别:

1.调用时机

拷贝构造函数在创建新对象时调用。

赋值运算符在给已存在对象赋值时调用。

2.资源管理

拷贝构造函数通常需要分配新的资源

赋值运算符通常需要释放旧资源,然后分配新资源

3.返回类型

拷贝构造函数没有返回类型,它创建并返回一个新对象

赋值运算符返回一个指向调用对象的引用。

4.默认行为

如果没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,执行成员的逐成员拷贝

如果没有显式定义赋值运算符,编译器会生成一个默认的赋值运算符,执行成员的逐成员赋值。

cpp 复制代码
class MyClass {
public:
    MyClass() { /* ... */ }
    MyClass(const MyClass& other) { /* 拷贝构造函数 */ }
    MyClass& operator=(const MyClass& other) { /* 赋值运算符 */ return *this; }
    // ...
};

如果开启返回值优化:输出:

返回值优化会将临时对象优化掉。

这不是C++标准,是各编译器的优化规则。

cpp 复制代码
#include <iostream>
using namespace std;

int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
struct A
{
    A()
    {
        cout << "construct:" << ++g_constructCount << endl;
    }
    A(const A& a)
    {
        cout << "copy construct:" << ++g_copyConstructCount << endl;
    }
    ~A()
    {
        cout << "destruct:"<<++g_destructCount << endl;
    }
};
A GetA()
{
    return A();
}

int main()
{
    A&& a = GetA();
    cout << "---" << endl;
    return 0;
}

g++ -std=c++11 -fno-elide-constructors main.cpp -o demo.exe
比之前少了一次拷贝构造和一次析构。

GetA()返回的对象没有再用来拷贝构造对象a,而是使用右值引用绑定了右值,让临时右值的生命周期延长了。我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构。


事实上,在C++98/03中,通常常量左值引用也经常来做性能优化。将上面的代码改成:

cpp 复制代码
    const A& a = GetA();

输出结果与右值引用一样,因为常量左值引用是一个"万能"的引用类型,可以接收左值,右值,常量左值,常量右值。需要注意的是普通的左值引用不能接受右值:

cpp 复制代码
    A& a = GetA();

编译错误:类型A的右值不能初始化非const类型的引用


实际上T&&并不是一定表示右值,它绑定的类型是未定的,既可能是左值又可能是右值。

cpp 复制代码
#include <iostream>
using namespace std;

template<typename T>
void f(T&& param)
{

}

int main()
{
    f(10);  //右值
    int x = 10;
    f(x);   //x是左值
    return 0;
}

上面的例子中有&&,这表示param实际上是一个未定的引用类型。该类型被称为universal references(可以认为是一种未定的引用类型)(universal:全体的) ,它必须被初始化,它是左值还是右值引用取决于它的初始化,如果&&被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值。

需要注意的是,只有当发生自动类型推断时,如函数模板的类型自动推导,或auto关键字,&&才是一个universal references。

cpp 复制代码
template<typename T>
void f(T&& param)   //这里的T的类型需要推导,所以&&是一个universal references
{

}

class Test
{
    Test(Test&& rhs);   //已经定义了一个特定的类型,没有类型推断,&&是一个右值引用
};

void f(Test&& rhs); //已经定义了一个确定的类型,没有类型推断,&&是一个右值引用

更复杂的例子:

cpp 复制代码
template<typename T>
void f(std::vector<T>&& param);

param是什么类型?

是右值引用类型,因为在调用这个函数之前,这个vector<T>中的推断类型已经确定了,所以到调用的时候就没有类型推断了。


cpp 复制代码
template<typename T>
void f(const T&& param);

这个param是universal references吗?其实它是一个右值引用类型。universal references仅仅在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用。
由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或右值引用的参数初始化,这时经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化被称为引用折叠。C++11中的引用折叠规则如下:

1)所有的右值引用叠加到右值引用上仍然还是一个右值引用。

2)所有的其他引用类型之间的叠加都将变成左值引用。


左值或右值是独立于它的类型的,右值引用可能是左值也可能是右值。比如下面的例子:

cpp 复制代码
    int&& var1 = 10;        //var1类型:int&&
    auto&& var2 = var1;     //var2存在类型推导,因此是一个universal reference,这里auto&& 最终被推导为int&

var1既是一个右值引用,var1本身也是一个左值(因为它有名字),此时auto被折叠为int&。


cpp 复制代码
    int w1;
    auto&& v1 = w1;

v1是一个universal reference,它被一个左值初始化,所以它最终是一个左值引用。


cpp 复制代码
    int a = 10;
    int&& b = a;

使用一个左值初始化右值引用是不合法的,会导致编译错误。

可以使用std::move:

cpp 复制代码
    int a = 10;
    int&& b = std::move(a);

std::move可以将一个左值转换为右值。

------

编译器将已命名的右值引用视为左值,而将未命名的右值引用视为右值。

cpp 复制代码
#include <iostream>
using namespace std;

void PrintValue(int& i)
{
    std::cout << "lvalue:" << i <<std::endl;
}
void PrintValue(int&& i)
{
    std::cout << "rvalue:" << i <<std::endl;
}

void Forward(int&& i)
{
    PrintValue(i);
}

int main()
{
    int i = 0;
    PrintValue(i);
    PrintValue(1);
    Forward(2);
    return 0;
}

lvalue:0

rvalue:1

lvalue:2
在Forward中调用PrintValue时,右值i变成了一个可命名的对象,编译器会将其当作左值处理。

相关推荐
single5941 小时前
【c++笔试强训】(第四十五篇)
java·开发语言·数据结构·c++·算法
yuyanjingtao1 小时前
CCF-GESP 等级考试 2023年9月认证C++五级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
魔法工坊2 小时前
只谈C++11新特性 - 删除函数
java·开发语言·c++
海螺姑娘的小魏3 小时前
Effective C++ 条款 26:尽可能延后变量定义式的出现时间
开发语言·c++
w(゚Д゚)w吓洗宝宝了3 小时前
C++ 环境搭建 - 安装编译器、IDE选择
开发语言·c++·ide
王老师青少年编程3 小时前
gesp(二级)(16)洛谷:B4037:[GESP202409 二级] 小杨的 N 字矩阵
数据结构·c++·算法·gesp·csp·信奥赛
机器视觉知识推荐、就业指导4 小时前
C++设计模式:解释器模式(简单的数学表达式解析器)
c++·设计模式·解释器模式
海螺姑娘的小魏4 小时前
Effective C++ 条款 16:成对使用 `new` 和 `delete` 时要采取相同形式
开发语言·c++
点云SLAM5 小时前
C++创建文件夹和文件夹下相关操作
开发语言·c++·算法
CodeClimb6 小时前
【华为OD-E卷 - 猜字谜100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od