C++20使用“复制和交换”方法实现安全处理异常的赋值运算符

以下笔记来自:C++20高级编程(第5版) p231-237

前期准备

对于 C++来说,如果没有自行编写拷贝构造函数或赋值运算符,C++会自动生成。然而,对于基本类型,编译器只会提供表层复制或赋值,也就是浅拷贝。

那么对于以下的类,如果使用了编译器提供了默认的拷贝构造函数,会导致程序运行错误:

cpp 复制代码
class Sheet {
public:
	Sheet(size_t width, size_t height) 
	    : m_width{ width }, m_height{ height }
	{
		m_chart = new int* [m_width];
		for (size_t i{ 0 }; i < m_width; i++ ) {
		    m_chart[i] = new int[m_height];
		}
	}
	
	~Sheet() {
		for (size_t i{ 0 }; i < m_width; i++) {
			delete[] m_chart[i];
		}
		delete[] m_chart;
		m_chart = nullptr;
	}

private:
	size_t m_width{ 0 };
	size_t m_height{ 0 };
	
	int** m_chart;
};

可以使用以下的代码验证,可以发现程序会报错:

cpp 复制代码
int main() {
	// 构造一个变量
	Sheet a1{ 5, 7 };
	// 使用默认的拷贝构造函数完成拷贝构造
	Sheet a2{ a1 };
	return 0;
}

因此,我们需要自己写一个拷贝构造函数来完成对于类的拷贝构造(深拷贝)。

cpp 复制代码
#include <iostream>

using namespace std;

class Sheet {
public:
	Sheet(size_t width, size_t height) 
		: m_width{ width }, m_height{ height }
	{
		m_chart = new int* [m_width];
		for (size_t i{ 0 }; i < m_width; i++ ) {
			m_chart[i] = new int[m_height];
		}
	}
	
	~Sheet() {
		for (size_t i{ 0 }; i < m_width; i++) {
			delete[] m_chart[i];
		}
		delete[] m_chart;
		m_chart = nullptr;
	}
	//    ADD   ///
	Sheet(const Sheet& src) 
		: Sheet{src.m_width, src.m_height}
	{
		for (size_t i{ 0 }; i < m_width; i++) {
			for (size_t j{ 0 }; j < m_height; j++) {
				m_chart[i][j] = src.m_chart[i][j];
			}
		}
	}
	///

private:
	size_t m_width{ 0 };
	size_t m_height{ 0 };
	
	int** m_chart;
	};

再次执行上面的代码,可以发现程序不会报错了,因为通过深拷贝,可以正确的分配内存了。

赋值运算符

那么,如果要实现赋值运算符,应该怎么实现?

首先,来看以下的实现方式:

cpp 复制代码
Sheet& operator=(const Sheet& rhs) {
	// 检查是不是自赋值
	if (this == &rhs) {
		return *this;
	}
	// 释放旧内存
	for (size_t i{ 0 }; i < m_width; i++) {
		delete[] m_chart[i];
	}
	delete[] m_chart;
	m_chart = nullptr;
	// 分配新内存
	m_width = rhs.m_width;
	m_height = rhs.m_height;
	
	m_chart = new int* [m_width];
	for (size_t i{ 0 }; i < m_width; i++) {
		m_chart[i] = new int[m_height];
	}
	// 复制数据
	for (size_t i{ 0 }; i < m_width; i++) {
		for (size_t j{ 0 }; j < m_height; j++) {
			m_chart[i][j] = rhs.m_chart[i][j];
		}
	}
	return *this;
}

该函数做了以下的事情:

  1. 检查自赋值
  2. 释放当前使用的内存
  3. 分配新的内存
  4. 复制各个内存

我在第一眼看的时候并没有发现什么错,不过经过书中的指点,我才明白:该代码存在以下的漏洞:

如果该代码成功的释放了内存,合理的设置了 m_width 与 m_height,但是分配内存的循环抛出了异常。如果出现了这种情况,则将不再执行该方法的剩余部分,而是直接从该方法中退出。此时 Sheet 实例受损,它的 m_width 与 m_height 数据成员声明了指定的大小,但 m_chart 数据成员不指向正确数量的内存。所以,该代码不能安全的处理异常!

所以,我们需要一种全有或全无的机制:要么全部成功,要么该对象保持不变。如果要实现一个能安全处理异常的赋值运算符,则要使用 复制和交换 的方法:

什么是复制和交换方法

该方法共有以下几步:

  1. 给目标类添加一个 swap() 方法(推荐提供一个非成员函数的版本,这样各种标准库算法都可以使用它了)
  2. 使用复制和变换的惯用方法
    1. 创建一个右边的副本
    2. 用当前对象与这个副本交换

代码演示

以下是代码演示:

首先,先写一个 swap 函数,成员函数版与非成员函数版。

cpp 复制代码
class Sheet {
public:

	// 之前的代码省略
	
	// 成员函数版
	void swap(Sheet& other) noexcept {
		std::swap(m_width, other.m_width);
		std::swap(m_height, other.m_height);
		std::swap(m_chart, other.m_chart);
	}

private:
	// 数据成员省略
};

// 再提供一个非成员函数版
void swap(Sheet& first, Sheet& second) noexcept {
	first.swap(second);
}

然后使用复制与交换的惯用方法,完成赋值运算符。

cpp 复制代码
Sheet& operator=(const Sheet& rhs) {
	Sheet temp{ rhs };
	swap(temp);
	return *this;
}

复制与交换方法通过 3 个阶段来实现:

  1. 第一阶段创建一个临时副本。这不修改当前 Sheet 对象的状态,因此,如果在这个阶段上发生异常,不会出现问题。
  2. 第二阶段使用 swap() 函数,将创建的临时副本与当前对象交换。swap() 永远不会抛出异常。
  3. 第三阶段销毁临时对象(由于发生了交换,temp 为原始对象)以清理内存。

复制和交换方法的好处

  • 使用了复制和交换惯用方法的情况下,不再需要自我赋值的检查了。
  • 可以避免代码重复,又可以保证强大的异常安全性
  • 复制和交换惯用方法不仅仅适用于赋值运算符。它可以用于任何需要多个步骤的操作,并且你希望将其转换为全有或全无的操作。

完整的代码

cpp 复制代码
#include <iostream>

using namespace std;

class Sheet {
public:
	Sheet(size_t width, size_t height) 
		: m_width{ width }, m_height{ height }
	{
		m_chart = new int* [m_width];
		for (size_t i{ 0 }; i < m_width; i++ ) {
			m_chart[i] = new int[m_height];
		}
	}
	
	~Sheet() {
		for (size_t i{ 0 }; i < m_width; i++) {
			delete[] m_chart[i];
		}
		delete[] m_chart;
		m_chart = nullptr;
	}
	
	Sheet(const Sheet& src) 
		: Sheet{src.m_width, src.m_height}
	{
		for (size_t i{ 0 }; i < m_width; i++) {
			for (size_t j{ 0 }; j < m_height; j++) {
				m_chart[i][j] = src.m_chart[i][j];
			}
		}
	}
	
	void swap(Sheet& other) noexcept {
		std::swap(m_width, other.m_width);
		std::swap(m_height, other.m_height);
		std::swap(m_chart, other.m_chart);
	}
	
	Sheet& operator=(const Sheet& rhs) {
		Sheet temp{ rhs };
		swap(temp);
		return *this;
	}

private:
	size_t m_width{ 0 };
	size_t m_height{ 0 };
	
	int** m_chart;
};

void swap(Sheet& first, Sheet& second) noexcept {
	first.swap(second);
}

int main() {
	Sheet a1{ 5, 7 };
	Sheet a2{ 2, 4 };
	
	a2 = a1;
	
	return 0;
}
相关推荐
东风吹柳18 分钟前
观察者模式(sigslot in C++)
c++·观察者模式·信号槽·sigslot
A懿轩A27 分钟前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
汪洪墩28 分钟前
【Mars3d】设置backgroundImage、map.scene.skyBox、backgroundImage来回切换
开发语言·javascript·python·ecmascript·webgl·cesium
云空34 分钟前
《QT 5.14.1 搭建 opencv 环境全攻略》
开发语言·qt·opencv
Anna。。36 分钟前
Java入门2-idea 第五章:IO流(java.io包中)
java·开发语言·intellij-idea
我曾经是个程序员1 小时前
鸿蒙学习记录
开发语言·前端·javascript
爱上语文1 小时前
宠物管理系统:Dao层
java·开发语言·宠物
大胆飞猪2 小时前
C++9--前置++和后置++重载,const,日期类的实现(对前几篇知识点的应用)
c++
1 9 J2 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
夕泠爱吃糖2 小时前
C++中如何实现序列化和反序列化?
服务器·数据库·c++