Learn C++学习笔记:第M章—移动语义和复制语义:为什么移动语义效率更高 & 怎么进行移动std::move

1、移动语义效率更高

前面讲到,之所以采用移动,原因①是为了避免多个智能指针指向同一个内存,导致的释放问题。除了这个优点之外,还有原因②移动语义还比复制语义多一个优点,效率更高。
具体来看一个例子:

template<class T>
class Auto_ptr3
{
	T* m_ptr;
public:
	Auto_ptr3(T* ptr = nullptr):m_ptr(ptr){}
	~Auto_ptr3(){delete m_ptr;}
 
	// 复制语义,进行深拷贝,避免重新释放内存
	Auto_ptr3(const Auto_ptr3& a){
		m_ptr = new T;
		*m_ptr = *a.m_ptr;
	}
 
	// 复制语义,同上
	Auto_ptr3& operator=(const Auto_ptr3& a){
		// Self-assignment detection
		if (&a == this)	return *this;
		// Release any resource we're holding
		delete m_ptr;
		// Copy the resource
		m_ptr = new T;
		*m_ptr = *a.m_ptr;
 
		return *this;
	}
 
	T& operator*() const { return *m_ptr; }
	T* operator->() const { return m_ptr; }
	bool isNull() const { return m_ptr == nullptr; }
};
 
class Resource{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};
 
Auto_ptr3<Resource> generateResource(){
	Auto_ptr3<Resource> res(new Resource);
	return res; // this return value will invoke the copy constructor
}
 
int main(){
	Auto_ptr3<Resource> mainres;
	mainres = generateResource(); // this assignment will invoke the copy assignment
	return 0;
}

一次简单的智能指针复制,这段程序将会进行3次内存的开辟释放!

Resource acquired
Resource acquired
Resource destroyed
Resource acquired
Resource destroyed
Resource destroyed

分析一下这6行输出分别对应哪一步:
①首先generateResource()函数里的new Resource在初始化时候会调用构造函数,打印一次Resource acquired
②然后复制给res时候调用构造函数Auto_ptr3(const Auto_ptr3& a),里面有new T,会在打印一次Resource acquired
③当Auto_ptr3 res(new Resource);这一行结束的时候,new Resource调用析构函数,打印一次Resource destroyed
④然后回到main函数,generateResource()函数赋值给mainres时候,调用Auto_ptr3& operator=(const Auto_ptr3& a),里面还有一次new T,这就会再次打印Resource acquired
mainres = generateResource();这一行结束时,generateResource()函数的返回值res会被释放,调用析构函数,打印一次Resource destroyed
⑥当函数结束,mainres释放,调用析构函数,再次打印Resource destroyed

如果采用复制语义,简简单单的一个复制就要发生三次内存的开辟和释放,这效率能不低吗?
如果采用移动语义,只需要一次,请看:
将复制函数分别改为:

	Auto_ptr4(Auto_ptr4&& a) noexcept
		: m_ptr(a.m_ptr)
	{
		a.m_ptr = nullptr; // we'll talk more about this line below
	}
 
	// Move assignment
	// Transfer ownership of a.m_ptr to m_ptr
	Auto_ptr4& operator=(Auto_ptr4&& a) noexcept
	{
		// Self-assignment detection
		if (&a == this)
			return *this;
 
		// Release any resource we're holding
		delete m_ptr;
 
		// Transfer ownership of a.m_ptr to m_ptr
		m_ptr = a.m_ptr;
		a.m_ptr = nullptr; // we'll talk more about this line below
 
		return *this;
	}

如此一来,上面的②和④两个内存开辟就避免了,只会产生一次内存的开辟和释放。

移动语义两个优点:
1、可以避免两个智能指针指向同一个内存
2、可以提高程序效率

2、怎么进行移动语义:std::move

使用std::move需要引入头文件: #include

移动语义会对被移动的对象进行更改,所以移动左值通常是不安全的,因为后续可能会使用到左值,所以我们需要对右值进行移动。因为右值只是临时的对象,总会在表达式末尾销毁,它原本上不会再次被使用。

所以针对左值和右值,可以有不同的重载,以便程序分别进行移动语义和复制语义。
移动不仅局限于之前举的例子:类这种,字符串等普通类型也可以。

对比以下例子:

template<class T>
void myswap(T& a, T& b) 
{ 
  T tmp { a };             // invokes copy constructor
  a = b;                   // invokes copy assignment
  b = tmp;                 // invokes copy assignment
}

template<class T>
void myswap(T& a, T& b) 
{ 
  T tmp { std::move(a) }; // invokes move constructor
  a = std::move(b);       // invokes move assignment
  b = std::move(tmp);     // invokes move assignment
}

但是需要注意的是,移动后的的左值不在保有原来的值,本身被清空,具体可以参看下面的例子:

#include 
#include 
#include  // for std::move
#include 
 
int main(){
	std::vector<std::string> v;
	std::string str = "Knock";
 
	std::cout << "复制语义\n";
	v.push_back(str); 
	
	std::cout << "str: " << str << '\n';
	std::cout << "vector: " << v[0] << '\n';
 
	std::cout << "\n移动语义\n";
	v.push_back(std::move(str)); // calls r-value version of push_back, which moves str into the array element
	
	std::cout << "str: " << str << '\n';
	std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';
 
	return 0;
}

输出结果如下:

Copying str
str: Knock
vector: Knock

Moving str
str:
vector: Knock Knock

请注意,这里面的str变量被移动后已经被清空。

当我们想要把左值当成右值使用时,以便调用move语义而不是copy语义时,就可以使用std::move。

3、std::move的安全使用

因为经过move之后的变量会被清空,所以这里存在安全隐患:
对于某种非临时对象,假如被移动的对象因为某种原因移动失败,这个源对象已经被损坏了。而复制语义不会存在这种,无论复制是否成功,源对象都不会被改变。
所以我们需要一种更安全的移动方式。

3.1 std::move_if_noexcept

std::move_if_noexcept如果对象具有noexcept move构造函数,则将返回可移动的r值,否则将返回可复制的l值。这样保证移动的安全性。

使用方法也很简单,只需要将std::move替换为std::move_if_noexcept即可。使用实例:

//origin
a = std::move(b); 

//new 
a = std::move_if_noexcept(b)

你可能感兴趣的:(C++)