C++11开始引入了std::move,然后又引入了一个右值得概念, 之前又有一个左值得概念。左值和右值网上特别多,这里主要谈谈自己的理解。引入std::move主要是为了优化对象的生命周期,以及优化函数参数传递方式。
C++参数传递方式
1 2 3 4 5 6 | //值传递 void SetName(std::string name) { } std::string name = "123"; SetName(name); |
上面的函数参数是值传递,一般对于标准数据类型,会采用值传递,对于复杂类型,结构体,类对象等并不会用值传递,因为会多一份对象的拷贝
1 2 3 4 5 6 | //值传递 void SetName(const std::string& name) { } std::string name = "123"; SetName(name); |
引用传递可以看成是指针的传递,并不会有临时对象的构造和析构,所以性能上会好一些。
但是无论是哪种传递方式,调用者外部的变量 name 有时候感觉会有一次 "多余的构造和析构"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Apples { public: void Add(const std::string& name) { //push_back 会拷贝构造一份,然后放入vector中 names_.push_back(name); } private: std::vector<std::string> names_; }; Apples apple; std::string one = "one"; //使用 one 变量做一些操作 ....... //引用传递 apple.Add(one); |
调用者定义的变量 ‘one’ 通过Add 传递进去之后, one变量已经没有了用处,自然就会被析构掉。但是push_back 又拷贝了一份。是不是感觉这里多了一份呢? 是不是直接把one 变量放入到 vector 中就不会多构造一份呢? 确实是可以,我们可以用指针的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Apples { public: void Add(std::string* name) { names_.push_back(name); } private: std::vector<std::string*> names_; }; Apples apple; std::string *one = new std::string("one"); ...... apple.Add(one); |
std::move 解决什么问题
从语法上支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Apples { public: void Add(const std::string& name) { //push_back 会拷贝构造一份,然后放入vector中 names_.push_back(name); } private: std::vector<std::string> names_; }; Apples apple; std::string one = "one"; //使用 one 变量做一些操作 ....... apple.Add(std::move(one)); |
那是不是直接在 调用的地方使用 std::move 就可以了,当然不是了。因为从源码可以看出 std::move没有做任何实质性的操作,仅仅是个强制类型转换而已.
1 2 3 4 | template <class _Ty> _NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable return static_cast<remove_reference_t<_Ty>&&>(_Arg); } |
所以还需要自己来实现一些东西才能真正的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Apples { public: void Add(const std::string& name) { names_.push_back(name); } //新增一个右值参数的函数。 void Add(std::string&& name) { names_.push_back(std::move(name)); } private: std::vector<std::string> names_; }; Apples apple; std::string one = "one"; //std::move之后会强制转换成右值对象,这样就可以匹配到Apples 类中的带有右值得函数。 apple.Add(std::move(one)); |
所以要支持std::move 我们的类也需要提供一些带有右值得函数
- 转移构造
1
2
3Apples(Apples&& other) {
//do something
} - 转移赋值
1
2
3Apples& operator=(Apples&& other) {
//do somthing
}
Apples 对象实现了转移构造和转移赋值,所以就可以使用std::move
当然并不是,我自己总结了一下,如果
不需要自己实现的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Apples { public: Apples() { company = "test"; names_.push_back("abc"); } private: //下面两个成员变量 是stl中的,已经实现了std::move, 所以Apples 类不需要自己实现 std::string company; std::vector<std::string> names_; }; Apples apple1; // 将apple1 直接转移到了 apple2. 其实是调用了 转移构造,但是Apples并没有自己实现转移构造函数。 因为编译器有一个默认的。就像默认的拷贝构造函数一样。 Apples apple2 = std::move(apple1); |
需要自己实现的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | class Apples { public: Apples() { company_ = "conpany"; config_ = (char*)malloc(10); strcpy_s(config_, 9, "test"); } ~Apples() { if (nullptr != config_) { free(config_); config_ = nullptr; } } //转移构造。 转移构造的目的很多时候是为了避免"类似深拷贝", 直接使用"类似浅拷贝"的方式。 Apples(Apples&& right) { company_ = std::move(right.company_); config_ = right.config_; right.config_ = nullptr; } private: char* config_ = nullptr; std::string company_; }; Apples apple1; //会调用到转移构造 Apples apple2 = std::move(apple1); Apples apple3; //这里还需要实现一个转移赋值,否则也会出问题。 apple3 = std::move(apple2) |
总结
- std::move 简化了一些编程,可以动态转移对象的生命周期,减少不必要的对象构造和析构。
- 自定义的结构体或者类 根据 成员变量是否实现了 std::move,以及是否有析构,来决定要不要实现std::move。
- std::move没有实质性的move, 这是一个强制类型转换,在调用的时候,匹配相应的函数。