1. 右值引用和移动语义

1.1 左值和右值

  • 左值 local value:存储在内存中、有明确存储地址(可寻址)的数据(x、y、z)
  • 右值 read value:不一定可以寻址,例如存储于寄存器中的数据;通常字面量都是右值,除了字符串常量(1、3)
int x = 1;
int y = 3;
int z = x + y;

 对于x++和++x虽然都是自增操作,但x++编译器首先生成一份临时值,然后对x自增,最后返回临时内容,所以x++是右值;++x是对x递增后返回自身,所以++x是左值

x++;    // 右值引用
++x; // 左值引用
int *p1 = &x++; // 右值引用所以编译失败
int *p2 = &++x; // 左值引用可以编译成功

1.2 左值引用和右值引用

  • 左值引用:必须引用一个左值。
  • 常量左值引用:可以引用左值或右值。可以引用右值的原理是延长右值的生命周期,但这种引用存在一个问题,常量左值引用导致无法修改对象内容。
int &x1 = 7;         // 非常量左值引用 编译错误
const int &x = 11; // 语句结束后,11的生命周期被延长了
const int x = 11; // 语句结束后,11立刻被销毁
  • 右值引用:引用右值且只能引用右值的方法
int &&k = 11;    // 右值引用(延长右值的生命周期)

  对于数字的表示可能不太清晰,因为数字本身就有些虚无缥缈,下面用一个类的例子来更好解释右值引用的优势,可以减少复制构造来优化性能(但实际上编译器会帮我们优化)

#include <iostream>
using namespace std;
class MyClass {
public:
char* pc;
MyClass();
MyClass(const MyClass& myclass);
~MyClass() { cout << "dtor" << endl; delete pc; }
void show() { cout << "Show: " << pc << endl; }
}; MyClass::MyClass() {
cout << "ctor" << endl;
pc = new char[10];
for (int i = 0; i < 10; i++)
pc[i] = 'c';
} MyClass::MyClass(const MyClass& myclass) {
cout << "copy ctor" << endl;
pc = myclass.pc; // **浅复制**, 编译器不优化则返回值调用赋值构造函数, 接下来析构原对象的时候这里内存会指向空内存!!!
for (int i = 0; i < 10; i++)
pc[i] = myclass.pc[i];
} MyClass make_myclass() {
MyClass mc; // 1.构造函数 3.析构函数, 析构这里的mc对象(此时如果是浅复制,新的对象指向的内存也将为空, 引发指针异常)
return mc; // 2.拷贝函数, 返回拷贝的对象
} int main() {
MyClass&& mc = make_myclass(); // 返回值的生命周期被延长
mc.show();
cout << endl;
MyClass mc2 = make_myclass(); // 再次调用拷贝构造函数
mc2.show();
}
  • 优化前:MyClass&& mc = make_myclass(); 调用后

    • ctor: 调用mc的构造函数
    • copy ctor:返回值时调用mc的拷贝函数(因为没有重写移动构造函数,所以返回值时才会调用拷贝构造函数)
    • dtor: 返回值后将mc销毁调用析构函数(注意这里的复制构造函数会存在问题:如果是浅复制,此时销毁的对象会把堆区内存销毁导致新的对象空引用,所以还是强调必须重写复制构造函数)
    • 由于右值引用,延长了右值的生命周期
    • dtor: main函数结束再调用一次析构函数

  • 编译器优化后:MyClass&& mc = make_myclass(); 注意其实编译器优化会解决一些潜在的问题,当然我们只要有堆内存操作都必须重写拷贝构造函数,就解决了这个问题,详情看代码注释

    • ctor: 调用构造函数
    • dtor: 调用析构函数

  • MyClass mc = make_myclass(); 该函数调用后

    • ctor: 调用mc的构造函数
    • copy ctor: 返回值 调用mc的拷贝函数
    • dtor: 返回值后销毁mc 调用析构函数
    • copy ctor: 为了构造mc2 调用构造函数
    • dtor: 销毁返回值临时变量 调用析构函数
    • dtor: main函数结束后再调用一次析构函数

1.3 移动语义

  上面其实已经用到了移动语义,移动语义主要就是解决C++复制构造对性能的影响。但也存在问题,例如移动构造函数运行过程中发生了异常,这会造成源对象和目标对象都不完整。这里再用一个例子说明,该Useless类内有一个元素个数为 n 的 char 数组,静态变量 ct 记录了对象个数。(下面的流程基于未优化-fno-elide-constructors编译,否则编译器会自己优化掉移动构造函数的部分)

  • Useless one(20, 'o'); 调用 int char 构造函数
  • Useless one(20, 'c'); 调用 int char 构造函数
  • Useless three(one + two);
    • one + two 调用 operator+ 运算符重载,在内部调用 int 构造函数构造了对象 temp
    • 返回值时调用移动语义构造函数(未优化的情况下,实际上因为重写了移动构造函数这里才会调用),夺走 temp 里指针指向的内容并把它的指针设置为空,这样它在销毁时不会把堆区内存清空
    • 临时对象 temp 被销毁
class Useless {
public:
int n; // 元素个数
char* pc; // 数据指针
static int ct; // 对象个数
void ShowObject()const;
Useless(int k);
Useless(int k, char ch);
Useless(Useless&& f); // 移动构造
~Useless();
Useless operator+(const Useless& f)const;
void ShowData() const;
};
int Useless::ct = 0;
Useless::Useless(int k) :n(k) {
printf("int 参数的构造函数; 对象个数为: %d\n", ++ct);
pc = new char[n];
ShowObject();
}
Useless::Useless(int k, char ch) :n(k) {
printf("int char参数的构造函数; 对象个数为: %d\n", ++ct);
pc = new char[n];
for (int i = 0; i < n; i++){
pc[i] = ch;
}
ShowObject();
}
Useless::Useless(Useless&& f) :n(f.n) {
printf("移动构造函数; 对象个数为: %d\n", ++ct);
pc = f.pc;
f.pc = nullptr;
f.n = 0;
ShowObject();
} Useless::~Useless() {
printf("析构函数调用; 元素个数为: %d\n", --ct);
ShowObject();
delete[] pc;
} Useless Useless::operator+(const Useless& f)const {
printf("进入 operator+\n");
Useless temp = Useless(n + f.n);
for (int i = 0; i < n; i++)
temp.pc[i] = pc[i];
for (int i = n; i < temp.n; i++)
temp.pc[i] = f.pc[i - n];
printf("离开 operator+\n");
return temp;
} void Useless::ShowObject() const {
printf("元素个数: %d, 数据地址: %x\n", n, (void*)pc);
} void Useless::ShowData()const {
if (n == 0)
printf("元素个数为空\n");
else
for (int i = 0; i < n; i++)
printf("%c ", pc[i]);
printf("\n");
} int main()
{
Useless one(20, 'o'); // int char 构造函数 对象个数1
printf("\n");
Useless two(20, 'c'); // int char 构造函数 对象个数2
printf("\n");
Useless three(one + two); // 1. operator+ 调用 int 构造函数 对象个数3; 2. operator+ 返回右值, 调用移动构造函数(减少了复制的次数) 对象个数4; 3.临时对象被销毁 对象个数3
printf("\n");
printf("object one: \n");
one.ShowData();
printf("object two: \n");
two.ShowData();
printf("object three: \n");
three.ShowData();
printf("\n");
}

1.4 强制移动

  移动构造函数和移动赋值运算符都必须使用右值,但如果让他们使用左值就需要一些特殊处理

Useless choices[10];
Useless best;
int pick = 5;
best = chioces[pick]; // 由于这里是左值,所以会调用普通的复制赋值运算符

  可以使用C++11头文件utility中提供的move函数来实现将左值转换为右值,但是注意右值的字段会被夺走,并且必须定义了移动赋值运算符或移动构造函数

#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << v[0] << ", " << v[1] << "\n";
}

2. 万能引用

  很多时候我们希望传递的是一个引用而非通过拷贝构造传递,这可以提高程序效率;但仅仅通过fn(className& c)来传递引用会导致不能传递右值,fn(const className& c)又会导致传递进来的参数不能被修改,所以提出了万能引用的概念

2.1 引用折叠

  万能引用实际上就是发生了类型推导,如果源对象是一个左值,则推导出左值引用;如果源对象是一个右值,则推导出右值引用。

void foo(int &&i) {}    // i为右值引用
template<class T> void bar(T &&t) {} // t为万能引用
template<class T> void bar(vector<T> &&t) {} // 非万能引用,必须是直接的T
int get_val() {return 5;}
int &&x = get_val(); // x 为右值引用
auto &&y = get_val(); // y 为万能引用

  C++11 通过一套引用叠加推导的规则来实现万能引用——引用折叠,可以注意到实际类型是左值引用,则最终类型一定是左值引用;只有引用类型是一个非引用类型或者右值引用,最后推导出来的才是一个右值引用

  通过下面几行代码理解引用折叠,首先是C++11规定的展开时的定义

  • 实参类型为T的左值, 则模板 T 展开为 T&

    • 此时Test形参的类型为 T& &&,经过折叠后为 T& 左值引用
    • 此时static_cast<T&>(t) 将t转为左值引用,所以调用左值引用的函数
  • 实参类型为T的右值, 则模板 T 展开为 T

    • 此时Test形参的类型为 T &&,所以为右值引用
    • 此时static_cast<T&&>(t) 将t转为右值引用,所以调用右值引用的函数
#include <iostream>
void process(int& i) {
std::cout << "左值引用" << std::endl;
}
void process(int&& i) {
std::cout << "右值引用" << std::endl;
}
template<class T>
void Test(T&& t) {
process(static_cast<T&&>(t));
}
int main() {
int a = 1;
Test(a); // C++11规定 实参类型为T的左值, 则模板T展开为int&
Test(1); // C++11规定 实参类型为T的右值, 则模板T展开为int
}

2.2 完美转发

  通过 std::forward<T>() 可以实现完美转发,不论左值还是右值都可以通过引用的方式传参,提高程序运行的效率。下面给出了一个完美转发的例子,打印了 T 的实际类型,并通过修改 t 的值实现了修改 a 的值(传入左值即左值引用),同样如果传入类的右值一样是右值引用。

#include <iostream>
template<class T>
void show_Type(T&& t) {
std::cout << "is int&: " << std::is_same_v<T, int&> << std::endl;
std::cout << "is int : " << std::is_same_v<T, int> << std::endl;
t = 10;
} template<class T>
void perfect_forwarding(T&& t) {
show_Type(std::forward<T>(t));
} int main()
{
int a = 5;
perfect_forwarding(5); // 该参数在不同函数间始终以右值引用方式传递
perfect_forwarding(a); // 该参数在不同函数间始终以左值引用方式传递
std::cout << a; // 由于以引用的方式传递, 这里内存中的数值也一样会修改
}

3. move 和 forward

  • std::move接受一个对象,并允许您将其视为临时对象(右值)。尽管这不是语义要求,但是通常,接受对右值的引用的函数会使它无效。当看到时std::move,表明该对象的值以后不应再使用,但是仍然可以分配一个新值并继续使用它
  • std::forward有一个用例:将模板化的函数参数(在函数内部)转换为用于传递它的调用方的值类别(左值或右值)。这允许将右值参数作为右值传递,并将左值作为左值传递,这是“完美转发”的方案

【C++ Primer Plus】C++11 深入理解右值、右值引用和完美转发的更多相关文章

  1. C++ 左值与右值 右值引用 引用折叠 => 完美转发

    左值与右值 什么是左值?什么是右值? 在C++里没有明确定义.看了几个版本,有名字的是左值,没名字的是右值.能被&取地址的是左值,不能被&取地址的是右值.而且左值与右值可以发生转换. ...

  2. Effective Modern C++:05右值引用、移动语义和完美转发

    移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引 ...

  3. C++11(列表初始化+变量类型推导+类型转换+左右值概念、引用+完美转发和万能应用+定位new+可变参数模板+emplace接口)

    列表初始化 用法 在C++98中,{}只能够对数组元素进行统一的列表初始化,但是对应自定义类型,无法使用{}进行初始化,如下所示: // 数组类型 int arr1[] = { 1,2,3,4 }; ...

  4. 第16课 右值引用(3)_std::forward与完美转发

    1. std::forward原型 template <typename T> T&& forward(typename std::remove_reference< ...

  5. (原创)C++11改进我们的程序之move和完美转发

    本次要讲的是右值引用相关的几个函数:std::move, std::forward和成员的emplace_back,通过这些函数我们可以避免不必要的拷贝,提高程序性能.move是将对象的状态或者所有权 ...

  6. c++11 标准库函数 std::move 和 完美转发 std::forward

    c++11 标准库函数 std::move 和 完美转发 std::forward #define _CRT_SECURE_NO_WARNINGS #include <iostream> ...

  7. [转][c++11]我理解的右值引用、移动语义和完美转发

    c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的 ...

  8. C++11之右值引用(一):从左值右值到右值引用

    C++98中规定了左值和右值的概念,但是一般程序员不需要理解的过于深入,因为对于C++98,左值和右值的划分一般用处不大,但是到了C++11,它的重要性开始显现出来. C++98标准明确规定: 左值是 ...

  9. [C++11]_[0基础]_[左值引用声明和右值引用声明]

    场景: 在 remove_reference 结构体中能看到右值引用的身影 &&, 那么这里的右值引用究竟有什么用呢? 常常也发现int& 和int&& 这两种 ...

  10. [c++11]右值引用、移动语义和完美转发

    c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的 ...

随机推荐

  1. 202402 湖北武汉 4D3N3P

    202402 湖北武汉 4D3N3P D0 / 10 杭州出发 普速列车25T Z47 杭州-武昌 城站22:22开 第3候车室 这趟列车是武汉局"华东三直"中的其中一列,另外两列 ...

  2. 普通继电器 vs 磁保持继电器 vs MOS管:工作原理与电路设计全解析

    普通继电器 vs 磁保持继电器 vs MOS 管:工作原理与电路设计全解析 0.引言 在智能控制系统中,我们经常会遇到这样的问题:如何用一个微弱的控制信号,驱动一台高功率设备? 比如,单片机的输出口通 ...

  3. CSP-S 2020模拟训练题1-信友队T2 挑战NPC

    题意简述 有一个\(k\)维空间,每维的跨度为\(L\),即每一维的坐标只能是\(0,1, \cdots ,L-1\).每一步你可以移动到任意一个曼哈顿距离到自己小于等于\(d\)的任意一个合法坐标. ...

  4. kubernetes集群之资源配额(Resource Quotas)

    一.简单介绍 资源配额(Resource Quotas)是用来限制用户资源用量的一种机制. 它的工作原理为: 资源配额应用在Namespace上,并且每个Namespace最多只能有一个Resourc ...

  5. 使用 SpringBoot 集成 WebService [需要身份验证]

    使用 JDK 自带的 wsimport 工具生成实体类 1.1 创建身份验证文件(用于 Webservice 身份验证-auth.txt # 格式 http://账号:密码@wsdl地址 # 案例 h ...

  6. TableFill:一天搞定1000人的数据填报工作丨2024袋鼠云秋季发布会回顾

    10月30日,袋鼠云成功举办了以"AI驱动,数智未来"为主题的2024年秋季发布会.大会深度探讨了如何凭借 AI 实现新的飞跃,重塑企业的经营管理方式,加速数智化进程. 会上,易知 ...

  7. [2025.5.11 鲜花/rain] 非适应性白日梦

    [2025.5.11 鲜花/rain] 非适应性白日梦 感觉人类太可悲了,连一些动物最基本的本能反应都不被允许 猫的应激反应是被允许的,人类的就是不被允许的,甚至 应激这一词的定义,对大部分人来说,都 ...

  8. HarmonyOS NEXT仓颉开发语言实战案例:外卖App

    各位周末好,今天为大家来仓颉语言外卖App的实战分享. 我们可以先分析一下页面的布局结构,它是由导航栏和List容器组成的.幽蓝君目前依然没有找到仓颉语言导航栏的系统组件,还是要自定义,这个导航栏有三 ...

  9. 下一代 2D 图像设计工具「GitHub 热点速览」

    长期以来,2D 设计领域似乎已是 Adobe 与 Figma 的天下,层叠的图层.熟悉的工具栏,一切都显得那么顺理成章,却也让不少设计师在创意的边界上感到了些许乏力.当我们以为设计工具的革新只能是小修 ...

  10. [c++算法] 树的直径,包教包会!

    哈喽大家好,我是 doooge.今天我们要将数论中的一个算法-树的直径. \[\Huge 树的直径 详解 \] 1.树的直径是什么 这是一棵图论中的树: 这棵树的直径就是这棵树中最长的一条简单路径. ...