关于Copy-and-Swap算法在移动赋值场景中的疑问及函数歧义问题咨询
你提的这个问题真的说到点子上了——我当初刚摸透copy-and-swap模式的时候,也对着这个点纠结了好久,总觉得哪里不对劲!
先帮你理清第一个误区:你担心copy-and-swap用于移动赋值时会“浪费地复制内容”,其实这个担心是多余的。当你给传值参数的赋值运算符传入右值时,编译器会用移动构造而非复制构造来创建那个临时的other对象,而不是你以为的复制操作。之后的swap只是交换两个对象的内部资源指针(或者说资源句柄),根本不会涉及到资源的复制,开销其实非常小,甚至很多场景下比你手写移动赋值的开销大不了多少,还顺带保留了copy-and-swap最核心的优势——异常安全。
然后说说你遇到的“函数歧义”问题:当你同时提供了传值参数的赋值运算符(copy-and-swap版本)和显式的右值引用参数的移动赋值运算符时,编译器在处理右值赋值(比如obj = std::move(another_obj))时会犯难——这两个重载都能匹配这个操作:
- 传值版本可以通过移动构造临时对象来适配右值;
- 移动赋值版本则直接绑定右值引用。
这两个匹配的优先级是完全相同的,编译器没办法判断你想用哪个,所以就会抛出歧义错误。
那该怎么解决这个问题?给你两个方向选:
方向一:坚持用copy-and-swap,放弃单独写移动赋值
这其实是大部分C++开发者的首选,因为它能让你的代码极度简洁,还自带异常安全保障。而且如我刚才所说,它处理移动赋值的开销并没有你想的那么大——移动构造+swap的组合,在资源是指针/句柄的类里,几乎是零额外开销。比如下面这个典型的copy-and-swap实现:class MyClass { public: // 省略拷贝构造、移动构造、析构、swap函数的实现 MyClass& operator=(MyClass other) { swap(*this, other); return *this; } };当你执行
obj = std::move(another_obj)时,other是通过移动构造出来的,swap只是交换内部资源,完全没有复制操作。方向二:分开实现拷贝赋值和移动赋值,放弃copy-and-swap的传值参数
如果你确实追求极致的性能(比如你的类的移动构造开销还是不可忽略),那可以把copy-and-swap的赋值运算符改成const左值引用传参,自己实现拷贝赋值的逻辑,再单独写移动赋值。这样两个重载的参数类型明确区分,就不会有歧义了。比如:class MyClass { public: // 拷贝赋值:const左值引用传参 MyClass& operator=(const MyClass& other) { if (this != &other) { // 手动实现拷贝资源的逻辑 } return *this; } // 移动赋值:右值引用传参 MyClass& operator=(MyClass&& other) noexcept { if (this != &other) { // 手动实现移动资源的逻辑 } return *this; } };但这种方式的缺点也很明显:你要自己写两份赋值逻辑,代码量增加,还得手动保证异常安全,失去了copy-and-swap的最大优势。
总的来说,除非你有非常明确的性能测试证明copy-and-swap的移动处理开销不可接受,否则我强烈推荐你保留copy-and-swap的传值赋值运算符,不需要单独写移动赋值——它已经能完美处理左值和右值的赋值场景,还能帮你少写很多代码。
内容来源于stack exchange

