标签:赋值运算符 参数 排序 unique except 移动赋值运算符 必须 自我 default
新标准的一个最主要的特性就是可以移动而非拷贝对象的能力,在某些情况下,对象拷贝后会立即销毁,在这些情况下,使用移动而非拷贝对象能够大大提升性能。
另外像 IO
类 unique_ptr
这样的类,都包含不能被共享的资源,这些类型的对象不能拷贝但是可以移动。
为了支持移动操作,新标准引入了一种新的引用类型------右值引用。所谓右值引用就是必须绑定到右值的引用。
右值引用有一个重要的性质:智能绑定到一个将要销毁的对象。
int i = 42;
int &r = i; //正确,r是i的左值引用
int &&rr = i; //错误,不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; //错误,i*42是一个右值
const int &r3 = i * 42; //正确,可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42; //正确,将rr2绑定到右值上
左值有持久的状态,而右值要么是字面值常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,可以得知:
这两个意味着,使用右值引用的代码可以自由地接管所引用的对象资源。
变量可以看作只有一个运算对象没有运算符的表达式,变量表达式都是左值。
不能将一个右值引用绑定到一个右值引用类型的变量上:
int &&rr1 = 42; //正确,字面常量是右值
int &&r2 = rr1; //错误,表达式rr1是左值
虽然不能讲右值直接绑定到一个左值上,但是可以显式地将一个左值转换为对应的右值引用类型。可以通过调用 move
函数来获得绑定到左值上的右值引用,此函数定义在头文件 utility
中。
int rr3 = std::move(rr1); //正确
调用 move
函数之后,可以对源对象赋予一个新值或者销毁它,但不能使用一个移后原对象的值。
为了使类型支持移动操作,需要定义移动构造函数和移动赋值运算符。这两个成员将从给定的对象窃取资源而不是拷贝资源。
StrVec::StrVec(StrVec &&s) noexcept //移动操作不应该抛出任何异常
:elements(s.elemenrs),first_free(s.first_free),cap(s.cap)
{
//切断源对象与被移动资源的联系
s.elements = s.first_free = nullptr;
}
由于移动操作是窃取资源,通常不分配内存,所以,移动操作通常不会抛出异常,将此事通知标准库可以避免误认为会抛出异常而做的一些额外的工作。
通知标准库的方法是在构造函数中指明 noexcept
,noexcept
承诺函数不会抛出异常。
在类的头文件和定义中(如果定义在类外的话)都需要指定 noexcept
。
注意:
不抛出异常的移动构造函数和移动赋值运算符都必须标记为 noexcept
。
标准库容器对异常发生时其自身的行为提供保障,例如 vector
保证,如果调用 push_back 时发生异常,vector
自身不会改变。
对一个 vector
调用 push_back
可能会要求为 vector
重新分配内存,重新分配内存的过程中,vector
将从旧空间移动到新的内存中,如果使用的是移动构造函数并且在这个过程中发生了异常,不能保证vector的自身不变,但是如果使用的是拷贝构造函数就能保证异常发生时 vector
自身不变。
针对于以上机制,除非 vector 知道移动构造函数不会发生异常,否则重新分配内存的过程将调用拷贝构造函数而不是移动构造函数,为了显式地告知标准库移动构造函数可以安全使用,就需要通过移动构造函数和 noexcept
来做到这一点。
StrVec& StrVec::(StrVec &&rhs) noexcept
{
//自我赋值检查
if(this != &rhs){
free(); //释放已有的元素
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = nullptr;
}
return *this;
}
从一个对象移动数据并不会销毁对象,但有时希望移动操作之后,源对象会被销毁。因此必须保证,当编写一个移动操作时,源对进入一个可析构的状态。
除了将移后源对象置为析构安全的状态外,移动操作还必须保证对象仍然是有效的,对象有效就是指可以安全地为其赋予新值或可以安全地使用而不依赖当前的值。另一方面,移动操作对移后源对象中留下的值没有任何要求,因此,程序不应该依赖移后源对象中的数据。
注意:
移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值有任何假设。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非 static
数据成员都可以移动,编译器才会为它合成构造函数或移动赋值运算符。编译器可以移动内置类型的成员,如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员。
const
或是引用类型,则类的移动赋值运算符被定义为删除的。注意:
定义了一个移动构造函数或移动赋值运算符的类,必须也定义自己的拷贝操作,否则,这些成员默认被定义为删除的。
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,赋值操作的情况类似。
如果一个类有一个拷贝构造函数但未定义移动构造函数,在此情况下,编译器不会合成移动构造函数,如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使试图通过 move
来移动时也是如此。
class Foo{
public:
Foo() = default;
Foo(const Foo&);
}
Foo x;
Foo y(x); //拷贝构造函数,x是一个左值
Foo z(std::move(x)); //拷贝构造函数,因为没有定义移动构造函数
move(x)
返回的类型是 Foo&
,可以转换成 const Foo&
,因此,可以使用拷贝构造函数。
注意:
拷贝构造函数代替移动构造函数几乎肯定是安全的,赋值运算符的情况类似。拷贝构造函数满足对应移动构造函数的要求:它会拷贝给定的对象,并将原对象置于有效状态。
如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来移动的,拷贝赋值运算符和移动赋值运算符的情况类似。
class HasPtr{
public:
HasPtr(HasPtr &&p)noexcept : ps(p.ps),i(p.o) {p.ps = 0;}
HasPtr& operator = (HasPtr rhs)
{swap(*this,rhs);return *this;}
};
观察赋值运算符,这个运算符有一个非引用参数,这意味着此参数要进行拷贝初始化。依赖于实参的额类型,拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数-------左值被拷贝,右值被移动。因此,单一赋值运算符就实现了拷贝赋值运算符和移动运算符两种功能。
hp = hp2; //hp2 是一个左值,hp2将通过拷贝构造函数来拷贝
hp = std::move(hp2); //移动构造函数移动hp2
不管使用的是拷贝构造函数还是移动构造函数,赋值运算符的函数体都 swap
两个运算对象的状态。
注意:
更新三/五法则,一般来说五个拷贝控制成员应该看作一个整体,如果一个类定义了任何一个拷贝操作,就应该定义所有的五个操作。
新标准中定义了一种移动迭代器适配器,一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器,一般来说一个迭代器的解引用运算符返回一个指向元素的左值,与其他迭代器不同,移动迭代器的解引用运算符将生成一个右值引用。
通过标准库函数 make_move_iterator
函数将一个普通迭代器转换为一个移动迭代器,此函数接受一个迭代器参数,返回一个移动迭代器。
移动迭代器支持正常的迭代器操作,可以将一对迭代器传递给算法,
void StrVec::reallocate()
{
auto newcapacity = size() ? 2 * size() : 1;
auto first = alloc.allocate(newcapacity);
auto last = uninitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first);
free();
elements = first;
first_free = last;
cap = elements + newcapacity;
}
uninitialized_copy
对输入序列中的每个元素调用 construct
来将元素 拷贝到目的位置。此算法使用迭代器的解引用运算符从输入序列中提取元素。由于传递给它的是移动迭代器,因此解引用运算符生成的是一个右值引用,这意味着 construct
将使用移动构造函数来构造元素。
注意:
由于一个移后源对象具有不确定状态,对其调用 std::move
是危险的。当我们调用move
时,必须绝对确认移动后源对象没有其它用户。
如果一个成员函数同时提供拷贝和移动版本,一个版本指向接受一个指向 const
的左值引用,第二个版本接受一个指向非 const
的右值引用。
void push_back(const X&); //拷贝,绑定到任意类型的X
void push_back(X&&); //移动,只能绑定到类型X的可修改右值
如果希望从实参中窃取数据,通常传递一个右值引用,为了达到这个目的,实参不能是 const
类型。
从一个对象进行拷贝的操作不应该改变该对象,因此通常不需要定义一个接受一个 X&
参数的版本。
通常在一个对象上调用成员函数,而不管对象是一个左值还是一个右值:
string s1 = "a value",s2 = "another";
auto n = (s1 + s2).find('a');
还有:
s1 + s2 = "wow";
上面的表达式对右值进行了赋值,在新标准中,可以强制左侧运算对象 (this) 指向的运算对象是一个左值,方法是在在参数列表后放置一个引用限定符:
class Foo{
public:
Foo& operator=(const Foo&) &; //只能向可修改的左值赋值
};
Foo& Foo::operator=(const Foo&) &
{
....
return *this;
}
引用限定符可以是 &
或是 &&
分别指出 this 可以指向一个左值或右值。类似 const
限定符,引用限定符只能用于非 static
成员函数,且必须同时出现在函数的声明和定义中。
对于 &
限定的函数,只能将其用于左值,对于 &&
限定的函数,只能将其用于右值。
Foo &retFoo(); //返回一个引用,retFoo调用是一个左值
Foo retVal(); //返回一个值,retVal调用是一个右值
Foo i,j; //i,j是左值
i = j;
retFoo() = j; //正确,retFoo()返回一个左值
retVal() = j; //错误,retVal()返回一个右值
i = retVal(); //正确
一个函数可以同时使用 const 和引用限定符,在此情况下,引用限定符必须跟随在const 限定符之后:
class Foo{
public:
Foo someMes() & const; //错误,const限定符必须在前
Foo someMes() const &; //正确,const限定符必须在前
};
成员函数可以根据是否有const来区分重载版本,引用限定符也可以区分重载版本,并且可以综合引用限定符和const 来区分一个成员函数的重载版本:
class Foo{
public:
Foo sorted() &&; //可用于改变的右值
Foo sorted() const &; //可用于任何类型的Foo
private:
vector<int> data;
};
//本对象是右值,因此可以原址排序
Foo Foo::sorted() &&
{
sort(data.begin(),data.end());
return *this;
}
//本对象是const或是一个左值,哪种情况我们都不能对其进行原址排序
Foo Foo::sorted() const &
{
Foo ret*(*this); //拷贝一个副本
sort(ret.begin(),ret.end()); //排序副本
return ret; //返回副本
}
编译器会根据调用 sorted 的对象的左值/右值属性来确定使用哪个版本的sorted:
retVal.sorted(); //retVal()是一个右值,调用Foo::sorted() &&
retFoo.sorted(); //retFoo()是一个左值,调用Foo::sorted() const &
当我们定义 const 成员函数时,可以定义两个版本,唯一的差别是一个版本有const限定,另一个没有。
引用限定函数则不一样,如果我们定义两个或者两个以上具有相同名字的和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加:
class Foo
{
Foo sorted() &&;
Foo sorted() const; //错误,必须加上引用限定符
using Comp = bool (const int&,const int&);
Foo sorted(Comp*); //正确,不同的参数列表
Foo sorted(Comp*); //正确,两个版本都没有使用引用限定符
};
注意:
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须具有引用限定符。
标签:赋值运算符 参数 排序 unique except 移动赋值运算符 必须 自我 default
原文地址:https://www.cnblogs.com/xiaojianliu/p/12496755.html