新标准最主要的特性是可以移动而非拷贝对象的能力。避免了由于移动对象较大而拷贝代价过大的缺点。标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。 右值引用。为了支持移动操作而引入的新的引用类型。通过&&来获得右值引用,而不是(&)取址符。一般而言,一个左值表达式是一个对象身份,而一个右值表达式是对象的值。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。
对于常规的引用,我们在定义时不能将其绑定到要求转换的表达式、字面常量或是返回最右值的表达式
但是,右值引用却相反,可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上
int i = 33; //我们定义了一个int类型值
int &r = i; //正确:r引用i
int &&rr = i; //错误:不能将右值引用绑定到一个左值上
int &r2 = i * 2; //错误:i * 2是一个右值
const int &r3 = i * 3; //正确:我们可以用一个const的引用绑定到一个右值上
int &&rr2 = i * 3; //正确:可以将rr2绑定到乘法结果上
####返回左值引用的函数,连同赋值,下标,解引用和前置递增/递减运算符,都是返回左值表达式的例子。我们可以将常规的引用绑定到其上
####返回非引用类型的函数,连同算术,关系,位运算和后置递增/递减运算符,都是生成右值。我们可以将一个const的左值(常规)引用或者一个右值引用绑定到这类表达式上
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,因此
所引用的对象将要被销毁
该对象没有其他用户
也就意味着:使用右值引用的代码可以自由接管所引用的对象的资源。
#Note#
右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态
变量可以看作只有一个运算对象而没有运算符的表达式。类似其他任何表达式,变量表达式也有属性,它是左值。因此,不能将一个右值引用绑定到另一个右值引用上来。
int &&rr1 = 32; //正确:字面常量是右值
int &&rr2 = rr1; //错误:表达式rr1是左值
但是我们可以通过调用move标准库函数来获得绑定到左值上的右值引用。
int &&rr3 = std::move(rr1); //正确
#注意#
调用move就意味着承诺:除了对rr1赋值或销毁外,我们将不再使用它。
即,我们调用move后,我们不能对移后源对象的值做任何假设
我们可以销毁一个移后源对象,也可以赋予新值,但是不能使用一个移后源对象的值。
类似string类,如果我们自己的类也支持移动和拷贝,那么也能从中受益
StrVec::StrVec(StrVec &&s) noexcept : element(s.element), first_free(s.first_free), cap(s.cap)
{
//令s进入空指针状态,对其运行析构函数是安全的
s.element = s.first_free = s.cap = nullptr;
}
关键字noexcept在新标准中引入的。
noexcept是我们承诺一个函数不抛出异常的一种方法,出现在阐述列表和初始化列表直接
class StrVec
{
public:
StrVec(StrVec&&) noexcept;
};
StrVec::StrVec(StrVec &&s) noexcept : /* 成员初始化器 */
{ /* 函数体 */ }
#Note#
不抛出异常的移动构造函数和移动赋值运算符必须标记位noexcept
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
//直接检测自赋值
if(this != &rhs) //这里检测this的地址和rhs的地址是否一样,即两者是否指向相同的对象
{
free(); //释放原有元素
element = rhs.element;
first_free = rhs.first_free;
cap = rhs.cap;
//然后将rhs置为可析构状态
rhs.element = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
只有当一个类没有定义任何自己的拷贝控制成员,且他的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
假定Y是一个类,它定义了自己拷贝构造函数,但是未定义自己的移动构造函数
struct hasY
{
hasY() = default;
hasY(hasY&&) = default;
Y mem; //hasY将有一个删除的移动构造函数
} //即编译器可以拷贝类型为Y的对象,但不能移动他们。
//如果我们显示地要求一个移动构造函数,编译器无法为其生成
hasY hy, hy2 = std::move(hy); //错误:移动构造函数是删除的
如果一个编译器既有移动构造函数,也有拷贝构造函数,编译器将使用普通的函数匹配规则来确定使用哪个构造函数。
例如:在StrVec类中,拷贝构造函数接收一个const StrVec的引用,可以适用于任何可以转换为StrVec的类型
移动构造函数接收一个StrVec&&参数,只能用于实参是右值的情况
StrVec v1, v2;
v1 = v2; //v2是左值:使用拷贝赋值
StrVec getVec(istream &); //getVec返回一个右值
v2 = getVec(cin); //getVec(cin)是一个右值:使用移动赋值
当一个类只定义拷贝构造函数,没有定义移动构造函数时,由于可以将一个右值引用转换为类的常量引用
class Foo
{
public:
Foo() = default;
Foo(const Foo&); //拷贝构造函数
//其他成员定义,但未定义移动构造函数
}
Foo x;
Foo y(x);//使用拷贝构造函数
Foo z(std::move(x)); //使用拷贝构造函数,因为移动构造函数未定义
因为调用了move(x),返回一个绑定到x的Foo&&。由于可以将一个Foo&&转化为const Foo&,
因此可以执行拷贝而不报错
建议:应该将五个拷贝控制成员看作一个整体,一般来说,如果一个类中定义了任何一个拷贝操作,它就应该定义所有五个操作。有些类必须要定义拷贝构造函数、拷贝赋值运算符和析构函数。这些类通常拥有一个资源,而拷贝资源又通常会产生不必要的开支,因此定义移动构造函数和移动赋值运算符可以避免此问题。
对于Message类和Folder类,通过定义移动操作,Message可以使用string和set的移动操作来避免contents和folders拷贝时的额外开销
//从本Message移动Folder指针
void Message::move_Folders(Message *m)
{
folders = std::move(m->folders); //使用set的移动赋值运算符
for(auto f : folders)
{
f->remMsg(m);
f->addMsg(this);
}
m->folders.clear(); //确保销毁m是无害的
//在执行move后,m.folders是空的,但不知道包含什么内容。因此确保为空
}
//Message的移动构造函数调用move来移动contents,并默认初始化自己的folders成员
Message::Message(Message&& m) : contents(std::move(m.contents))
{
move_Folders(&m); //移动folders并更新Folder指针
}
//移动赋值运算符直接检查自赋值情况
Message& Message::operator=(Message &&rhs)
{
if(&rhs != this)
{
remove_from_Folders(); //将原本的Message从所有Folders中移除
contents = std::move(rhs.contents); //移动赋值运算符,从rhs将contests移动到this对象。
move_Folder(&rhs); //重置Folders指向本Message
}
return *this;
}
建议:由于一个移后源对象具有不确定状态,对其调用std::move时危险的。当我们调用move时,必须确认移后源对象没有其他用户。 除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动的版本,它也能从中受益。
区分移动和拷贝的重载函数通常是,一个版本接收一个const T&,另一个版本接收T&&
例如:
class StrVec
{
public:
void push_back(const std::string&); //拷贝元素
void push_back(std::string&&); //移动元素
//其他成员定义
};
void StrVec::push_back(const string& s)
{
chk_n_alloc(); //确保又空间容纳新的元素
//在first_free指向的元素中构造一个s的副本
alloc.construct(first_free++, s);
}
void StrVec::push_back(string&& s)
{
chk_n_alloc(); //确保空间够用
alloc.construct(first_free++, std::move(s));
}
StrVec vec; //空vec
string s = "some string"; //定义了一个string,s是一个左值
vec.push_back(s); //调用的const string&版本
vec.push_back("other string"); //“other string”为字符串常量,因此调用的string&&版本
引用限定符的使用
我们通常在一个对象上调用成员函数,而不管该对象是一个左值还是右值
string s1 = "vaule 1", s2 = "value 2";
auto n = (s1 + s2).find('v');
s1 + s2返回的是一个右值,我们直接调用了find成员,有时候右值的使用方式可能令人惊讶
可以 s1 + s2 = "wow!" 编译器不会报错并能正确赋值给一个右值
通过在参数列表后放置一个引用限定符
class Foo
{
public:
Foo &operator=(const Foo&) &; //表示左值限定,即只能向可修改的左值赋值
Foo someMem() const &; //当引用限定符和const同时使用,const一定在前。
Foo sorted() &&; //表示右值限定,即只能向可改变的右值赋值
Foo sorted() const &; //可用于任何类型的Foo限定
private:
vector<int> data;
}
Foo &Foo::operator=(const Foo &rhs) &
{
//执行rhs赋予本对象所需的工作
return *this;
}
Foo Foo::sorted() &&
{
sort(data.begin(), sort.end());
return *this;
}
Foo Foo::sorted() const &
{
Foo ret(*this); //拷贝一个副本
sort(ret.data.begin(), ret.data.end()); //排序副本
return ret; //返回副本
}
Foo &retFoo(); //返回一个引用,是左值
Foo retVal(); //返回一个值,是右值
Foo i, j; //i和j是左值
retFoo() = i; //正确:retFoo()返回一个左值
retVal() = j; //错误:reVal()返回一个右值
i = retVal(); //正确,我们可以将一个右值赋值操作给一个左值
retVal().sorted(); //reVal()是一个右值,调用Foo::sorted() &&
retFoo().sorted(); //retFoo()是一个左值,调用Foo::sorted() const &
如果我们定义两个或两个以上具有相同名字,相同参数列表的成员函数,如果有其中一个具有引用限定符,其他所有同名同参的成员都要带上限定符