深圳网站建设的价格,wordpress高级插件,广西建设工程管理网站,上海闵行最新封闭通知目录
1. 基本概念
1.1 左值与左值引用
1.2 右值和右值引用
1.3 左值引用与右值引用
2. 右值引用实用场景和意义
2.1 左值引用的使用场景
2.2 左值引用的短板
2.3 右值引用和移动语义
2.3.1 移动构造
2.3.2 移动赋值
2.3.3 编译器做的优化
2.3.4 总结
2.4 右值引用…目录
1. 基本概念
1.1 左值与左值引用
1.2 右值和右值引用
1.3 左值引用与右值引用
2. 右值引用实用场景和意义
2.1 左值引用的使用场景
2.2 左值引用的短板
2.3 右值引用和移动语义
2.3.1 移动构造
2.3.2 移动赋值
2.3.3 编译器做的优化
2.3.4 总结
2.4 右值引用引用左值
2.5 右值引用的其他场景插入接口
3. 完美转发
3.1 万能引用
3.2 forward完美转发在传参的过程中保留对象原生类型属性
3.3 完美转发的使用场景
1. 基本概念
传统的C语法中就有引用的语法而C11中新增了的右值引用语法特性所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用都是给对象取别名。
1.1 左值与左值引用 左值 左值是一个表示数据的表达式(如变量名或解引用的指针)有如下特性 我们可以获取它的地址可以对它赋值不一定能赋值但一定能取地址左值可以出现赋值符号的左边右值不能出现在赋值符号左边定义时const修饰符后的左值不能给他赋值但是可以取它的地址。 左值引用 左值引用就是给左值的引用给左值取别名。 int main()
{// 以下的p、b、c、*p都是左值int* p new int(0);int b 1;const int c 2;// 以下几个是对上面左值的左值引用int* rp p;int rb b;const int rc c;int pvalue *p;return 0;
} 1.2 右值和右值引用 右值 右值也是一个表示数据的表达式如临时变量字面常量、表达式返回值函数返回值(这个不能是左值引用返回要是传值返回)等等有如下特性 右值可以出现在赋值符号的右边但是不能出现出现在赋值符号的左边右值不能取地址。综上左值和右值最大区别在于左值可以取地址右值不可以取地址因为右值是临时变量没有实际被存储起来。 补充 C里又把右值分为两类纯右值和将亡值 纯右值内置类型的对象10、a b……将亡值自定义类型的对象 传值返回生成的拷贝to_string(1234)、匿名对象string(11111)、s1 hello 右值引用 右值引用就是对右值的引用给右值取别名。 int main()
{double x 1.1, y 2.2;// 以下几个都是常见的右值10;//字面常量x y;//表达式返回值fmin(x, y);//函数返回值传值返回// 以下几个都是对右值的右值引用int rr1 10;double rr2 x y;double rr3 fmin(x, y);/*这里编译会报错error C2106: “”: 左操作数必须为左值10 1; x y 1; fmin(x, y) 1;*//*这里编译会报错右值不能取地址cout 10 endl;cout (x y) endl;cout fmin(x, y) endl;*/return 0;
} 右值是不能取地址的但是给右值取别名后会导致右值被存储到特定位置且可以取到该位置的地址也就是说例如不能取字面量10的地址但是rr1引用后可以对rr1取地址也可以修改rr1。如果不想rr1被修改可以用const int rr1 去引用这个了解一下实际中右值引用的使用场景并不在于此这个特性也不重要。 int main()
{double x 1.1, y 2.2;int rr1 10;const double rr2 x y;rr1 20;rr2 5.5; // 报错return 0;
} 1.3 左值引用与右值引用 左值引用总结 左值引用只能引用左值不能引用右值但是const左值引用既可以应用左值也可以引用右值 int main()
{// 左值引用只能引用左值不能引用右值。int a 10;int ra1 a; // ra为a的别名//int ra2 10; // 编译失败因为10是右值// const左值引用既可引用左值也可引用右值。const int ra3 10;const int ra4 a;return 0;
} 右值引用总结 右值引用只能引用右值不能引用左值但是右值引用可以引用move以后的左值 int main()
{// 右值引用只能引用右值不能引用左值。int r1 10;int a 10;/*error C2440: “初始化”: 无法从“int”转换为“int ”message : 无法将左值绑定到右值引用int r2 a;*/// 右值引用可以引用move以后的左值int r3 move(a);return 0;
} 总结 左值引用只能引用左值不能引用右值但是const左值引用既可以引用左值也可以引用右值右值引用只能引用右值不能引用左值但是右值引用可以引用move以后的左值。 右值引用是通过移动构造和移动赋值来极大提高深拷贝的效率详情见下文 2. 右值引用实用场景和意义
2.1 左值引用的使用场景 左值引用解决的是拷贝构造引发的深拷贝而带来的开销过大、效率低的问题 左值引用做参数防止传值传参引发的拷贝构造问题导致效率低左值引用做返回值防止返回对象发生拷贝构造的操作导致效率低 void func1(cpp::string s)
{}
void func2(const cpp::string s)
{}
int main()
{cpp::string s1(hello);func1(s1);//值传参func2(s1);//传引用传参// string operator(char ch) 传值返回存在深拷贝// string operator(char ch) 传左值引用没有拷贝提高了效率s1 a;//左值引用作为返回值return 0;
} 总结 我们都清楚string类的运算符是左值引用作为返回值这样做避免了传值返回引发的拷贝构造而这样做的原因在于string类的拷贝构造为深拷贝要经历开空间等操作开销太大了导致效率低传值传参同样也是会发生拷贝构造深拷贝这个问题为了避免如此之大的开销使用左值引用可以很好的解决此问题因为左值引用就是取别名无开销提高了效率。 2.2 左值引用的短板 左值引用可以避免一些不必要的拷贝构造操作但是并不是所有情况都是可以避免的 左值引用做参数能够完全避免传参时不必要的拷贝操作左值引用做返回值并不能完全避免函数返回对象时不必要的拷贝操作。 当函数返回的是一个临时对象时不能使用引用返回因为临时对象出了函数作用域就销毁了只能使用传值返回而传值返回难免会引发拷贝构造带来的深拷贝问题但是无法避免这就是左值引用的短板示例 namesapce cpp
{cpp::string to_string(int value){bool flag true;if (value 0){flag false;value 0 - value;}cpp::string str;while (value 0){int x value % 10;value / 10;str (0 x);}if (flag false){str -;}std::reverse(str.begin(), str.end());return str;}
} 因为这里的to_string是传值返回所以在调用to_string的时候一定会调用拷贝构造而拷贝构造实现的又是一个深拷贝效率低 int main()
{cpp::string ret cpp::to_string(1234);//string(const string s) -- 深拷贝return 0;
} 如果强硬的把上面的to_string实现成左值引用返回那么又会出现一个问题我str是临时对象因为是左值引用返回所以返回的是str的别名把别名作为返回值再区拷贝构造ret对象但是临时对象str出了作用域就调用析构函数销毁了即使能够访问对象的值但是空间已经不存在了此时就发生了内存错误。不能返回局部变量的引用 综上所述为了解决左值引用的短板C11引出了右值引用但并不是简单的把右值引用作为返回值要对string进行改造详情见下文 2.3 右值引用和移动语义 移动构造 string拷贝构造的const左值引用会接收左值和右值但是编译器遵循最匹配原则如果我们单独增加一个右值引用版本的拷贝构造函数使其只能接收右值根据最匹配原则遇到右值传入右值引用版本的拷贝构造函数遇到左值传入左值引用版本的拷贝构造函数这样就能解决了左值引用带来的弊端而上述单独增加的函数就是我们的移动构造 移动赋值 operator函数采用的是const左值引用接收参数因此无论赋值时传入的是左值还是右值都会调用原有的operator函数。增加移动赋值之后由于移动赋值采用的是右值引用接收参数因此如果赋值时传入的是右值那么就会调用移动赋值函数最匹配原则。string原有的operator函数做的是深拷贝而移动赋值函数中只需要调用swap函数进行资源的转移因此调用移动赋值的代价比调用原有operator的代价小。 2.3.1 移动构造 为了解决左值引用的短板我们需要在cpp::string中增加移动构造移动构造的本质是将参数右值将亡值的资源窃取过来占位已有那么就不用做深拷贝了所以它叫做移动构造就是窃取别人的资源来构造自己。因为将亡值的特点就是很快就要被销毁了在你销毁之前还不如把你的资源通过移动构造传给别人。 该移动构造函数要做的就是调用swap函数将传入右值的资源窃取过来为了能够更好的得知移动构造函数是否被调用可以在该函数当中打印一条提示语句。 namespace cpp
{class string{public://移动构造string(string s):_str(nullptr), _size(0), _capacity(0){cout string(string s) -- 移动构造资源转移 endl;swap(s);}private:char* _str;size_t _size;size_t _capacity;};
} int main()
{cpp::string ret cpp::to_string(1234);//转移将亡值的资源cpp::string s1(hello);cpp::string s2(s1);//深拷贝左值拷贝时不会被资源转移cpp::string s3(move(s1));//转移将亡值的资源return 0;
} 移动构造与拷贝构造的区别 在没有添加移动构造之前拷贝构造采用的是const左值引用接收参数所以无论左值还是右值都会被传进去势必会引发一系列左值引用的短板添加移动构造后由于移动构造采用右值引用接收参数只能接收右值根据编译器的最匹配原则左值传入左值引用的拷贝构造右值传入右值引用的移动构造 2.3.2 移动赋值 移动赋值是一个赋值运算符重载函数该函数的参数是右值引用类型的移动赋值也是将传入右值的资源窃取过来占为己有这样就避免了深拷贝所以它叫移动赋值就是窃取别人的资源来赋值给自己的意思。 在当前的string类中增加一个移动赋值函数该函数要做的就是调用swap函数将传入右值的资源窃取过来为了能够更好的得知移动赋值函数是否被调用可以在该函数中打印一条提示语句。 namespace cpp
{class string{public:// 移动赋值string operator(string s){cout string operator(string s) -- 移动赋值 endl;swap(s);return *this;}private:char* _str;size_t _size;size_t _capacity;};
} int main()
{cpp::string ret;//string(string s) -- 移动构造资源转移ret cpp::to_string(1234);//string operator(string s) -- 移动赋值资源转换return 0;
} 来区分下移动赋值和operator 在没有增加移动赋值之前由于原有operator函数采用的是const左值引用接收参数因此无论赋值时传入的是左值还是右值都会调用原有的operator函数。增加移动赋值之后由于移动赋值采用的是右值引用接收参数因此如果赋值时传入的是右值那么就会调用移动赋值函数最匹配原则。string原有的operator函数做的是深拷贝而移动赋值函数中只需要调用swap函数进行资源的转移因此调用移动赋值的代价比调用原有operator的代价小。 总结 这里运行后我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收编译器就没办法优化了。cpp::to_string函数中会先用str生成构造生成一个临时对象但是我们可以看到编译器很聪明的在这里把str识别成了右值调用了移动构造。然后在把这个临时对象做为cpp::to_string函数调用的返回值赋值给ret1这里调用的移动赋值。这里虽然调用两次函数但都只是资源的移动不需要进行深拷贝大大提高了效率 2.3.3 编译器做的优化 int main()
{cpp::string s cpp::to_string(1234);return 0;
} 1、先来看下没有移动构造编译器做的优化 不优化 如果没有移动构造那我们先前实现的to_string只能够传值返回传值返回会先拷贝构造出一个临时对象再用这个临时对象再拷贝构造我们接收返回值的对象。如图所示 优化 C11标准出来之前也就是C98的情况本来应该是两次拷贝构造但是编译器对其进行了优化连续两次的拷贝构造函数最终被优化成一次直接拿str拷贝构造s。 2、再来看看有移动构造编译器做的优化 不优化 C11出来后我们假设它不优化根据先前的了解不优化的话左值str会拷贝构造给一个临时对象这个临时对象就是一个右值将亡值随后进行移动构造也就是先拷贝构造再移动构造 优化 C11这里编译器进行优化后左值str会被优化成右值通过move把左值变为右值再移动构造给一个临时对象此临时对象再移动构造给s但是编译器还会再进行一次优化把左值str识别出右值后直接移动构造给s。也就是只进行一次移动构造 3、来看看编译器对移动赋值的处理 当我们不是用函数的返回值来构造一个对象而是用一个之前已经定义出来的对象来接收函数的返回值测试代码如下 int main()
{cpp::string ret;ret cpp::to_string(1234);return 0;
} 此时编译器会把左值str会被优化成右值通过move把左值变为右值再移动构造给一个临时对象此临时对象再通过移动赋值传给之前已经定义出来的对象。 这里编译器并没有对这种情况进行优化因为如果是用一个已经存在的对象接收编译器就没办法优化了。cpp::to_string函数中会先用str生成构造生成一个临时对象但是我们可以看到编译器很聪明的在这里把str识别成了右值调用了移动构造。然后在把这个临时对象做为cpp::to_string函数调用的返回值赋值给ret1这里调用的移动赋值。 2.3.4 总结 左值引用的深拷贝 -- 拷贝构造 / 拷贝赋值右值引用的深拷贝 -- 移动构造 / 移动赋值 C11后STL中的容器都是增加了移动构造和移动赋值。 2.4 右值引用引用左值 move函数 按照语法右值引用只能引用右值但右值引用一定不能引用左值吗因为有些场景下可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时可以通过move函数将左值转化为右值。C11中std::move()函数位于utility头文件中该函数名字具有迷惑性它并不搬移任何东西唯一的功能就是将一个左值强制转化为右值引用然后实现移动语义。move函数的定义 templateclass _Ty
inline typename remove_reference_Ty::type move(_Ty _Arg) _NOEXCEPT
{// forward _Arg as movablereturn ((typename remove_reference_Ty::type)_Arg);
} 注意 move函数中_Arg参数的类型不是右值引用而是万能引用。万能引用跟右值引用的形式一样但是右值引用需要是确定的类型。一个左值被move以后它的资源可能就被转移给别人了因此要慎用一个被move后的左值。 测试如下 int main()
{cpp::string s1(hello world);// 这里s1是左值调用的是拷贝构造cpp::string s2(s1);//string(const string s) -- 深拷贝// 这里我们把s1 move处理以后, 会被当成右值调用移动构造// 但是这里要注意一般是不要这样用的因为我们会发现s1的// 资源被转移给了s3s1被置空了。cpp::string s3(std::move(s1));//string(string s) -- 移动构造return 0;
} 2.5 右值引用的其他场景插入接口 C11后STL容器中的插入接口函数也增加了右值引用的版本 注意 C98的时候push_back函数只有const左值引用版本所以这就会导致无论是左值还是右值都会被传入这个左值引用版本的push_back势必会引发后续的深拷贝而带来的开销过大等问题。C11出来后push_back函数增加了右值引用版本如果传入push_back函数的是一个右值那么在push_back函数构造节点时这个右值就可以匹配到容器的移动构造函数进行资源的转移这样就避免了深拷贝提高了效率。 int main()
{listcpp::string lt;cpp::string s1(1111);// 这里调用的是拷贝构造lt.push_back(s1);//string(const string s) -- 深拷贝// 下面调用都是移动构造5lt.push_back(2222);//string(string s) -- 移动构造lt.push_back(std::move(s1));//string(string s) -- 移动构造return 0;
} 上述代码中的插入第一个元素s1就会匹配到push_back的左值引用版本在push_back函数内部就会调用string的拷贝构造函数进行深拷贝而后面插入的两个元素时由于传入的是右值因此会匹配到push_back的右值引用版本此时在push_back函数内部就会调用string的移动构造函数进行资源的转移。 3. 完美转发
3.1 万能引用 应用在模板中时不代表右值引用而是万能引用万能引用既能接收左值也能接收右值。 templatetypename T
void PerfectForward(T t)//万能引用
{//……
} 万能引用的作用 模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值。 示例 void Fun(int x) { cout 左值引用 endl; }
void Fun(const int x) { cout const 左值引用 endl; }
void Fun(int x) { cout 右值引用 endl; }
void Fun(const int x) { cout const 右值引用 endl; }
templatetypename T
void PerfectForward(T t)
{Fun(t);
}
int main()
{PerfectForward(10);//右值int a;PerfectForward(a);//左值PerfectForward(std::move(a));//右值const int b 8;PerfectForward(b);//const左值PerfectForward(std::move(b));//const右值return 0;
} 注意看上面的Fun函数我写了四个分别是左值引用、const左值引用、右值引用、const右值引用。main函数中我把左值、右值、const左值、const右值均作为参数传入了函数模板PerfectForward里头因为其参数类型是万能引用所以既可以接收左值也可以接收右值可是最终的测试结果却全为左值引用了 实际传入PerfectForward函数模板的左值和右值均匹配到了左值引用版本的Fun函数而传入PerfectForward函数模板的const左值和const右值均匹配到了const左值引用版本的Fun函数。造成此现象的根本原因在于右值被引用后会导致右值被存储到特定位置这时这个右值可以被取到地址并且可以被修改所以在PerfectForward函数中调用Func函数时会将t识别成左值。 这也就是万能引用限制了接收的类型在后续使用中均退化成了左值但是我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。 3.2 forward完美转发在传参的过程中保留对象原生类型属性 我们想要在传参的过程中保留对象的原生类型属性就需要用到forward函数 templatetypename T
void PerfectForward(T t)
{//完美转发Fun(std::forwardT(t));//std::forwardT(t)在传参的过程中保持了t的原生类型属性。
} 完美转发后左值、右值、左值引用、右值引用就可以被传入到理想状态下的函数接口了。 3.3 完美转发的使用场景 这里把先前模拟实现的list拖过来做测试案例先前实现的list是没有对push_back函数和insert函数写一个右值引用版本的所以这就会导致无论数据是左值还是右值都会传入左值引用的版本势必在构建节点的时候引发深拷贝测试代码如下 int main()
{cpp::listcpp::string lt;cpp::string s1(1111);//右值lt.push_back(s1);//左值lt.push_back(2222);//右值lt.push_back(std::move(s1));//右值
} 为了避免深拷贝带来的开销过大我们对push_back和insert函数单独写一个右值引用的版本同样也要对构造函数写一个右值引用的版本因为创建节点需要用到节点类的构造函数 //节点类
templateclass T
struct list_node
{//……//右值引用节点类构造函数list_node(T val):_next(nullptr), _prev(nullptr), _data(val){}
};
templateclass T
class list
{
public://……//右值引用版本的push_backvoid push_back(T xx){insert(end(), xx);}//右值引用版本的insertiterator insert(iterator pos, T xx){Node* newnode new Node(xx);//创建新的结点Node* cur pos._node; //迭代器pos处的结点指针Node* prev cur-_prev;//prev newnode cur//链接prev和newnodeprev-_next newnode;newnode-_prev prev;//链接newnode和curnewnode-_next cur;cur-_prev newnode;//返回新插入元素的迭代器位置return iterator(newnode);}
private:Node* _head;
} 虽然这里实现了右值引用版本但是实际的运行结果依然是深拷贝的和没写之前的运行结果一模一样原因如下 根据先前的了解我们得知应用在模板中时不代表右值引用而是万能引用万能引用既能接收左值也能接收右值。但是在后续的使用中会把接收的类型全部退化成左值既然退化成左值那么自然会进入后续的深拷贝 此情况就是典型的完美转发的使用场景解决办法如下 我们需要在传参的过程中保留对象的原生类型属性就需要用到forward函数 //右值引用节点类的构造函数
list_node(T val):_next(nullptr), _prev(nullptr), _data(std::forwardT(val))//完美转发
{}
//右值引用版本的push_back
void push_back(T xx)
{//完美转发insert(end(), std::forwardT(xx));
}
//右值引用版本的insert
iterator insert(iterator pos, T xx)
{//完美转发Node* newnode new Node(std::forwardT(xx));//……return iterator(newnode);
}