C++_Bug:现代写法拷贝构造中 swap 写法之小坑
文章又名:深入剖析HashTable赋值拷贝中std::swap的那些坑
案发背景
bear实现了链地址法的HashTable,最后在补充析构函数、赋值拷贝和拷贝构造这里。博主果断采用swap现代写法——
namespace hash_bucket
{
template<class K,class V,class HashFunc=hashfunc<K>>class HashTable{public:HashTable():_table(11),_n(0){}~HashTable(){for (int i = 0; i < _table.size(); i++){HashData<K, V>* head = _table[i];while (head){HashData<K, V>* next = head->_next;delete head;head = next;}_table[i] = nullptr;}_n = 0;}HashTable(const HashTable& ht):_table(11), _n(0){// 拷贝for (int i = 0; i < ht._table.size(); i++){HashData<K, V>* head = ht._table[i];while (head){HashData<K, V>* next = head->_next;Insert(head->_kv);head = next;}}_n = ht._n;}HashTable& operator=(HashTable ht){std::swap(ht,*this);return *this;}
//... 以下为增删查改具体代码 (忽略)
}一旦实现了拷贝构造,我就可以在赋值拷贝里使用转移大法:
HashTable& operator=(HashTable ht){std::swap(ht,*this);return *this;}案发现场
测试代码:
/** 测试4:析构、赋值重载和拷贝构造*
*/
namespace hash_bucket
{void test4(){HashTable<int, int> ht;int a[] = { 19,30,5,36,13,20,21,12 };for (auto e : a){ht.Insert({ e,e });}HashTable<int, int> ht3;int a2[] = { 18, 25, 36, 49, 5, 13, 28, 7, 31, 17, 42, 55, 11, 22 };for (auto e : a2){ht3.Insert({ e,e });}HashTable<int, int> ht2 = ht;//拷贝构造ht2.Print(); cout << "----------ht2--------------" << endl;ht3.Print(); cout << "----------ht3--------------" << endl;ht3 = ht2;// 赋值拷贝ht3.Print(); cout << "----------ht3--------------" << endl;}
}
int main()
{hash_bucket::test4();return 0;
}运行截图:
真相是什么
很明显,赋值拷贝的代码出了问题。
可是调用了库里的swap怎么还能出错呢?
分析
std::swap 对自定义类型的默认实现本质是 “三次赋值”:
template<class T>
void swap(T& a, T& b) {T temp(a); // 1. 用a拷贝构造tempa = b; // 2. 用b赋值给ab = temp; // 3. 用temp赋值给b
}当你在赋值运算符中调用 std::swap(ht, *this) 时,相当于:
HashTable& operator=(HashTable ht) {// 此时ht是实参的拷贝(已通过你的拷贝构造完成深拷贝)HashTable temp(ht); // 用ht拷贝构造temp(再次深拷贝)ht = *this; // 用*this赋值给ht(调用当前operator=,导致递归!)*this = temp; // 用temp赋值给*thisreturn *this;
}是的,这里会导致递归。而且这里还多进行了一次深拷贝:
多做一次深拷贝(temp 的创建),我们的需求只是 “交换资源”,无需额外拷贝。
解决
HashTable& operator=(HashTable ht){std::swap(ht._table,_table);std::swap(ht._n, _n);return *this;}运行截图:
为什么显式交换成员变量没问题?
HashTable& operator=(HashTable ht) {std::swap(ht._table, _table); // 直接交换资源容器(指针/vector)std::swap(ht._n, _n); // 交换元素个数return *this;
}- 没有触发
std::swap对整个对象的默认三次赋值,因此不会递归调用operator=。 - 仅交换资源的 “所有权”(
_table存储的指针 / 链表节点的归属权从ht转移到*this),无需额外深拷贝,效率更高。 - 依赖
ht的析构函数释放*this原来的旧资源(因为ht是局部变量,离开作用域时会自动析构),逻辑清晰且安全。
提问:std::swap(ht._table,_table);在swap里,_table和ht._table交换过程中还不是会拷贝出来一个temp;这和我原来的写法有什么区别
明确
std::swap(ht._table, _table)中temp拷贝的是什么
std::swap(ht._table, _table);
// 针对vector的swap实现(简化版)
void swap(vector<HashData*>& a, vector<HashData*>& b) {vector<HashData*> temp(a); // 用a拷贝构造temp(拷贝的是vector容器本身)a = b; // 把b的vector内容赋给ab = temp; // 把temp的内容赋给b
}这里的 temp 拷贝的是 vector 容器本身(容器内的指针会被复制,但指针指向的链表节点内存不会被复制)。
原来的
std::swap(ht, *this)中temp拷贝的是什么
void swap(HashTable& a, HashTable& b) {HashTable temp(a); // 用a拷贝构造temp(调用深拷贝构造函数)a = b; // 调用operator=,导致递归b = temp; // 再次调用operator=
}这里的 temp 拷贝的是 整个 HashTable 对象,包括:
- 调用深拷贝构造函数,为
temp重新分配所有链表节点内存(完整复制_table中的每个节点,成本极高); - 同时复制
_n等其他成员。
总结:swap(成员) 是 “轻量转移”,swap(对象) 是 “重量级灾难”
std::swap(ht._table, _table)中,temp拷贝的是vector容器(轻量操作,不复制节点内存),且不涉及operator=调用,因此高效且安全;- 原来的
std::swap(ht, *this)中,temp拷贝的是整个对象(触发深拷贝,复制所有节点),且强制调用operator=导致递归,因此既低效又危险。
也算是查漏补缺,swap写法这个问题我一直没在意,今天就出现了。
不管了,感谢遇见(冷脸)
