【25】考虑写出一个不抛异常的swap函数

探讨了C++中std::swap的局限性及其在资源管理类中的应用,介绍了如何通过全特化方法解决资源管理类Widget的swap问题,确保异常安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、swap交换对象值,std提供了实现方法模版的实现,它的实现是交换对象值。

 1 namespace std 
 2 {
 3  template<typename T>
 4  void swap(T& a,T& b)
 5  {
 6   T temp(a);
 7   a = b;
 8   b = a;
 9  }
10 }

2、考虑下面的情况下,资源管理类Widget,内部只有一个指针WidgetImpl,该指针指向一个对象,这个对象中包含真正数据。对这种资源管理类执行swap,如果调用std中的swap,有哪些动作?对于Widget,copy构造一次,copy赋值两次,同时对于WidgetImpl,copy构造一次,copy赋值两次。我们知道,对于这种情况,只需要交换WidgetImpl就可以了。因此,std中的swap不能满足需求。

3、怎么解决呢?

  std中的swap是个模版方法,不能满足需求,因此我们要全特化一个方法。在方法中,交换WidgetImpl。因为,WidgetImpl是private,不能在外部访问。我们可以在Widget中访问,让Widget暴露一个成员方法,让全特化的swap调用成员方法。

 1 class Widget
 2 {
 3 public:
 4  void swap(Widget& rhs)
 5  {
 6   using namespace std::swap;
 7   swap(this->_widgetImpl,rhs._widgetImpl);
 8  }
 9 };
10 
11 namespace WidgetStuff 
12 {
13  template<>
14  void swap<Widget>(Widget& a,Widget& b)
15  {
16   a.swap(b);
17  }
18 }

注意:可以全特化std中的东西,但是不要在std中添加东西,因此全特化的方法,放在WidgetStuff命名空间内。

4、全特化方法是模版方法的一个特例,从范围角度看,模版方法>全特化方法,那么问题来,对于Widget的交换方法swap,模版方法和全特化方法都满足,那么是调用全特化方法,还是使用模版方法实例化一个呢?

  对于C++的名称查找法则,可以这样认为,先在小范围里找,小范围里没有,再到大范围找。

5、目前C++只允许对类模版偏特化(指定部分模版形参),不允许对方法模版偏特化,但是可以重载。

6、假如在模版方法中调用swap呢?

  根据上面的查找规则,先在小范围里找,小范围里找不到,再到大范围里找,但是这里要暴露大范围里的swap,使用using std::swap;

7、为了异常安全性,成员方法swap绝不抛出异常。非成员方法swap允许抛出异常,因为copy构造和copy赋值都可能导致异常。

 

<think>我们需要为`ImageDataSave`类编写一个拷贝赋值运算符,以实现深拷贝。由于类中包含`std::unique_ptr`成员,编译器默认生成的拷贝赋值运算符会被删除(因为`std::unique_ptr`可拷贝)。因此,我们必须自己实现。 拷贝赋值运算符需要处理以下几点: 1. 自赋值检查:避免将自己赋值给自己而导致资源错误释放。 2. 释放当前对象持有的资源(由`std::unique_ptr`自动管理,但我们需要重新分配内存)。 3. 复制基本类型成员(channel, width, height, step)。 4. 为新数据分配内存并复制源对象的数据。 然而,由于`std::unique_ptr`在赋值时自动释放原有内存,我们需要显式释放,但需要重新分配内存并复制数据。 实现方式有两种: a) 拷贝并交换(copy-and-swap)惯用法:通过拷贝构造函数创建一个临时对象,然后交换成员。 b) 直接实现:检查自赋值后,分配新内存并复制数据。 这里我们采用直接实现,因为拷贝并交换需要额外的临时对象,可能效率稍低,但更安全。过,为了清晰和异常安全,我们也可以考虑使用拷贝并交换。但题目要求写拷贝赋值,我们提供两种方式供参考。 方式一:直接实现 注意:直接实现需要确保在复制过程中异常安全(如果内存分配失败,应保持对象变)。 我们可以先分配新内存,然后再修改成员变量,这样在分配失败时(异常会破坏当前对象。 步骤: 1. 检查自赋值。 2. 分配新内存(使用智能指针的reset,并捕获异常)。 3. 复制基本类型成员。 4. 复制数据。 方式二:使用拷贝并交换(更推荐,因为异常安全且代码简洁) 这里我们展示两种方式,但更推荐方式二(拷贝并交换),因为它利用已有的拷贝构造函数和析构函数,更安全。 由于我们之前已经实现了拷贝构造函数,因此可以很方便地使用拷贝并交换。 我们先实现方式二(拷贝并交换): 需要先实现一个swap成员函数或友元函数,以支持交换操作。 但是,为了简化,我们可以在赋值运算符内部使用拷贝构造函数创建临时对象,然后交换。 具体实现: 拷贝赋值运算符(拷贝并交换): ImageDataSave& operator=(const ImageDataSave& other) { if (this != &other) { ImageDataSave temp(other); // 调用拷贝构造函数 swap(*this, temp); } return *this; } 同时,我们需要一个swap函数: void swap(ImageDataSave& a, ImageDataSave& b) noexcept { using std::swap; swap(a.channel, b.channel); swap(a.width, b.width); swap(a.height, b.height); swap(a.step, b.step); swap(a.data, b.data); } 注意:由于`std::unique_ptr`支持swap,所以我们可以直接交换。 但是,更常见的做法是提供一个成员函数swap,然后通过非成员函数swap来调用它(以便与标准库兼容)。 这里我们提供一个非成员swap函数(作为友元或独立函数)。由于类中的数据成员都是public,我们可以直接实现非成员swap过,为了封装性,我们可以在类内提供swap成员函数,然后定义一个非成员swap。 但为了简单,且类成员都是public,我们可以直接定义非成员swap。 方式一(直接实现)的代码: 我们也可以提供直接实现,但要注意异常安全: ImageDataSave& operator=(const ImageDataSave& other) { if (this != &other) { // 先分配新内存 std::unique_ptr<char[]> newData; if (other.data) { newData = std::make_unique<char[]>(other.step * other.height); std::memcpy(newData.get(), other.data.get(), other.step * other.height); } // 如果上面没有异常,则开始替换 channel = other.channel; width = other.width; height = other.height; step = other.step; data = std::move(newData); // 释放旧内存,替换为新内存 } return *this; } 这种方式在分配内存和复制数据时,如果发生异常会影响当前对象(因为newData是局部变量,异常时析构,而当前对象的数据成员还没有改变)。当没有异常时,再将新数据移动赋值给data,并更新其他成员。 两种方式都可以,这里我们选择方式二(拷贝并交换)因为更简洁且安全。 但是,注意:拷贝并交换可能会引入额外的临时对象构造和析构开销(对于大内存对象,交换指针很快,但临时对象的构造和析构会有一次内存分配和释放,而直接赋值也是需要一次分配和释放,所以开销类似)。过,拷贝并交换的代码更健壮。 下面我们给出完整的拷贝赋值运算符(采用拷贝并交换)以及swap函数: 注意:为了使swap更高效,我们将其定义为非成员函数,并利用std::swap来交换每个成员。 步骤: 1. 在类外部定义swap函数(或者作为友元函数)。 2. 在赋值运算符中使用拷贝构造函数创建临时对象,然后调用swap交换。 但是,我们也可以将swap函数定义为类的友元函数,以便在非成员函数中访问私有成员(过这个类的成员都是public,所以需要友元)。 实现: 首先,在类外部定义swap函数(如果类成员变为private,则需要定义为友元): ```cpp #include <algorithm> // for std::swap void swap(ImageDataSave& a, ImageDataSave& b) noexcept { using std::swap; swap(a.channel, b.channel); swap(a.width, b.width); swap(a.height, b.height); swap(a.step, b.step); swap(a.data, b.data); } ``` 然后,在类中实现赋值运算符: ```cpp class ImageDataSave { public: // ... 其他成员 // 拷贝赋值运算符(使用拷贝并交换) ImageDataSave& operator=(const ImageDataSave& other) { if (this != &other) { // 通过拷贝构造函数创建临时对象 ImageDataSave temp(other); // 交换当前对象和临时对象的内容 swap(*this, temp); } // 临时对象temp(原内容)被析构 return *this; } }; ``` 注意:swap函数必须在类定义之后声明,否则编译器在类内使用swap时找到。 另一种做法是将swap函数放在类内作为友元,这样可以直接在类内声明并定义: ```cpp class ImageDataSave { public: // ... 其他成员 friend void swap(ImageDataSave& a, ImageDataSave& b) noexcept { using std::swap; swap(a.channel, b.channel); swap(a.width, b.width); swap(a.height, b.height); swap(a.step, b.step); swap(a.data, b.data); } // 拷贝赋值运算符 ImageDataSave& operator=(const ImageDataSave& other) { if (this != &other) { ImageDataSave temp(other); swap(*this, temp); } return *this; } }; ``` 这样,swap函数是内联的,且只有该类使用。 完整代码: ```cpp #include <memory> #include <cstring> #include <algorithm> // 实际上在函数内部使用std::swap,需要包含<algorithm>或<utility> class ImageDataSave { public: int channel; int width; int height; int step; std::unique_ptr<char[]> data; // 构造函数 ImageDataSave(int ch, int w, int h, int s, const char* src) : channel(ch), width(w), height(h), step(s), data(new char[s * h]) { std::memcpy(data.get(), src, s * height); } // 拷贝构造函数 ImageDataSave(const ImageDataSave& other) : channel(other.channel), width(other.width), height(other.height), step(other.step), data(other.data ? std::make_unique<char[]>(other.step * other.height) : nullptr) { if (other.data) { std::memcpy(data.get(), other.data.get(), other.step * other.height); } } // 友元swap函数 friend void swap(ImageDataSave& a, ImageDataSave& b) noexcept { using std::swap; swap(a.channel, b.channel); swap(a.width, b.width); swap(a.height, b.height); swap(a.step, b.step); swap(a.data, b.data); } // 拷贝赋值运算符 ImageDataSave& operator=(const ImageDataSave& other) { if (this != &other) { ImageDataSave temp(other); swap(*this, temp); } return *this; } }; ``` 注意:在C++11以后,还可以通过移动赋值运算符来实现拷贝赋值,但这里我们按照要求只实现拷贝赋值。 另外,我们也可以为移动语义提供支持(移动构造函数和移动赋值运算符),但题目没有要求。 测试示例: ```cpp int main() { char rawData[100] = {0}; ImageDataSave img1(3, 10, 10, 10, rawData); ImageDataSave img2(1, 5, 5, 5, nullptr); img2 = img1; // 调用拷贝赋值运算符 // 验证深拷贝 assert(img2.data != nullptr); assert(img2.data != img1.data); // 指针同 assert(img2.channel == img1.channel); // 其他成员同理 } ``` 总结:拷贝赋值运算符通过拷贝并交换技术实现,确保了异常安全和代码简洁。 --- §§相关问题§§: 1. 为什么拷贝赋值运算符需要检查自赋值? 2. 拷贝并交换(copy-and-swap)惯用法是如何保证异常安全的? 3. 在C++中,为什么`std::unique_ptr`成员会导致默认的拷贝赋值运算符被删除? 4. 移动赋值运算符应该如何实现?与拷贝赋值运算符有何同?</think>### 拷贝赋值运算符实现(深拷贝) 以下是 `ImageDataSave` 类的完整拷贝赋值运算符实现: ```cpp #include <memory> #include <cstring> #include <algorithm> class ImageDataSave { public: int channel; int width; int height; int step; std::unique_ptr<char[]> data; // 拷贝赋值运算符(深拷贝) ImageDataSave& operator=(const ImageDataSave& other) { if (this != &other) { // 1. 检查自赋值 // 2. 分配新内存(如果源对象有数据) std::unique_ptr<char[]> newData; if (other.data) { newData = std::make_unique<char[]>(other.step * other.height); std::memcpy(newData.get(), other.data.get(), other.step * other.height); } // 3. 更新成员(异常安全) channel = other.channel; width = other.width; height = other.height; step = other.step; // 4. 交换数据所有权(原子操作,无异常) data.swap(newData); } return *this; } // 原始构造函数 ImageDataSave(int ch, int w, int h, int s, const char* src) : channel(ch), width(w), height(h), step(s), data(new char[s * h]) { std::memcpy(data.get(), src, s * height); } // 拷贝构造函数 ImageDataSave(const ImageDataSave& other) : channel(other.channel), width(other.width), height(other.height), step(other.step), data(other.data ? std::make_unique<char[]>(other.step * other.height) : nullptr) { if (data) { std::memcpy(data.get(), other.data.get(), other.step * other.height); } } }; ``` ### 关键实现原理 1. **自赋值检查**: ```cpp if (this != &other) ``` - 防止 `x = x` 导致的内存提前释放 - 避免必要的内存分配和拷贝操作 2. **异常安全的内存分配**: ```cpp std::unique_ptr<char[]> newData; if (other.data) { newData = std::make_unique<char[]>(other.step * other.height); // ... } ``` - 使用临时智能指针 `newData` 分配新内存 - 仅在内存分配成功后执行内存拷贝 - 确保原始数据受影响(强异常安全保证) 3. **基本成员更新**: ```cpp channel = other.channel; width = other.width; height = other.height; step = other.step; ``` - 在安全更新数据指针前复制简单类型成员 - 这些操作异常 4. **原子指针交换**: ```cpp data.swap(newData); ``` - 使用 `swap()` 原子操作转移所有权 - 异常(保证操作完整性) - 旧数据由 `newData` 自动释放(离开作用域时) ### 使用示例 ```cpp int main() { char rawData[100] = {0}; ImageDataSave img1(3, 10, 10, 10, rawData); ImageDataSave img2(1, 5, 5, 5, nullptr); // 拷贝赋值 img2 = img1; // 验证深拷贝 assert(img2.data != nullptr); assert(img2.data != img1.data); // 指针同 assert(img2.channel == 3); assert(img2.width == 10); // 自赋值测试 img2 = img2; // 安全操作 assert(img2.data != nullptr); } ``` ### 异常安全分析 1. **内存分配失败**: - `std::make_unique` 出 `std::bad_alloc` - 类成员保持变(原始数据受影响) - 满足强异常安全保证 2. **数据拷贝失败**: - `std::memcpy` 异常 - 但若使用自定义拷贝可能出,此时需额外处理 3. **成员更新**: - 基本类型赋值异常 - 指针交换异常 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值