Move Semantics
关于移动语义与完美转发,Back To Basics 2021 讲解的深入浅出,因此做此篇笔记进行记录
首先看下面三行代码
string getData() {
return "data\0";
}
int main()
{
std::vector<std::string> coll;
coll.reserve(3);
std::string s{ getData() };
coll.push_back(s);
coll.push_back(getData());
}
经过这几行代码,在C++ 98 堆栈中的情况如下:
可以看到,代码中先是将getData中的返回值复制给了s,因此s占用了一段堆空间,之后执行
coll.push_back(s);时是进行了一次堆复制,给到coll的第一段空间
而coll.push_back(getData());在执行时:
getData() 在return的时候,会申请一段堆空间,之后将这段堆空间中的内容复制给coll的第二段,这种方式当然是没有bug出现的,但可以明显的看到,getData所申请的空间利用率较低,申请堆空间再进行释放也无疑会占用资源。
而C++11中引入了新特性来解决这种资源浪费,即移动语义:
将栈中的getData返回地址删除,直接将堆空间中的内存交给coll
毫无疑问,这种方式是比用一个字符串s进行复制来的更快,但可读性也会因此降低,代码的可读性也是开发时候的重点。
因此C++11中引入了另一种新特性std::move();
如果将上面的代码更改,用这种方式执行:
string getData() {
return "data\0";
}
int main()
{
std::vector<std::string> coll;
coll.reserve(3);
std::string s{ getData() };
coll.push_back(s);
coll.push_back(std::move(s));
}
堆栈中的情况则如下:
s则变为了将亡值,即有效但未指明的状态。
当然此时就可以再次对s进行赋值变为正常值。
比较推荐的用法为如下:
1、输入时进行内存转移
std::vector<std::string> allRows;
std::string row;
while (std::getline(myStream, row)) {
allRows.push_back(std::move(row));
}
2、交换两个变量:
void swap(std::string& a, std::string& b) {
std::string tmp{std::move(a)};
a = std::move(b);
b = std::move(tmp);
}
C++的vector中内置了两种push_back:一种带移动语义一种不带,可以根据在vs中直接跳转到 两个不同的实现:
这里可以直接看到C++对coll.push_back(getData());进行了优化直接使用了移动语义的版本,即使用右值引用来将getData() 返回地址进行转交。
移动语义在类中的使用
如果类中不声明移动构造函数和拷贝构造函数,那么移动构造函数是默认存在的,请看下面的代码:
class Cust {
private:
std::string first;
std::string last;
int val;
public:
Cust(const std::string& f, const std::string& l, int v) : first{ f }, last{ l }, val{ v } {}
friend std::ostream& operator<< (std::ostream& strm, const Cust& c) {
return strm << "[" << c.val << ":" << c.first << " " << c.last << "]";
}
};
int main()
{
std::vector<Cust> v;
Cust c2{ "Joe","Fox",77 };
v.push_back(std::move(c2));
}
如果传入的是一个右值,则在执行的时候回默认直接调用移动构造函数
但如果声明了一段拷贝构造函数,如下:
class Cust {
private:
std::string first;
std::string last;
int val;
public:
Cust(const std::string& f, const std::string& l, int v) : first{ f }, last{ l }, val{ v } {}
Cust(const Cust&) = default;// 拷贝构造
friend std::ostream& operator<< (std::ostream& strm, const Cust& c) {
return strm << "[" << c.val << ":" << c.first << " " << c.last << "]";
}
};
int main()
{
std::vector<Cust> v;
Cust c2{ "Joe","Fox",77 };
v.push_back(std::move(c2));
}
则在执行的时候即使使用了右值进行传值,也不会将所有权转交,而是自动调用拷贝构造
可以看到c2中仍然保存着原有的内容,没有变为将亡值。
如果自行已了移动构造函数:
class Cust {
private:
std::string first;
std::string last;
int val;
public:
Cust(const std::string& f, const std::string& l, int v) : first{ f }, last{ l }, val{ v } {}
// Cust(const Cust&) = default;// 拷贝构造
Cust(const Cust& c): first{c.first},last{c.last},val{c.val}{}// 拷贝构造
Cust(Cust &&c) // 移动构造
: first{ std::move(c.first) }, last{ std::move(c.last) }, val{ c.val }{
c.val *= -1;
}
friend std::ostream& operator<< (std::ostream& strm, const Cust& c) {
return strm << "[" << c.val << ":" << c.first << " " << c.last << "]";
}
};
执行的结果如下:
因为代码中转交val的所有权而是将其*-1 因此还存在内容 -77。
完美转发:
如果我们想要一段代码能够接受所有的值类型,则需要对一段代码进行三次重载:
而如果我们一个函数有三个形参,那么我们则需要27个重载来实现这种功能。
但这种形式显然很麻烦,因此C++11引入了另一种新特性,完美转发
完美转发需要有三个要点:
1、使用模板变量
2、定义变量的时候带上双引用符号&&
3、使用std::forward<>() 进行所有权的转交
即使用如下方式定义:
class C {
};
void foo(const C&);
void foo(C&);
void foo(C&&);
template<typename T>
void callFoo(T&& x) {
foo(std::forward<T>(x));
}
这样则可以接受任何类型的参数,只有在x为右值时,std::forward<>() 才退化为std::move()
右值引用和universal reference(万能引用或未定义引用)的区别
如下只有在使用模板进行定义的时候,才代表universal reference 其他则都是右值引用
而vector中如何对此进行支持的呢?
引入了emplace_back:情况emplace_back的源码:
template <class... _Valty>
_CONSTEXPR20_CONTAINER decltype(auto) emplace_back(_Valty&&... _Val) {
// insert by perfectly forwarding into element at end, provide strong guarantee
auto& _My_data = _Mypair._Myval2;
pointer& _Mylast = _My_data._Mylast;
if (_Mylast != _My_data._Myend) {
return _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
}
emplace_back 中使用的就是univesal reference
此时我们则可以调用emplace_back 来进行之前我们预想的那种复杂入口参数:
class Cust {
private:
std::string first;
std::string last;
int val;
public:
Cust(const std::string& f, const std::string& l, int v) : first{ f }, last{ l }, val{ v } {}
// Cust(const Cust&) = default;// 拷贝构造
Cust(const Cust& c): first{c.first},last{c.last},val{c.val}{}// 拷贝构造
Cust(Cust &&c) // 移动构造
: first{ std::move(c.first) }, last{ std::move(c.last) }, val{ c.val }{
c.val *= -1;
}
friend std::ostream& operator<< (std::ostream& strm, const Cust& c) {
return strm << "[" << c.val << ":" << c.first << " " << c.last << "]";
}
};
int main(){
std::vector<Cust> v;
Cust c2{ "Joe","Fox",77 };
v.push_back(std::move(c2));
std::cout << "c2:" << c2 << endl;
std::string tempfirst{ "Jil" };
std::string templast{ "Cook" };
v.emplace_back(std::move(templast), templast, 39);
}
而不是在vector中实现27中形式的push_back.
显然这种混合了右值,左值的情况,push_back是无法实现的:
如果调用则会报错
auto&& 作为右值/未定义 引用的各种情况:
可以看到auto& 是不能接受右值,而auto&&则可以。