拷贝与交换惯用法
用拷贝与交换惯用法实现赋值运算符,优点是简洁优雅,天然能处理自赋值且异常安全,瑕疵是会有额外的开销(包括额外的时间与空间成本,但大多数情况下都微不足道)。
class Widget
{
public:
Widget() : p(new int) {}
Widget(int a) : p(new int(a)) {}
Widget(const Widget& theWidget);
Widget(Widget&& theWidget) noexcept;
~Widget() { delete p; }
int get() const { return *p; }
const Widget& operator=( Widget theWidget);
void swap(Widget& lhs, Widget& rhs) noexcept;
//...
private:
int *p;
};
Widget::Widget(const Widget& theWidget) : //拷贝构造函数
p(new int(theWidget.get())) {}
Widget::Widget(Widget&& theWidget) noexcept :p(theWidget.p) //移动构造函数
{
theWidget.p = nullptr;
}
void Widget::swap(Widget& lhs, Widget& rhs) noexcept
{
using std::swap; //编译器会选择最适合的版本,标准库版本不会隐藏自定义版本
swap(lhs.p, rhs.p);
}
const Widget& Widget::operator=( Widget theWidget) //根据传入的值是右值还是左值调用对应的构造函数
{
swap(*this, theWidget); //与局部变量进行交换
return *this;
}
如果同时实现了拷贝构造与移动构造函数,则拷贝与交换实现的单一赋值运算符自动实现拷贝与移动两种赋值运算符的功能。
单独实现拷贝赋值运算符如下:
Widget& Widget::operator=(const Widget &theWidget)
{
if (this != &theWidget) //检查自赋值
{
auto temp = new int(theWidget.get()); //拷贝新内存
delete p; //释放旧内存
p = temp; //完成赋值
}
return *this;
}
其中条件判断是可选的,拷贝能正确处理自赋值且异常安全。原因是,在释放旧内存前拷贝了新内容,且拷贝过程是唯一可能发生异常的地方,一旦发生异常,程序立即终断,不会执行后续的释放操作。增加条件判断的好处是减少自赋值情况下拷贝的开销。
单独实现移动赋值运算符如下;
Widget& Widget::operator=(Widget &&theWidget) noexcept
{
if (this != &theWidget) //检查自赋值
{
p = theWidget.p;
theWidget.p = nullptr; //把移后源对象置于可析构状态
}
return *this;
}
因为移动操作不分配新的内存空间,所以不会抛出异常,只需检查自赋值即可,与上文提到的swap实现相比,优势在于开销较小。
总结一下,拷贝与交换惯用法天然处理自赋值且异常安全,在需要动态分配内存的时候(需要考虑自赋值和异常)推荐使用,并且额外增加的开销在绝大多数情况下都可忽略不计。