左值和右值概念:
当一个对象被用作右值的时候,用的对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
一个重要的原则:在需要右值的地方可以用左值代替,但是不能把右值当做左值使用。当一个左值被当做右值使用时,实际使用的是它的内容。
几种熟悉运算符的左右值情况:
使用关键字decltype的时候,如果表达式的求值结果是左值,则decltype作用于该表达式得到的是一个引用类型。
右值引用(&&)就是绑定到右值的引用。其重要性质是只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源移动到另一个对象中。
左值具有持久的状态,右值要么是字面常量,要么是表达式求值过程中创建的对象。
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使变量是右值引用类型也不行。
int &&rr1=42;//正确:字面常量是右值
int &&rr2=rr1;//错误,表达式rr1是左值
通过定义在头文件(utility)标准库的move函数,我们可以显式地将一个左值转换成对应的右值引用类型。
int &&rr3=std::move(rr1);//正确,必须使用std::move而不是用using
需要注意的是,我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
int main()
{
//原则:左值引用绑定左值,右值引用绑定右值,常量引用可以绑定右值。
int i=1;
const int &a1=i*2;//常量引用绑定左值
int &a2=i;//左值引用绑定左值,变量是左值
int &&a3=i*2;//右值引用绑定右值
//int &a4=i*2;//左值引用不能绑定右值
//int &&a5=i;//右值引用不能绑定左值
int &&a6=std::move(a3);//std::move函数可以返回左值的右值引用
}
类的移动构造函数和移动运算符
#include <string>
#include <iostream>
using std::string;
class A
{
public:
A(string s=""):sp(new string(s)){}//默认构造函数
A(A &&rhs) noexcept:sp(rhs.sp){rhs.sp=nullptr;}//移动构造函数
A& operator=(A &&rhs) noexcept//移动赋值函数
{
if(this!=&rhs)//处理自赋值
{
delete sp;//释放左侧对象
sp=rhs.sp;//赋值
rhs.sp=nullptr;//使rhs进入折构状态
}
return *this;
}
~A(){delete sp;}
void print_debug()
{
std::cout<<sp<<std::endl;
}
private:
string *sp;
};
int main()
{
A a1("hello world");
a1.print_debug();//string *sp的内存地址
A a2(std::move(a1));//移动构造
a1.print_debug();//0,即空对象null
a2.print_debug();// 内存地址
A a3("I have an apple"),a4;
a3.print_debug();//内存地址
a4.print_debug();//null 0
a4=std::move(a3);
a3.print_debug();//0 null
a4.print_debug();//内存地址
}
与拷贝操作不同,编译器不会为某些类合成移动操作。只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
#include <string>
#include <iostream>
class B
{
public:
int i;
std::string s;
void print_debug()
{
std::cout<<"i="<<i<<std::endl;
std::cout<<"s="<<s<<std::endl;
}
};
class C
{
public:
B mem;
};
int main()
{
B b,b2;
b.i=1;
b.s="hello world";
b2.i=2;
b2.s="I have an apple";
b.print_debug();//i=1.s="hello world"
B b1=std::move(b);//合成移动构造函数
b.print_debug(); //i=1.s=""
b1.print_debug();//i=1.s="hello world"
C c;
c.mem=b2;
c.mem.print_debug();//i=1.s="I have an apple"
C c1=std::move(c);//合成移动构造函数
c.mem.print_debug();//i=2.s=""
c1.mem.print_debug();//i=2.s="I have an apple"
return 0;
}
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义成删除的。
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值操作类似。即原则移动右值,拷贝左值。即当右边是一个右值,优先使用移动构造函数,当右边是一个左值,只能使用拷贝函数或者使用std::move将左值转换。
如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则对象是通过拷贝构造函数来“移动”的。拷贝赋值运算符和移动赋值运算符类似。
#include <string>
#include <iostream>
using std::string;
class A
{
public:
A(string s=""):sp(new string(s)){std::cout<<"A()"<<std::endl;}//默认构造函数
//A(A& rhs)
A(const A& rhs)//拷贝构造函数
{
string s=(*(rhs.sp))+" copy";//区别于移动构造
sp=new string(s);
std::cout<<"A(A& rhs)"<<std::endl;
}
A& operator=(const A &rhs)//拷贝赋值运算符
{
std::cout<<"A& operator=(const A &rhs)"<<std::endl;
string s=(*(rhs.sp))+" copy";
delete sp;
sp=new string(s);
return *this;
}
//A(A &&rhs) noexcept:sp(rhs.sp){rhs.sp=nullptr;}//移动构造函数
//A& operator=(A &&rhs) noexcept//移动赋值函数
//{
// if(this!=&rhs)//处理自赋值
// {
// delete sp;//释放左侧对象
// sp=rhs.sp;//赋值
// rhs.sp=nullptr;//使rhs进入折构状态
// }
// return *this;
//}
~A(){delete sp;}
void print_debug_sp_address()
{
std::cout<<sp<<std::endl;
}
void print_debug_sp_content()
{
std::cout<<*sp<<std::endl;
}
private:
string *sp;
};
A get(string s)
{
A a(s);
return a;
}
int main()
{
A a1("hello world");
a1.print_debug_sp_content();
A a2=a1;//对象为左值,调用拷贝赋值函数
//A a3=get("apple");//函数返回值为右值,调用移动构造函数
A a4=std::move(a1);//无移动赋值函数,右值也调用拷贝赋值
a2.print_debug_sp_content();//"hello world copy"
//a3.print_debug_sp_content();//"apple"
a4.print_debug_sp_content();//"hello world copy"
return 0;
}
赋值运算符可同时实现拷贝赋值和移动赋值,使用自定义版本的拷贝并交换赋值运算符(swap)。
#include <string>
#include <iostream>
using std::string;
class A
{
public:
A(string s=""):sp(new string(s)){std::cout<<"A()"<<std::endl;}//默认构造函数
A(const A& rhs)//拷贝构造函数
{
string s=(*(rhs.sp))+" copy construct";//区别于移动构造
sp=new string(s);
std::cout<<"A(A& rhs)"<<std::endl;
}
void swap(A &a,A &b)
{
string *sp=a.sp;
a.sp=b.sp;
b.sp=sp;
}
A& operator=(A rhs)//拷贝赋值和移动赋值运算符
{
swap(*this,rhs);
return *this;
}
A(A &&rhs) noexcept
{
sp=new string(*(rhs.sp)+" move construct");
rhs.sp=nullptr;
}//移动构造函数
~A(){delete sp;}
void print_debug_sp_address()
{
std::cout<<sp<<std::endl;
}
void print_debug_sp_content()
{
std::cout<<*sp<<std::endl;
}
private:
string *sp;
};
int main()
{
A a1("apple");
A a2,a3;
a2=a1;//拷贝赋值
a3=std::move(a1);//移动赋值
a2.print_debug_sp_content();//"apple copy construct"
a3.print_debug_sp_content();//"apple move construct"
return 0;
}
区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受T&&。
void push_back(const X&);//拷贝
void push_back(X&&);//移动
this的左值/右值属性的方式与定义const成员函数相同,即,在参数列表后放置一个 引用限定符。对于&限定的函数,我们只能将它用于左值;对于&&限定的函数,只能用于右值。一个函数可以同时用const和引用限定,此情况下,引用限定符必须跟随在const限定符之后。
Foo &retFoo();//返回一个引用。调用是一个左值
Foo rerVal();//返回一个值。右值
Foo i,j;
retFoo()=j;//正确
redVal()=j;//错误
i=retVal();//正确
Foo someMem() & const;//错误
Foo anotherMem() const &;//正确
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。