c++11--左值,右值,移动语义,引用折叠,模板类型推断,完美转发

1.移动语义
移动构造和移动赋值均属于移动语义范畴。
移动语义的实现依赖于右值概念,右值引用。
1.1.一个移动构造的实例

#include <iostream>
using namespace std;
class HasPtrMem{
public:HasPtrMem():d(new int(3)){cout << "Construct: " << ++n_cstr << endl;}HasPtrMem(const HasPtrMem& h):d(new int(*h.d)){cout << "Copy construct: " << ++n_cptr << endl;}HasPtrMem(HasPtrMem&& h):d(h.d){h.d = nullptr;cout << "Move construct: " << ++n_mvtr << endl;}~HasPtrMem(){delete d;cout << "Destruct: " << ++n_dstr << endl;}int *d;static int n_cstr;static int n_dstr;static int n_cptr;static int n_mvtr;
};int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;
HasPtrMem GetTemp(){HasPtrMem h;// 1.局部对象构造cout << "Resource from " << __func__ << ": " << hex << h.d << endl;return h;
}// 3.局部对象析构int main()
{// 2.匿名对象拷贝构造--实参为即将析构的局部变量时匹配到右值版本// 4.对象a拷贝构造,实参为匿名临时对象时匹配到右值版本HasPtrMem a = GetTemp();// 3.匿名对象析构cout << "Resource from " << __func__ << ": " << hex << a.d << endl;// 4.对象a析构
}

对其执行:g++ -std=c++11 44.cpp -fno-elide-constructors -o 44,再执行./.44
在这里插入图片描述
上述以函数内临时变量为实参拷贝构造匿名临时变量,以匿名临时变量为实参拷贝构造变量a时均触发了移动拷贝构造。
移动拷贝构造的形参是一个右值引用类型。

我们需要先理解:
(1). 左值,右值。
(2). 左值引用,常量左值引用,右值引用作为形参时可接收的实参。及各类实参和形参匹配的优先级。

才能较好的把握移动拷贝,移动赋值的使用。
从理解上来说,针对需要管理资源的类型,我们将拷贝和赋值区分为浅拷贝,浅赋值和深拷贝,深赋值。
浅指的是我们通过窃取目标对象资源来完成资源转移。深指的是我们自己产生资源,然后使其和目标对象内容一致。
这种场景下,类型通过提供移动拷贝,移动赋值来实现浅拷贝,浅赋值。通过提供拷贝构造,赋值来实现深拷贝,深赋值。

1.2.左值,右值
c++程序中,所有的值必属于左值,将亡值,纯右值三者之一。
将亡值,纯右值均属于右值范畴。

(1). 纯右值
和C中右值概念一致。
a. 非引用返回的函数返回的匿名临时变量值。
b. 一些运算表达式,诸如 1 + 3 产生的匿名临时变量值。
c. 不跟对象关联的字面量值,比如:2、‘c’、true。
d. 类型转换函数返回的匿名临时变量值。
e. lambda表达式返回的匿名临时变量值。

(2). 将亡值
C++特有。
a. 返回右值引用T&&的函数返回值–匿名右值引用类型。
b. std::move的返回值–匿名右值引用类型。
c. 转换为T&&的类型转换函数的返回值–匿名右值引用类型。

(3). 左值
排除(1),(2)后的可标识函数,对象值都属于左值。

1.3.非常量左值引用,常量左值引用,非常量右值引用,常量右值引用
常量和非常量,左值和右值是可以同时用于修饰对象的两类性质。

右值存在的价值一个是用于实现移动语义,一个是用于实现完美转发。
一般,常量右值引用没有存在的意义,因为:
(1). 右值用于移动语义时,我们窃取其资源后,需要对其修改以便阻止资源多次释放,野指针等问题。
(2). 如果需要引用右值且让右值不可通过引用更改,直接使用常量左值来引用即可。

使用右值作为实参来初始化右值引用,常量左值引用后,右值的生命期会被延长。
通过右值引用可以对引用对象进行修改,通过常量左值引用无法对引用对象进行修改。

#include <iostream>class A
{
public:A(){printf("A()_%x\n", this);}A(const A& a){printf("A(const A&_%x)\n", this);}~A(){printf("~A()_%x\n", this);}
public:int i = 0;
};A fun()
{A a;return a;
}int main()
{// 这里的变量除了a1,还有一个隐藏起来的匿名临时变量。// 汇编执行时:// 是匿名临时变量先接受了函数的返回值// 再通过匿名临时变量完成了a1的初始化// 这里函数内部临时变量a,变量a1均是左值// 匿名临时变量是右值A a1 = fun();printf("tag1\n");A&& a2 = fun();printf("tag2\n");a2.i = 1;const A& a3 = fun();printf("tag3\n");return 0;
}

在这里插入图片描述
上述实例反映了两点:
(1). 通过采用右值引用或常量左值引用接受函数fun的非引用类型返回值。使得返回值对应的匿名临时右值对象的生命期延长了。
(2). 通过右值引用可以修改所引用变量,通过常量左值引用则不可。

以下表格整理了引用类型及每种类型可以接受的初始化实参类型。

引用类型可以引用的值类型注记
非常量左值引用非常量左值
常量左值引用非常量左值,常量左值,非常量右值,常量右值全能类型,用于深拷贝
非常量右值引用非常量右值移动语义,完美转发
常量右值引用非常量右值,常量右值

1.4.std::move

#include <iostream>class A 
{
public:A():m_c('a'){printf("A()_%x\n", this);}A(A& a):m_c(a.m_c){printf("A(&)_%x\n", this);fun(a);}A(const A& a):m_c(a.m_c){printf("A(const&)_%x\n", this);fun(a);}A(A&& a):m_c(a.m_c){printf("A(&&)_%x\n", this);fun(a);}A(const A&& a):m_c(a.m_c){printf("const A(&&)_%x\n", this);fun(a);}A& operator=(A& a){printf("=(A&)_%x\n", this);m_c = a.m_c;fun(a);return *this;}A& operator=(const A& a){printf("=(const A&)_%x\n", this);m_c = a.m_c;fun(a);return *this;}A& operator=(A&& a){printf("=(A&&)_%x\n", this);m_c = a.m_c;fun(a);return *this;}A& operator=(const A&& a){printf("=(const A&&)_%x\n", this);m_c = a.m_c;fun(a);return *this;}void fun(A&& a){printf("fun(A&&)_%x\n", this);}void fun(const A&& a){printf("fun(const A&&)_%x\n", this);}void fun(A& a){printf("fun(A&)_%x\n", this);}void fun(const A& a){printf("fun(constA&)_%x\n", this);}~A(){printf("~A()_%x\n", this);}private:char m_c;
};void funn(A a)
{}int main()
{A a;const A a2;printf("tag\n");// std::move针对非常量左值,返回非常量右值引用。// 非常量右值引用虽然是一个非常量右值的别名,但由于有了名字。所以,右值引用被当成一个左值。// 故,通过非常量右值引用调用fun时,匹配到非常量左值引用版本。A a3 = std::move(a);printf("tag1\n");// std::move针对常量左值,返回常量右值引用。// 常量右值引用虽然是一个常量右值的别名,但由于有了名字。所以,右值引用被当成一个左值。// 故,通过常量右值引用调用fun时,匹配到常量左值引用版本。A a4 = std::move(a2);printf("tag2\n");// std::move针对非常量右值,返回非常量右值引用。// 非常量右值引用虽然是一个非常量右值的别名,但由于有了名字。所以,右值引用被当成一个左值。// 故,通过非常量右值引用调用fun时,匹配到非常量左值引用版本。A a5 = std::move(A());printf("tag3\n");A& aa1 = a;// std::move针对非常量左值引用,返回非常量右值引用。A aaa1 = std::move(aa1);printf("tag4\n");const A& aa2 = a2;// std::move针对常量左值引用,返回常量右值引用。A aaa2 = std::move(aa2);printf("tag5\n");A&& aa3 = A();// std::move针对非常量右值引用,返回非常量右值引用。A aaa3 = std::move(aa3);printf("tag6\n");const A&& aa4 = A();// std::move针对常量右值引用,返回常量右值引用。A aaa4 = std::move(aa4);printf("tag7\n");A b;funn(b);printf("tag5\n");return 0;
}

在这里插入图片描述
在这里插入图片描述
针对std::move简单的总结是:
(1). 针对左值实参,左值引用实参会返回此实参的右值引用类型。
(2). 针对右值实参,右值引用实参会返回此实参的右值引用类型。
(3). 保持实参的常量属性不变。

注意点:
(1). 右值引用虽然是一个右值的引用,但由于引用本身是有名字的,所以,右值引用是一个左值类型。
(2). std::move返回实参的右值引用,但这里相当于一个得到了匿名右值引用。所以将此返回值传参调用函数时,右值引用会被当成右值类型。而非(1)中的左值类型。

关于std::move保持实参的常量属性不变的辅助实例

#include <iostream>void fun(int* && a)
{printf("int* &&\n");
}void fun(const int* && a)
{printf("const int* &&\n");
}void fun(int* const && a)
{printf("int* const &&\n");
}void fun(const int* const && a)
{printf("const int* const &&\n");
}int main()
{int a1 = 10;const int a11 = 10;int *a = &a1;const int* a2 = &a11;int * const a3 = &a1;const int* const a4 = &a11;fun(std::move(a));fun(std::move(a2));fun(std::move(a3));fun(std::move(a4));return 0;
}

在这里插入图片描述

1.5.编译器的返回值优化
上述各个观察函数返回值的实例我们编译时,加了-fno-elide-constructors选项来关闭编译器针对返回值的优化。当开启此优化时:

#include <iostream>class A
{
public:A(){printf("A()_%x\n", this);}A(A&a){printf("A(&)_%x\n", this);}A(const A&){printf("A(const A&)_%x\n", this);}A(A&&){printf("A(&&)_%x\n", this);}A(const A&&){printf("A(const&&)_%x\n", this);}~A(){printf("~A()_%x\n", this);}
};A fun()
{A a;return a;
}int main()
{A b= fun();return 0;
}

在这里插入图片描述
b直接霸占了fun内部的变量a
此优化并不是对任何情况均有效。还有一些情形即使存在此优化,也不能达到最好效果。
而移动语义总是可以在显式控制下采用最有效的方法实现目标。

2.完美转发
完美转发指的是在函数模板中,完全依照模板参数的类型,将参数传递给函数模板中调用的另外一个函数。
2.1.引用折叠
为了支持完美转发,c++既需要引入右值,右值引用。也需要引入引用折叠。
引用折叠进一步分为左值引用折叠,右值引用折叠。

// TR的类型定义为:T&,声明v的类型为TR& -> v的实际类型为:T&,又因为T为const int所以,v的实际类型为const int&,所以TR& v = 1;对应const int& v = 1;
#include <iostream>
typedef const int T;
typedef T& TR;
int main()
{TR& v = 1;
}
TR的类型定义声明v的类型v的实际类型
T&TRT&
T&TR&T&
T&TR&&T&
T&&TRT&&
T&&TR&T&
T&&TR&&T&&

将上述引用折叠结合模板类型推断可以实现完美转发。

2.2.模板类型推断
当模板的形参中对模板类型采用T&,T&&的形式时,推断T的类型时:
当转发函数的实参是类型X的一个左值或左值引用,则模板参数T被推导为X&类型。
当转发函数的实参是类型X的一个右值或右值引用,则模板参数T被推导为X&&类型。

此时需结合引用折叠规则来最终确定参数的实际类型。
一个实例

template<typename T>
void IamForwording(T&& t){IrunCodeActually(static_cast<T&&>(t));
}

对于上述形式的转发函数,如果我们调用转发函数时传入了一个X类型的左值或左值引用。则:
(1). 模板参数T被推断为X&类型。
(2). 引用折叠完成后的实际的转发函数是

void IamForwording(X& t){IrunCodeActually(<X&>(t));
}

这样通过左值或左值引用调用转发函数,最终调用实际函数的左值引用版本。

如果我们调用转发函数时传入了一个X类型的右值或右值引用。则:
(1).模板参数T被推断为X&&类型。
(2).引用折叠完成后的实际的转发函数是

void IamForwording(X&& t){IrunCodeActually(<X&&>t);
}

这样通过右值或右值引用调用转发函数,最终调用实际函数的右值引用版本。
上述<X&&>t并不多余,因为若直接才有t的形式调用实际函数,由于右值引用类型自身是左值所以将匹配到左值引用版本。

2.3.完美转发实例

#include <iostream>
using namespace std;void RunCode(int&& m){ cout << "int&&" << endl; }
void RunCode(int& m){ cout << "int&" << endl; }
void RunCode(const int&& m) { cout << "const int&&" << endl; }
void RunCode(const int& m) { cout << "const int&" << endl; }template<typename T>
void PerfectForward(T&& t){RunCode(forward<T>(t));
}int main(){int a;int b;const int c = 1;const int d = 0;PerfectForward(a);PerfectForward(move(b));PerfectForward(c);PerfectForward(move(d));
}

在这里插入图片描述

将上述转发函数改为如下形式,一样可以达到效果:

template<typename T>
void PerfectForward(T&& t){RunCode(static_cast<T&&>(t));
}

若上述模板形参变成如下:

template<typename T>
void PerfectForward(T& t){RunCode(static_cast<T&&>(t));
}int main(){int a;int b;const int c = 1;const int d = 0;PerfectForward(a);PerfectForward(move(b));PerfectForward(c);PerfectForward(move(d));
}

则执行PerfectForward(move(b));会报错,因为此时无法完成将一个右值引用匹配到【T推断为int&&,T&t的实际类型为int&int&的转换。
PerfectForward(move(d));可以执行,此时是一个常量右值引用匹配到【T推断为const int&&,T&t的实际类型为const int&const int&的转换,然后内部执行RunCode会转变为一个const int&&类型参与RunCode的匹配过程。

这里static_cast<T&&>折叠后变为static_cast<const int&&>,这里的右值引用之所以不会当成作值类似std::move返回的右值引用,是因为这里得到的右值引用是一个匿名的右值引用。所以,参与函数传参时,不会被当成左值。

为了完整,我们在main中再补充一组测试

printf("tag\n");
int& aa = a;
const int& caa = c;
int&& aaa = move(a);
const int&& caaa = move(d);
PerfectForward(aa);
PerfectForward(caa);
PerfectForward(aaa);
PerfectForward(caaa);

上述补充内容对应的输出为:
在这里插入图片描述
因为这里的aaa,caaa是有名字的右值引用,所以参与函数传参时,被当作左值对待了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/283411.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis Set类型

集合类型也是保存多个字符串类型的元素的&#xff0c;但和列表类型不同的是&#xff0c;集合中 1&#xff09;元素之间是无序的 2&#xff09;元素不允许重复 一个集合中最多可以存储2的32次方个元素。Redis 除了支持集合内的增删查改操作&#xff0c;同时还支持多个集合取交…

链表之带头双向循环链表(C语言版)

我们之前已经介绍过链表的知识了&#xff0c;这里我们直接开始实现带头双向循环链表 数据结构之单链表&#xff08;不带头单向非循环链表&#xff09;-CSDN博客 第一步&#xff1a;定义结构体 //定义结构体 typedef int SLTDateType; typedef struct Listnode {SLTDateType d…

【消息中间件】Rabbitmq的基本要素、生产和消费、发布和订阅

原文作者&#xff1a;我辈李想 版权声明&#xff1a;文章原创&#xff0c;转载时请务必加上原文超链接、作者信息和本声明。 文章目录 前言一、消息队列的基本要素1.队列:queue2.交换机:exchange3.事件:routing_key4.任务:task 二、生产消费模式1.安装pika2.模拟生产者进程3.模…

Web前端-HTML(常用标签)

文章目录 1. HTML常用标签1.1 排版标签1&#xff09;标题标签h (熟记)2&#xff09;段落标签p ( 熟记)3&#xff09;水平线标签hr(认识)4&#xff09;换行标签br (熟记)5&#xff09;div 和 span标签(重点)6&#xff09;排版标签总结 1.2 标签属性1.3 图像标签img (重点)1.4 链…

shell子进程管理

简介 在我们平时写代码过程中&#xff0c;可能经常会遇到串行执行速度慢 &#xff0c;串行无法执行多个任务&#xff0c;这时便需要使用子进程同时执行。使用父进程创建子进程时&#xff0c;子进程会复制父进程的内存、文件描述符和其他相关信息。当然&#xff0c;子进程可以独…

Web前端-JavaScript(js表达式)

文章目录 JavaScript基础第01天1.编程语言概述1.1 编程1.2 计算机语言1.2.1 机器语言1.2.2 汇编语言1.2.3 高级语言 1.4 翻译器 2.计算机基础2.1 计算机组成2.2 数据存储2.3 数据存储单位2.4 程序运行 3.初始JavaScript3.1 JavaScript 是什么3.2 JavaScript的作用3.3 HTML/CSS/…

医疗智能化革命:AI技术引领医疗领域的创新进程

一、“AI”医疗的崛起 随着人工智能&#xff08;AI&#xff09;技术的崛起&#xff0c;"AI"医疗正在以惊人的速度改变着医疗行业的面貌。AI作为一种强大的工具&#xff0c;正在为医疗领域带来前所未有的创新和突破。它不仅在医学影像诊断、病理学分析和基因组学研究等…

tomcat错误

Error running Tomcat8: Address localhost:1099 is already in use window环境&#xff0c;打开cmd netstat -ano | findstr :1099发现对应PID为24732 结束PID taskkill /PID 24732 /F

R语言【rgbif】——occ_search对待字符长度大于1500的WKT的特殊处理真的有必要吗?

一句话结论&#xff1a;只要有网有流量&#xff0c;直接用长WKT传递给参数【geometry】、参数【limit】配合参数【start】获取所有记录。 当我在阅读 【rgbif】 给出的用户手册时&#xff0c;注意到 【occ_search】 强调了 参数 【geometry】使用的wkt格式字符串长度。 文中如…

配置Nginx解决跨域问题

Nginx 中将前端请求中的所有以 “/apiUrl” 开头的路径代理到 http://192.12.200.101:9813 例如&#xff1a; /apiUrl/login > http://192.12.200.101:9813/login 配置nginx环境 进入Nginx 的配置文件编辑界面: sudo nano /etc/nginx/conf.d/default.conf开始编辑 defaul…

计算机网络(1):开始

计算机网络&#xff08;1&#xff09;&#xff1a;开始 计算机网络在信息时代中的作用 21世纪的一些重要特征就是数字化、网络化和信息化&#xff0c;是一个以网络为核心的信息时代。要实现信息化就必须依靠完善的网络&#xff0c;因为网络可以非常迅速地传递信息。因此网络现…

STM32/STM8资源节约主义设计方式

STM32/STM8资源节约主义设计方式 在小资源芯片进行代码设计时&#xff0c;如STM32C0系列&#xff0c;STM8系列&#xff0c;因为官方库本身要包含各种场景应用特征的支持&#xff0c;所以会有一些冗余的代码占用更多FLASH空间。当需要实现资源占用最简化设计方式时&#xff0c;…