前言
本来是想在C++11里写这篇文章的,发现东西很多,就单独列一篇文章了,
右值这个概念是在C++11中提出来的,以前只有左值和左值引用的概念,C++11后提出了右值和右值引用,为什么要提出右值和右值引用?请看讲解
一、什么是右值?
那先想想什么是左值?----出现在等号左边的一定是左值,出现在等号右边的不一定是左值
int p = 5;
int a = p;
int *pa = &a;
int *pb = new int(1);
const int& paa = 5;
//以上的变量都是左值
delete pb;
什么是右值?第一种理解,不能被改变的值,因为他无法出现在等号左边(玩一点花活是可以的,详见最后一条,但是通常这么说);第二种理解:是一种数据的表达式,如常量,表达式的返回值等等,如
10;
double x = 5,y = 6;
x + y;
int func(){return 1;
}
这里的10,x + y,func()都是右值,
有没有左值和右值明显的区分方法?
有的兄弟有的
能取地址的一定是左值,取不了地址的一定是右值 !!!!!!!!!!!!!
注意:这里说的都是一定,不存在特殊情况
右值分为两种!!!!!!
1.纯右值,表示一个临时对象或者字面值常量
2.将亡值,表示一个即将被释放资源的对象,一般是右值引用或者move()操作,记住这个将亡值,后面会提到
C++文档
二、右值引用
什么是左值引用?这个很熟悉了,用的也很多,不过多解释。
右值引用就是对右值引用,给右值取别名
double x = 5,y = 6;
int && pa = 5;
int && pb = (x + y);
这里的pa和pb都是对右值的引用,那pa,pb是左值还是右值?
是左值(埋个坑,后面过来填)
//右值取别名后,可以修改
int main()
{double x = 1, y = 2;int&& rr1 = 10;rr1 = 20;return 0;
}
“左值持久,右值短暂”
右值引用只能绑定到临时对象,我们知道:所引用的对象将要被销毁,并且该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自由的接管所引用的对象的资源(好好看看,为了后面的移动构造埋伏笔)
三、左值引用和右值引用的联系与作用、移动构造、移动赋值
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!比较重要
左值引用可以引用右值-----加const,防止权限的扩大
右值引用也可以引用左值--------使用move(),但有可能会出问题(后面解释)
注意:虽然规则在这,但是权限问题还是别瞎玩
比如:
const int a = 5;
int &&pb = move(a);
左值引用和右值引用的作用!!!!!!!!!!!!!!
左值引用的作用?
减少拷贝,增加效率
1.作函数参数.2.作返回值
比如经常写的重载流插入和流提取
ostream& operator<<(ostream& os);
左值引用这么全面为什么要实现右值引用?那左值引用当然是有缺点的了,他没有解决返回临时变量的问题,如果函数返回值是一个临时变量,出了作用域就销毁了,那还能返回左值引用吗?当然不能,只能传值返回,传值返回就要发生拷贝,减少效率,如果是一个int类型还好,如果是vector<vector< int >> func()这种呢?
所以右值引用就是来解决这个问题的。
string func()
{string a("aaaaaaaaaaaaaaaaa")return a;
}
int main()
{string it = func();return 0;
}
这里正常来说是不是两次拷贝构造?返回一个s的临时对象—第一次,构造it—第二次,但是编译器会对其进行优化,连续的构造+拷贝构造会优化成一次,那我们怎么看呢?只能自己搓一个string了。
namespace myspace
{class string{friend std::ostream& operator<<(std::ostream& os, const myspace::string& s);public:string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity;};
}
myspace::string func()
{myspace::string a("aaaaaaaaaaaaaaaaa");return a;
}
int main()
{myspace::string it = func();return 0;
}
一输出????????????怎么啥也没输出,欸你是不是搁这瞎讲啊,别急,这里是NRVO的缘故,当时我也到处问了好久,有一个大佬找的一篇文章很好
建议看下面这篇文章:
NRVO
简单来说就是,我这本来出了func()函数a就销毁了,还得进行两次拷贝构造才到it,那我为什么不直接构造it呢?所以:编译器进行了优化,相当于直接构造it,不经过任何函数
这就是什么都不输出的原因了。
那我就想看怎么办呢?
建议进入DevC++;
点工具->编译选项->在编译时加入下面命令换成这句话-fno-elide-constructors
这样就能看到有两次深拷贝了,这里随着编译器的优化还是减少了很多拷贝的,
有点小跑题,回到右值引用的作用上来,上面讲了右值引用是为了减少拷贝,那怎么减少呢?看原来左值的拷贝构造怎么实现的,搞一个string tmp,然后swap,----深拷贝,那这里的a是不是出了作用域就被销毁了?那我直接把资源“窃取”过来不好吗,还搞什么深拷贝–这里形象的称为移动构造(与拷贝构造相对应),就是不进行深拷贝,直接把数据移动过来,提高效率
string(string&& s):_str(nullptr),_size(0),_capacity(0)
{cout << "string(string&& s) " << endl;swap(s);(这里的s是左值还是右值?好好想想后面会提到)
}
此时再运行代码(还是要关NRVO,不然还是直接构造了)
这里细心的人可能会有疑问,为什么不是拷贝构造 + 移动构造?这a不是左值吗?
a确实是左值,但是a出了func()是不被销毁了?所以编译器进行优化,与其返回a的构造,直接当作右值调用移动构造很爽啊。
既然有移动构造,就会有移动赋值
string& operator=(string&& s){cout << "string& operator=(string&& s)" << endl;swap(s);return *this;}
这样是不是很好啊
来的是个左值,就去正常深拷贝,这是避免不了的,来的是个右值,我直接移动赋值,将资源窃取过来,就不用拷贝了。
STL里的一些函数比如说push_back,insert在C++11后都提供了右值引用的接口,也是为了减少拷贝,增加效率
tips:区分移动和拷贝的重载函数通常有一个版本接受const T&,而另一个版本接受一个T&&.
四、noexcept
noexcept是异常学的,回忆一下noexcept,是我们承诺一个函数不抛出异常的方法,和这有什么关系呢?
注意:移动操作“窃取”资源,它通常不分配任何资源,所以,移动操作通常不会抛出异常
tips:不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
为什么?
举一个例子:在标准库中的vector,如果调用push_back产生异常,自身的行为没有发生改变,push_back可能会使vector重新分配空间,但是到了自己写,如果重新分配的过程中使用了移动构造,移动了一部分后抛出了一个异常,是不就出问题了?旧空间中的移动源元素已经被改变,而新空间中没构造的元素可能尚且不存在,vector本身是不就变了?这与库里的不符合,所以要标记为noexcept
五、move
上面讲了右值引用可以引用左值----加上move,被move的对象有什么变化呢?可以猜到move—移动,就是把这个对象给移动了,
注意1.不要对常量变量进行move
2.不要使用move返回局部变量
所以move干啥了呢?通过一段代码演示一下
和我们猜想的一样,把资源“移动”了,所以被move的对象就会被置空,所以move确实可以加快效率,但是要确保安全,看下面这个图片
六、完美转发
官方解释
好高级的名字
先看一段代码
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; }
template<typename 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;
}
模板中的既有左值传过来,又有右值传过来,那我怎么区分呢?所以,我们规定:
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
但是如果不使用完美转发,t的属性还是左值,会退化回去,看运行结果
密码的怎么全给我干左值上了,其实对上面细心看的人可以发现问题了,这里的t可以取地址,这t就是左值啊,那我右值不是被吞了,所以C++11搞出了完美转发来处理这种情况,使用forward关键字
通过这个就可以看出怎么写了Fun(forward<T>(t));
forward< T >(t)在传参的过程中保持了t的原生类型属性。
比如自己实现vector / list等等的时候,要想保证使用原生属性就要使用forward
七、类中默认生成的移动构造和移动赋值函数
先看结论:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。(移动赋值同理)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
class X {int i;//内置类型可以移动std::string s;//string定义了自己的移动操作
};
class hasX {X mem; //X有合成的移动操作
};
int main()
{X x,x2 = std::move(x);//使用合成的移动构造函数hasX hx, hx2 = std::move(hx);//使用合成的移动构造函数return 0;
}
上面的可以自行验证
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
default(和delete区分开)
作用:假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成
比如上面的移动构造,就可以声明为default
关于删除的一些要点:
1.与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
2.如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
3.类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
4.类似拷贝赋值运算符,如果有类成员是 const 的或是引用,则类的移动赋值运算符被定义为删除的。
!!!!!!!!!!!!!!!
这里面说了这么多法则那我怎么记呢?
看下面
八、三五法则
所有五个拷贝控制成员(拷贝构造,赋值运算符重载,析构函数,移动赋值,移动构造)应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,他就应该定义所有五个操作,某些类必须定义拷贝构造函数、拷贝赋值运算符和析构函数才能正确工作,这些类通常具有一个资源,而拷贝成员必须拷贝此资源,一般来说,拷贝一个资源会导致一些额外开销,在这种拷贝并非必要的情况下,定义了移动构造函数和移动赋值运算符的类就可以避免此问题。
简单来说:如果只是个简单的日期类,这五个成员也没必要写,编译器自己生成的就够用,如果像string这种,当然每个就得实现,讲深浅拷贝的时候就有这个要点
九、右值和左值引用成员函数
先看这段代码
int main()
{string s1 = "a value", s2 = "another";auto n = (s1 + s2).find('a');s1 + s2 = "wow";return 0;
}
这种是不很怪异的用法啊,右值是不也出现在了左边啊,为了维护向后的兼容性,新标准库类仍然允许向右值赋值,但是我们不希望在自己的类中出现这种用法,我们希望强制左侧操作对象是一个左值,所以C++里用一种方式与const类似
即:在参数列表后放置一个引用限定符
class Foo {
public:Foo& operator= (const Foo&other);Foo& retFoo();//调用是一个左值Foo retval();//调用是一个右值};
Foo& Foo::operator=(const Foo& other) {//....return *this;
}
int main()
{Foo i, j;i = j;i.retFoo() = j;i.retval() = j;return 0;
}
经过&限定的函数,我们只能将他用于左值,对于&&限定的函数,只能用于右值;
一个函数可以同时用const和引用限定,此情况下,引用限定符必须跟在const之后,另外,它就像一个成员函数一样可以根据是否有const来区分重载
比如:
class Foo {
public:Foo(vector<int>&da) {for (int i = 0; i < da.size(); ++i)data.push_back(da[i]);}Foo sorted()&&;Foo sorted()const &;Foo& operator= (const Foo&other)&;Foo& retFoo() {return *this;}//调用是一个左值Foo retval() {Foo ret(this->data);return ret;}//调用是一个右值
private:vector<int> data;
};
Foo Foo::sorted()&& {sort(data.begin(), data.end());cout << "Foo Foo::sorted()&&" << endl;for (const auto& e : data){cout << e << " ";cout << endl;}return *this;
}
Foo Foo::sorted()const & {Foo ret(*this);sort(ret.data.begin(), ret.data.end());cout << "Foo Foo::sorted()const &" << endl;for (const auto& e : ret.data){cout << e << " ";cout << endl;}return ret;
}int main()
{vector<int>v1({ 3,1,2 }), v2({ 1,2,3 });Foo i(v1), j(v2);i.retval().sorted();cout << endl;i.retFoo().sorted();return 0;
}
这里的sorted()& 和 sorted()const& 构成了重载,编译器会根据调用sorted的对象的左值或者右值属性来区分哪个使用sorted版本,看运行结果
当我们对一个右值进行sorted的时候,它可以安全的直接排序,对象是一个右值,意味着没有其他用户,因此我们可以改变对象,当对一个const右值或者一个左值进行sorted的时候,我们不能改变对象,所以需要拷贝data
注意:当我们定义const成员时,可以定义两个版本,一个有一个没有,引用限定则不一样,如果我们想重载多个函数,就必须对所有函数都加上引用限定符,或者都不加
class Foo {
public:Foo sorted()&&;Foo sorted()const;//errorusing Comp = bool(const int&, const int&);Foo sorted(Comp*);//正确,不同的参数列表Foo sorted(Comp*)const;//正确,两个版本都没有引用限定符
};
//这样会发生什么呢?自己去验证,还是很好想的
Foo Foo::sorted()const& {Foo ret(*this);return ret.sorted();
}
总结
。。。。。。。。。。。。。。。。。。。。。。。。。。。累死了,一个右值写了快五个小时,这个真的走心了,查阅了很多资料,经过理解才写出来的,(这个法则以及很多看着就很正式的话也不是我编的,都是来自权威著作上的)。