文章目录
- C++ 自定义 swap 函数的实现指南
-
- 不同情况下的实现方法
- 为什么需要自定义 swap?
- 如何为不同情况实现自定义 swap
-
- 1. 对于普通类(非模板类)
- 2. 对于类模板
- 如何正确调用 swap
- 核心要点总结
请记住: 1. 当 std::swap 对你的类型效率不高时候,提供一个 swap 成员函数, 并确定这个函数不抛出异常。 2. 如果你提供一个 member swap,也该提供一个 non-member swap用来调用前者。对于 classes(而非templates), 也请特化 std::swap 3. 调用 swap 时应针对 std::swap 使用 using 声明式, 然后调用 swap 并且不带任何“命名空间修饰词” 4. 为“用户定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西
C++ 自定义 swap 函数的实现指南
简单来说,当标准库的 std::swap 对于你的类来说效率低下时,你就应该考虑自己实现一套 swap 机制。
不同情况下的实现方法
| 普通类(非模板类) | 提供成员函数 swap+ 特化 std::swap | 1. 在类内定义公有 swap 成员函数。2. 为 std::swap 提供全特化版本,调用成员函数。 |
| 类模板 | 提供成员函数 swap+ 在同名空间定义非成员 swap | 1. 在类模板内定义公有 swap 成员函数。2. 在类模板所在的命名空间内,定义同名的非成员 swap 函数模板。 |
| 通用代码中调用 | 使用 using std::swap;+ 无限制调用 | 1. 使用 using std::swap; 将标准 swap 引入当前作用域。2. 直接调用 swap(a, b),让编译器自动选择最佳版本。 |
为什么需要自定义 swap?
标准库的 std::swap 默认通过拷贝(或移动)构造和赋值来实现交换。对于某些管理大量资源的类(例如使用 pimpl 手法的类),这种默认交换效率极低。
考虑以下类:
class WidgetImpl { // 包含大量数据
private:
int a, b, c;
std::vector<double> v;
// … 更多数据
};
class Widget { // 使用 pimpl 手法
private:
WidgetImpl* pImpl; // 指针,指向包含真实数据的对象
};
交换两个 Widget 对象,本质上只需要交换它们内部的 pImpl 指针。但默认的 std::swap 会复制整个 Widget 对象,进而复制整个 WidgetImpl 对象,这是非常大的性能浪费。因此,我们需要一个定制的、高效的 swap。
如何为不同情况实现自定义 swap
1. 对于普通类(非模板类)
第一步:在类内提供一个公有的 swap 成员函数 这个函数负责高效地交换对象的内部资源,并且应声明为 noexcept,因为它通常只操作内置类型(如指针)。
class Widget {
public:
void swap(Widget& other) noexcept {
using std::swap; // 允许ADL查找基础类型的swap
swap(pImpl, other.pImpl); // 仅交换指针
}
private:
WidgetImpl* pImpl;
};
第二步:在 std 命名空间中特化 std::swap C++允许我们为自定义类型全特化标准模板。这个特化版本直接调用我们刚刚写的成员函数。
namespace std {
template<> // 全特化标志
void swap<Widget>(Widget& a, Widget& b) noexcept {
a.swap(b);
}
}
这样,当代码中调用 std::swap 交换两个 Widget 对象时,就会使用我们这个高效的特定版本。
2. 对于类模板
问题来了:C++不允许对函数模板进行偏特化。因此,我们不能用上述方法为类模板(如 Widget<T>)特化 std::swap。
解决方案是:
第一步:同样在类模板内提供公有 swap 成员函数。
template<typename T>
class Widget {
public:
void swap(Widget& other) noexcept {
using std::swap;
swap(pImpl, other.pImpl);
}
};
第二步:在类模板所在的命名空间内,定义一个非成员的 swap 函数模板。
namespace WidgetStuff { // 你的类所在的命名空间
template<typename T>
class Widget { ... };
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) noexcept { // 非成员函数
a.swap(b);
}
}
这个非成员函数可以通过参数依赖查找(ADL)被编译器找到。
为什么不重载 std::swap? C++标准禁止向 std 命名空间添加全新的模板(即重载),只允许对现有模板进行特化。因此,将自定义的 swap 放在你自己的命名空间是正确的做法。
如何正确调用 swap
在编写通用模板代码时,你无法预知类型 T 是否有自定义的 swap。为了能智能地调用最合适的版本,需要遵循一个固定模式:
template<typename T>
void doSomething(T& obj1, T& obj2) {
using std::swap; // 1. 将 std::swap 引入当前作用域
// …
swap(obj1, obj2); // 2. 不加任何命名空间限制地调用 swap
// …
}
编译器看到 swap(obj1, obj2) 时,会按照以下顺序查找:
切记不要这样写:std::swap(obj1, obj2); 这会让编译器只在 std 命名空间中查找,从而可能错过你写的、更高效的专属 swap 版本。
核心要点总结
- 动机:当默认 std::swap 效率低下时,为你的类定制 swap。
- 实现核心:提供一个高效、不抛异常的 swap 成员函数作为基础。
- 普通类:额外特化 std::swap 来调用成员函数。
- 类模板:额外在类所在命名空间提供非成员 swap 函数模板来调用成员函数。
- 调用法则:在通用代码中,先 using std::swap;,然后无限制地调用 swap(…),让编译器为你选择最佳版本。



