在讲这个之前, 必须得讲一下左值和右值, 这个知识真的是很冷门又冷门, 如果不是C++11的std::move, 我想我八辈子都不会知道这是什么东西, 晦涩难懂
左值
简单的来说就是赋值符号左边的值, 准确的来说, 左值就是表达式执行后依然存在的持久的对象
右值
右边的值,表达式执行过后就不再存在的临时对象, 但是C++肯定是要把编程搞得更复杂的, 所以右值又有纯右值, 和将亡值
纯右值: 纯粹的右值要么是纯粹的的字面量, 比如2233
, true
;要么是求值结果相当于字面量或者是匿名临时对象, 比如一个加法2*3
,非引用返回的临时变量, 运算表达式的临时变量, 原始字面量,lambda表达式都是纯右值
将亡值: 就是可以被移动并且将要被销毁的值. 这个说得有点抽象, 看个代码
std::vector<int> foo(){
std::vector<int> temp={1,2,3,4};
return temp;
}
int main(int argc,char *argv[]){
auto res=foo();
return 0;
}
现在这个写法相信大家肯定都看过了, 但是这个存在一个问题, foo的返回值在这个里面作为一个临时的对象被返回, 这个赋值语句做了什么呢, 他把这个临时对象拷贝一遍, 这个返回的对象或者值事实上就是一个右值而且是个纯右值, 因为他是表达式求值后得到的值, 如果这个foo()
特别的大, 系统产生的开销是很庞大的,还有很多这样的情况,比如vector的增长, 如果vector的capacity不够用的时候, 它就会产生拷贝的行为, 看一段官方英文的文档
Rvalue references are a new reference type introduced in C++0x that help solve the problem of unncessatry copying and enable perfect forwarding. When the right-hand saide of an assignment is an rvalue, then the left-side object can steal resources from the right-hand side object rather than proforming a separate allocation, thus enabling move semantics.
我的翻译是这样的:
右值引用是一种新型的引用, 在C++0x十分被推荐使用, 它用来解决一个问题, 这个问题是关于如何避免不必要的拷贝和更好的进行forward, 当一个右值需要赋值到左值的时候, 左值可以去“窃取”右值的资源, 而且还并不需要开辟一个新的空间,
我们大概理解了这个东西, 大概就是为了防止比如vector insert的时候去拷贝临时对象, 而是去指向他的引用, 这个是个好东西, 可以防止吃内存, 我们也可以大胆猜测, 事实上右值引用就是指针的浅拷贝.
但是关于我找到了这样的两个代码, 来看这个代码:
int main() {
std::string s1("Hello");
std::string s2("World");
s1+s2=s2;
std::cout<<"s1:"<<s1<<std::endl;
std::cout<<"s2:"<<s2<<std::endl;
std::string()="World";
}
超屌的, 这段代码居然是可以编译的, 而且还可以执行, s1+s2
很明显他是一个右值, 但是却被放在了左边, std::string()
是个临时对象竟然可以赋值, 类似的代码还有
void complex_test(){
std::complex<int> c1(3,8),c2(1,0);
c1+c2=std::complex<int>(6,9);
std::cout<<c1<<" "<<c2<<std::endl;
std::complex<int>() =std::complex<int> (59,1);
}
这似乎是标准库的bug, 但是这样做确实是不被允许的
现在有了右值我们就有了另一种代码, 我们在向某个函数传入临时对象的时候, 如果这个对象后面不会再使用我们就可以传入他的右值引用, 这个过程不回去调用它的拷贝构造函数,而是调用他的引用构造函数,
//现在我们有两套拷贝的构造函数
//这是vector的iterator
iterator
insert(const_iterator __position,const value_type& __x);
iterator(const_iterator __position,value_type&& __x){
return emplace(__ position,std::move(__x));
}
那么, 我有什么办法来进行使用呢?
std::move
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.insert(v.end(),str);
std::cout << "After copy, str is \"" << str << "\"\n";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
v.insert(v.end(),std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
这里用到了std::move, 在C++11后就有了这个东西, 它可以把左值强制转化为右值, 你可以解释它为一个cast, 以前我们如果要在C++里面让一个指针搬家, 我们需要先把它拷贝, 再把它清空,让它指向空, 现在不用了, 现在你可以移动(窃取)他.
所以我们有了
左值引用和右值引用
直接看代码
#include <iostream>
#include <string>
void reference(std::string& str) {
std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
std::cout << "右值" << std::endl;
}
int main()
{
std::string lv1 = "string,"; // lv1 是一个左值
// std::string&& r1 = s1; // 非法, 右值引用不能引用左值
std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
std::cout << rv1 << std::endl; // string,
const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的申明周期
// lv2 += "Test"; // 非法, 引用的右值无法被修改
std::cout << lv2 << std::endl; // string,string
std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象声明周期
rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
std::cout << rv2 << std::endl; // string,string,string,
reference(rv2); // 输出左值
return 0;
}
完美转发 std::forward
如果我们声明一个右值引用, 那么这个右值引用本生其实上是一个左值对吧, 看代码
void reference(int& v) {
std::cout << "左值" << std::endl;
}
void reference(int&& v) {
std::cout << "右值" << std::endl;
}
template <typename T>
void pass(T&& v) {
std::cout << "普通传参:";
reference(v); // 始终调用 reference(int& )
}
int main() {
std::cout << "传递右值:" << std::endl;
pass(1); // 1是右值, 但输出左值
std::cout << "传递左值:" << std::endl;
int v = 1;
pass(v); // r 是左引用, 输出左值, 因为编译器帮我们优化了这一步,上面并不是一个右值的引用, 而是一个引用的引用
return 0;
}
对于 pass(1) 来说,虽然传递的是右值,但由于 v 是一个引用,所以同时也是左值。因此 reference(v) 会调用 reference(int&),输出『左值』。而对于pass(v)而言,v是一个左值,为什么会成功传递给 pass(T&&) 呢?
其实上述的代码都还有问题, 那就是事实上如果不用模版函数还会有错误
void check_lr(int &value){
std::cout<<"This is a lvalue"<<std::endl;
}
void check_lr(int &&value){
std::cout<<"This is a rvalue"<<std::endl;
}
void do_check(int&& value){
check_lr(value);
}
int main() {
int x=520;
check_lr(x); //binggo
check_lr(1); //bingo
check_lr(std::move(x));//bingo
do_check(520); //为什么到了check里面520变成了一个左值,(事实上他变成了一个named object),这个叫imperfect forward!
do_check(std::move(x));//为什么你也成了左值
//do_check(x); //编译都过不了 一个右值引用的形参怎么可以接受一个左值呢
//所以上面那个template的pass是编译器替我们做的优化, 进行了类型的推导, 尽可能的可以编译, 它强行把我们传入的v作为一个左值的引用,这个叫引用坍缩规则
return 0;
}
以下摘自 快速入门C++11,14
引用坍缩规则
这是基于引用坍缩规则的:在传统 C++ 中,我们不能够对一个引用类型继续进行引用,但 C++ 由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用,既能左引用,又能右引用。但是却遵循如下规则:
因此,模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。这才使得 v 作为左值的成功传递。
完美转发就是基于上述规律产生的。所谓完美转发,就是为了让我们在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用)。为了解决这个问题,我们应该使用 std::forward 来进行参数的转发(传递):
#include <iostream>
#include <utility>
void reference(int& v) {
std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v) {
std::cout << "普通传参:";
reference(v);
std::cout << "std::move 传参:";
reference(std::move(v));
std::cout << "std::forward 传参:";
reference(std::forward<T>(v));
}
int main() {
std::cout << "传递右值:" << std::endl;
pass(1);
std::cout << "传递左值:" << std::endl;
int v = 1;
pass(v);
return 0;
}