参考文献
- https://www.cnblogs.com/sunchaothu/p/11392116.html
https://stackoverflow.com/questions/3106110/what-are-move-semantics/3109981#3109981
#include
#include
using namespace std;
class A{
public:
A():i(new int[500]){
cout<<"class A construct!"<
对于拷贝构造函数运行时可以看到
class A construct!
class A copy!
class A destruct!
class A copy!
class A destruct!
class A destruct!
发生了一次构造和两次拷贝!在每次拷贝中数组都得重新申请内存,而被拷贝后的对象很快就会析构,这无疑是一种浪费。
对于移动移动构造函数;可以看到输出为
class A construct!
class A move
class A destruct!
class A move
class A destruct!
class A destruct!
原先的两次构造变成了两次移动!!在移动构造函数中,我们做了什么呢,我们只是获取了被移动对象的资源(这里是内存)的所有权,同时把被移动对象的成员指针置为空(以避免移动过来的内存被析构),这个过程中没有新内存的申请和分配,在大量对象的系统中,移动构造相对与拷贝构造可以显著提高性能!这里noexcept告诉编译器这里不会抛出异常,从而让编译器省一些操作(这个也是保证了STL容器在重新分配内存的时候(知道是noexpect)而使用移动构造而不是拷贝构造函数),通常移动构造都不会抛出异常的。
int lv = 4;
int &lr = lv;// 正确,lr是l的左值引用
int &&rr = lv; // 错误,不可以把右值引用绑定到一个左值
如果使用std::move 函数
int &&rr = std::move(lv); // 正确,把左值转换为右值
可以看到 std::move的作用是把左值转换为右值的。
让我们看一看 std::move 的源码实现:
// FUNCTION TEMPLATE move
template
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast&&>(_Arg);
}
可以看到std::move 是一个模板函数,通过remove_reference_t获得模板参数的原本类型,然后把值转换为该类型的右值。用C++大师 Scott Meyers 的在《Effective Modern C++》中的话说, std::move 是个cast ,not a move.
值得注意的是: 使用move意味着,把一个左值转换为右值,原先的值不应该继续再使用(承诺即将废弃)
我们可以使用 move语义实现一个 交换操作,swap;在不使用 Move 语义的情况下
swap(A &a1, A &a2){
A tmp(a1); // 拷贝构造函数一次,涉及大量数据的拷贝
a1 = a2; // 拷贝赋值函数调用,涉及大量数据的拷贝
a2 = tmp; // 拷贝赋值函数调用,涉及大量数据的拷贝
}
如果使用 Move语义,即加上移动构造函数和移动赋值函数:
void swap_A(A &a1, A &a2){
A tmp(std::move(a1)); // a1 转为右值,移动构造函数调用,低成本
a1 = std::move(a2); // a2 转为右值,移动赋值函数调用,低成本
a2 = std::move(tmp); // tmp 转为右值移动给a2
}
可以看到move语义确实可以提高性能,事实上, move语义广泛地用于标准库的容器中。C++11标准库里的std::swap 也是基于移动语义实现的。
说到了 swap, 那就不得不说一下啊 move-and-swap 技术了
看下面一段代码,实现了一个 unique_ptr ,和标准的std::unqiue_ptr的含义一致,智能指针的一种。
template
class unique_ptr
{
T* ptr;
public:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
// move constructor
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
/* unique_ptr& operator=(unique_ptr&& source) // 这里使用右值引用
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
} */
// move and swap idiom replace the move assignment operator
unique_ptr& operator=(unique_ptr rhs) // 这里不用引用,会调用移动构造函数
{
std::swap(ptr, rhs.ptr);
// std::swap(*this,rhs) // is also ok
return *this;
}
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
};
在这里如果要按照常规办法写移动赋值函数,函数体内需要写一堆检查自赋值等冗长的代码。使用 move-and-swap语义,只用简短的两行就可以写出来。 在移动赋值函数中 source 是个局部对象,这样在形参传递过来的时候必须要调用拷贝构造函数(这里没有实现则不可调用)或者移动构造函数
,(事实上仅限右值可以传进来了)。然后 std::swap 负责把原先的资源和source 进行交换,完成了移动赋值。这样写节省了很多代码,很优雅。
右值引用的出现弥补了 C++ 在移动语义上的缺失。在右值引用出现之前,我们在函数调用传参时,只有两种语义:给它一份拷贝(按值传递),或者给它一份引用(按引用传递)。void inc_by_value(int i) { ++i; }
void inc_by_ref(int &i) { ++i; }
int main() {
int i = 0;
inc_by_value(i);
inc_by_ref(i);
std::cout << i << std::endl; // output: 1
}
在上面的这个场景中,语义的缺失并不明显,但当我们处理持有资源的对象时,就不是那么和谐了:
class Socket {
public:
void take(Socket other) { sockfd_ = other.sockfd_; }
void take(Socket &other) {
sockfd_ = other.sockfd_;
other.sockfd_ = 0;
}
private:
int sockfd_;
};