前面讲到,之所以采用移动,原因①是为了避免多个智能指针指向同一个内存,导致的释放问题。除了这个优点之外,还有原因②移动语义还比复制语义多一个优点,效率更高。
具体来看一个例子:
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
这一行结束的时候,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、可以提高程序效率
使用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。
因为经过move
之后的变量会被清空,所以这里存在安全隐患:
对于某种非临时对象,假如被移动的对象因为某种原因移动失败,这个源对象已经被损坏了。而复制语义不会存在这种,无论复制是否成功,源对象都不会被改变。
所以我们需要一种更安全的移动方式。
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)