1.移动语义
1.为什么要有移动语义?
C++中有拷贝构造函数和拷贝复制运算符,但是这需要占用一定的空间
class MyClass
{
public:
MyClass(const std::string& s)
: str{ s }
{};
MyClass(const MyClass& m)
{
str=m.str;
}
private:
std::string str;
};
int main()
{
MyClass A{ "hello" };
MyClass B=A;
return 0;
}
这个代码非常的简单就是A对象中有存储字符串hello\0,把A拷贝复制给B,此时会调用拷贝构造函数
但是来看下面的例子
class MyClass
{
public:
MyClass(const std::string& s)
: str{ s }
{};
private:
std::string str;
};
int main()
{
std::vector<MyClass> myClasses;
MyClass tmp{ "hello" };
myClasses.push_back(tmp);
myClasses.push_back(tmp);
return 0;
}
在这个例子中,我们创建了一个容器以及一个MyClass对象tmp,我们将tmp对象添加到容器中2次每次添加时,都会发生一次拷贝操作,但是既然tmp在两次拷贝构造之后就要销毁,那第二次拷贝构函数这个操作是不是有点冗余,要是能有一个函数可以把tmp对象的数据拷贝,但不需要执行拷贝构造函数就好了
2.移动语义
所谓移动语义,就像其字面意思一样,即把数据从一个对象中转移到另一个对象中,从而避免拷贝操作所带来的性能损耗
我们回到上文的例子,对于myClasses容器的第一次push_back,我们期望执行的是拷贝操作,而对于myClasses容器的第二次push_back,由于之后我们不再需要tmp对象了,因此我们期望执行的是移动操作
对于容器的push_back函数来说,它一定针对拷贝操作和移动操作有不同的重载实现,而重载用到的即是左值引用与右值引用。伪代码如下:
class vector
{
public:
void push_back(const MyClass& value) // const MyClass& 左值引用
{
// 执行拷贝操作
}
void push_back(MyClass&& value) // MyClass&& 右值引用
{
// 执行移动操作
}
};
std::vector真正做的,是委托具体类型自己去执行拷贝操作与移动操作
class MyClass
{
public:
MyClass(const std::string& s) //拷贝构造
: str{ s }
{};
MyClass(MyClass&& rvalue) //移动构造
:str{ std::move(rvalue.str) }
{}
private:
std::string str;
};
3.移动构造
在移动构造函数中,我们要做的就是转移成员数据。我们的MyClass有一个std::string类型的成员,该类型自身实现了移动语义,因此我们可以继续调用std::string类型的移动构造函数
在有了移动构造函数之后,我们就可以在需要时通过它来创建新的对象,从而避免拷贝操作的开销
int main()
{
std::vector<wrt::MyClass> myClasses;
wrt::MyClass tmp{ "hello" };
myClasses.push_back({ std::move(tmp) }); //调用移动构造
return 0;
}
还记得移动语义的精髓嘛?数据拿过来用就完事儿了。因此,在移动构造函数中,我们将传入对象A的数据转移给新创建的对象B。同时,还需要关注的重点在于,我们需要把传入对象A的数据清除,不然就会产生多个对象共享同一份数据的问题
class MyClass
{
public:
MyClass()
: val{ 998 }
{
name = new char[] { "Peter" };
}
// 实现移动构造函数
MyClass(MyClass&& rValue) noexcept
: val{ std::move(rValue.val) } // 转移数据
{
rValue.val = 0; // 清除被转移对象的数据
name = rValue.name; // 转移数据
rValue.name = nullptr; // 清除被转移对象的数据
}
~MyClass()
{
if (nullptr != name)
{
delete[] name;
name = nullptr;
}
}
private:
int val;
char* name;
};
int main()
{
MyClass A{};
MyClass B{ std::move(A) }; // 通过移动构造函数创建新对象B
return 0;
}
4.移动赋值运算符
与拷贝构造函数和拷贝赋值运算符一样,除了移动构造函数之外,C++11还引入了移动赋值运算符。移动赋值运算符也是接收右值引用,它的实现和移动构造函数基本一致。在移动赋值运算符中,我们也是从传入的对象中转移数据,并将该对象的数据清除:
class MyClass
{
public:
MyClass()
: val{ 998 }
{
name = new char[] { "Peter" };
}
MyClass(MyClass&& rValue) noexcept
: val{ std::move(rValue.val) }
{
rValue.val = 0;
name = rValue.name;
rValue.name = nullptr;
}
// 移动赋值运算符
MyClass& operator=(MyClass&& myClass) noexcept
{
val = myClass.val;
myClass.val = 0; //数据清空
name = myClass.name;
myClass.name = nullptr; //数据置空
return *this;
}
~MyClass()
{
if (nullptr != name)
{
delete[] name;
name = nullptr;
}
}
private:
int val;
char* name;
};
int main()
{
MyClass A{};
MyClass B{};
B = std::move(A); // 使用移动赋值运算符将对象A赋值给对象B
return 0;
}
5. 移动构造函数和移动赋值运算符的生成规则
—— 定义一个空类,C++98中是默认生产四个函数:构造 析构 拷贝构造 拷贝赋值运算符函数
但是C++11中,我们写一个空类,会默认生成6个函数:原先的四个+移动构造+移动赋值运算符函数
——如果在类中定义了拷贝构造函数或者拷贝赋值运算符或者析构函数,那么编译器就不会自动生成移动构造函数和移动赋值运算符。此时,如果调用移动语义的话,由于编译器没有自动生成,因此会转而执行拷贝操作
——析构函数有一点值得注意,许多情况下,当一个类需要作为基类时,都需要声明一个virtual析构函数,此时需要特别留意是不是应该手动的为该类定义移动构造函数以及移动赋值运算符(没有定义的话会默认调用拷贝操作)此外,当子类派生时,如果子类没有实现自己的析构函数,那么将不会影响移动构造函数以及移动赋值运算符的自动生成:
class MyClass
{
public:
MyClass()
{}
// 我们定义了拷贝构造函数,这会禁止编译器自动生成移动构造函数和移动赋值运算符
MyClass(const MyClass& value)
{}
};
int main()
{
MyClass A{};
MyClass B{ std::move(A) }; // 执行的是拷贝构造函数来创建对象B
return 0;
}
——如果我们在类中定义了移动构造函数,那么编译器就不会为我们自动生成移动赋值运算符。反之,如果我们在类中定义了移动赋值运算符,那么编译器也不会为我们自动生成移动构造函数
以移动构造函数为例,如果定义移动构造函数,编译器不会自动生成移动赋值运算符,此时,移动赋值运算符的调用并不会转而执行拷贝赋值运算符,而是会产生编译错误:
class MyClass
{
public:
MyClass()
{}
// 我们定义了移动构造函数,这会禁止编译器自动生成移动赋值运算符,并且对移动赋值运算符的调用会产生编译错误
MyClass(MyClass&& rValue) noexcept
{}
};
int main()
{
MyClass A{};
MyClass B{};
B = std::move(A); // 对移动赋值运算符的调用产生编译错误:attempting to reference a deleted function
return 0;
}
6noexcept
其实刚才在实现移动构造函数的时候 还有看string中支持的移动构造函数都有一个关键字noexcept
首先介绍一个概念 "强异常保证(strong exception guarantee)"
所谓强异常保证,即当我们调用一个函数时,如果发生了异常,那么应用程序的状态能够回到函数调用之前
如果学习过C++异常处理的小伙伴一看这个关键字就非常熟悉,他的意思在这里就是不会抛出异常的移动构造函数
拷贝构造函数是会分配内存,因此很可能会抛出异常,但是移动构造是更改数据的所有权,所以一般不会抛异常
先来看一个没有noexcept的拷贝构造函数
class MyClass
{
public:
MyClass(int x)
:_x(x)
{}
MyClass(const MyClass& lvalue) //拷贝构造函数
{
cout << "MyClass(const MyClass& lvalue" << endl;
_x = lvalue._x;
throw runtime_error("copy exception");
}
// 我们定义了移动构造函数,这会禁止编译器自动生成移动赋值运算符,并且对移动赋值运算符的调用会产生编译错误
MyClass(MyClass&& rValue) noexcept
:_x(std::move(rValue._x))
{
rValue._x = 0;
cout << " MyClass(MyClass&& rValue) noexcept" << endl;
}
~MyClass()
{
cout << "~MyClass()" << endl;
}
private:
int _x;
};
int main()
{
try
{
MyClass A{ 1 };
MyClass B{A};
//MyClass B{ std::move(A) };
}
catch(runtime_error e)
{
cout << "Catch!!!" <<e.what()<< endl;
}
return 0;
}
再来看移动拷贝里面抛异常的情况
class MyClass
{
public:
MyClass(int x)
:_x(x)
{}
MyClass(const MyClass& lvalue) //拷贝构造函数
{
cout << "MyClass(const MyClass& lvalue" << endl;
_x = lvalue._x;
throw runtime_error("copy exception");
}
// 我们定义了移动构造函数,这会禁止编译器自动生成移动赋值运算符,并且对移动赋值运算符的调用会产生编译错误
MyClass(MyClass&& rValue) noexcept
:_x(std::move(rValue._x))
{
rValue._x = 0;
cout << " MyClass(MyClass&& rValue) noexcept" << endl;
throw runtime_error("copy exception");
}
~MyClass()
{
cout << "~MyClass()" << endl;
}
private:
int _x;
};
int main()
{
try
{
MyClass A{ 1 };
MyClass B{ std::move(A) };
}
catch(runtime_error e)
{
cout << "Catch!!!" <<e.what()<< endl;
}
return 0;
}
但是如果你在声明了noexcept的移动构造函数中抛异常,此时会直接报错,并不会捕捉异常
2.完美转发
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); //const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
std::forward 完美转发在传参的过程中保留对象原生类型属性
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t)); //完美转发
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); //const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}