std::move()与移动构造函数

目录

std::move()

C++移动构造函数


std::move()

std::move的功能是将一个左值强制转化为右值引用,并返回该右值引用,继而可以通过右值引用使用该值,以用于移动语义(移动构造函数或移动赋值运算符)。

std::move 的函数原型定义

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type&&>(t);
}

首先要知道,虽然move的功能是将一个左值转换为右值,但是在调用move时传给move的参数可以是左值也可以是右值。

std::move的函数实现方式

函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配(可以传递左值或右值)。关于引用折叠的使用场景如下:

当传入的参数为左值时,形参就变为了T& && t,应用折叠后就变为了T&

string s("hello");
std::move(s) => std::move(string& &&) => 引用折叠后 std::move(string&)
此时:T的类型为string&

typename remove_reference<T>::type,即返回值的类型type为string 
整个std::move被实例化如下
string&& move(string& t) //t为左值,移动后不能在使用t
{
    //通过static_cast将string&强制转换为string&&
    return static_cast<string&&>(t); 
}

当传入的参数为右值时,形参就变为了T&& && t,应用折叠后就变为了T&&

std::move(string("hello")) => std::move(string&&)
//此时:T的类型为string 

typename remove_reference<T>::type,即返回值的类型type为string 
//整个std::move被实例如下
string&& move(string&& t) //t为右值
{
    return static_cast<string&&>(t);  //返回一个右值引用
}

简单来说,右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用。 

我们再来看一下函数体的实现

return static_cast<typename remove_reference<T>::type&&>(t);

我们来分析一下remove_reference的实现

//原始的,最通用的版本
template <typename T> struct remove_reference{
    typedef T type;  //定义T的类型别名为type
};
 
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
 
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }   
  
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本 

总结

        std::move实现,首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast<T>之所以能使用类型转换,是通过remove_refrence<T>::type模板移除T&&,T&的引用,获取具体类型T。

示例分析1

//摘自https://zh.cppreference.com/w/cpp/utility/move
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
    std::string str = "Hello";//此时的str是个左值
    std::vector<std::string> v;

    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    std::cout << "After copy, str is \"" << str << "\"\n";

    //调用移动构造函数,并且会在移动构造函数内将str置空,因此最好不要再次使用str
    v.push_back(std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n";
    std::cout << "The contents of the vector are \"" << v[0]
                                         << "\", \"" << v[1] << "\"\n";
}

输出

After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"

示例分析2 

#include <iostream>
using namespace std;
class movedemo {
public:
	movedemo() :num(new int(0)) {
		cout << "construct!" << endl;
	}
	//拷贝构造函数
	movedemo(const movedemo& d) :num(new int(*d.num)) {
		cout << "copy construct!" << endl;
	}
	//移动构造函数
	movedemo(movedemo&& d) :num(std::move(d.num)) {
		d.num = NULL;
		cout << "move construct!" << endl;
	}
public:     //这里应该是 private,使用 public 是为了更方便说明问题
	int* num;
};
int main() {
	movedemo demo;
	cout << "demo2:\n";
	movedemo demo2 = demo;
	//cout << *demo2.num << endl;   //可以执行
	cout << "demo3:\n";
	movedemo demo3 = std::move(demo);
	//此时 demo.num = NULL,因此下面代码会报运行时错误
	//cout << *demo.num << endl;
	return 0;
}

输出

construct!
demo2:
copy construct!
demo3:
move construct!

C++移动构造函数

现在有如下一段代码 

#include <iostream>
using namespace std;
class demo{
public:
   demo():num(new int(0)){
      cout<<"construct!"<<endl;
   }
   //拷贝构造函数
   demo(const demo &d):num(new int(*d.num)){
      cout<<"copy construct!"<<endl;
   }
   ~demo(){
      cout<<"class destruct!"<<endl;
   }
private:
   int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

执行过程:

  1. 执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的默认构造函数生成一个匿名对象;
  2. 执行 return demo() 语句,会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_demo() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁);
  3. 执行 a = get_demo() 语句,再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_demo() 函数返回的对象会被析构);
  4. 程序执行结束前,会自行调用 demo 类的析构函数销毁 a。

执行结果:

construct!            <-- 执行 demo()
copy construct!       <-- 执行 return demo()
class destruct!       <-- 销毁 demo() 产生的匿名对象
copy construct!       <-- 执行 a = get_demo()
class destruct!       <-- 销毁 get_demo() 返回的临时对象
class destruct!       <-- 销毁 a

        如上所示,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。

        那么当类中包含指针类型的成员变量,使用其它对象来初始化同类对象时,怎样才能避免深拷贝导致的效率问题呢?C++11 标准引入了解决方案,该标准中引入了右值引用的语法,借助它可以实现移动语义。

        简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

        事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

        以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。

下面程序对 demo 类进行了修改:

#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    demo(const demo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。

当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。

此时的输出结果为

construct!             <--执行demo()
move construct!        <--执行return demo,将demo()生成的匿名对象通过移动构造函数转移给临时对象
class destruct!        <--销毁demo()产生的匿名对象
move construct!        <--执行a=get_demo(),将临时对象通过移动构造函数转移给a
class destruct!        <--销毁get_demo()返回的临时对象
class destruct!        <--销毁a

如果使用左值初始化同类对象,但也想调用移动构造函数完成,有没有办法可以实现呢?

C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。

猜你喜欢

转载自blog.csdn.net/ThinPikachu/article/details/122537693