Última atualização:2026-10-12
Idiom copy-and-swap em C++¶
Por que precisamos do idiom de copiar e trocar?
Qualquer classe que gerencie recursos (wrappers, como smart pointers) precisa implementar o big three. Embora o objetivo e a implementação do construtor de cópia e do destrutor sejam simples,
o operador de atribuição por cópia é, sem dúvida, o mais sutil e o mais difícil.
O que fazer? Quais armadilhas evitar?
copy-swap é a solução: ele ajuda muito o operador de atribuição a cumprir duas coisas: evitar duplicação de código e fornecer uma forte garantia de exceção.
Como ele funciona?
Conceitualmente, ele funciona usando a funcionalidade do construtor de cópia para criar uma cópia local dos dados e, em seguida, usando a função de troca para obter os dados copiados, trocando os dados antigos pelos novos. Depois, a cópia temporária é destruída, levando consigo os dados antigos. O que nos resta é uma cópia dos dados novos.
Para usar copy-swap, precisamos de três coisas:
- um construtor de cópia válido
- um destrutor válido (ambos são a base de qualquer wrapper e, portanto, já deveriam estar completos de qualquer forma) e uma função de troca.
A função de troca é uma função que não lança exceções e que troca dois objetos ou membros de uma classe. Ficaríamos tentados a usar std::swap em vez de fornecer o nosso próprio método, mas isso não é possível. std::swap usa em sua implementação o construtor de cópia e o operador de atribuição por cópia, e acabaríamos tentando definir o operador de atribuição em termos de si mesmo!
(E não só isso: a chamada incondicional a swap usará o nosso operador swap personalizado, pulando as construções e destruições desnecessárias de classes que std::swap causaria.)
O exemplo concreto é o seguinte:
namespace A {
template<typename T>
class smart_ptr {
public:
smart_ptr() noexcept : ptr_(new T()) {
}
smart_ptr(const T &ptr) noexcept : ptr_(new T(ptr)) {
}
smart_ptr(smart_ptr &rhs) noexcept {
ptr_ = rhs.release(); // 释放所有权,此时rhs的ptr_指针为nullptr
}
void swap(smart_ptr &rhs) noexcept { // noexcept == throw() 保证不抛出异常
using std::swap;
swap(ptr_, rhs.ptr_);
}
T *release() noexcept {
T *ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
T *get() const noexcept {
return ptr_;
}
private:
T *ptr_;
};
// 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
template<typename T>
void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
lhs.swap(rhs);
}
}
// 注释开启,会引发ADL冲突
//namespace std {
// // 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
// template<typename T>
// void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
// lhs.swap(rhs);
// }
//
//}
int main() {
using std::swap;
A::smart_ptr<std::string> s1("hello"), s2("world");
// 交换前
std::cout << *s1.get() << " " << *s2.get() << std::endl;
swap(s1, s2); // 这里swap 能够通过Koenig搜索或者说ADL根据s1与s2的命名空间来查找swap函数
// 交换后
std::cout << *s1.get() << " " << *s2.get() << std::endl;
s1=s2;
}
Agora, para que o s1=s2 acima funcione, é preciso implementar o operador de atribuição.
Método 1
Para evitar a autoatribuição, costuma-se escrever assim.
Ruim!
Não tem segurança de exceção, apenas segurança contra autoatribuição
smart_ptr &operator=(const smart_ptr &rhs) {
if (*this != rhs) {
delete ptr_;
ptr_ = new T(rhs.ptr_); // 当new 发生异常,此时ptr_指向的而是一块被删除区域,而不是被赋值对象的区域
return *this;
}
return *this;
}
Método 2
Se new lançar uma exceção, ptr_ permanece intacto! Também lida com a autoatribuição! Ainda não é bom o bastante!
Isso provoca inchaço de código e, com isso, outro problema: redundância de código
// 方法2:如果new出现异常,ptr_会保持原装! 也可以处理自我赋值! 还是不够好!
smart_ptr &operator=(const smart_ptr &rhs) {
T *origin = ptr_;
ptr_ = new T(rhs.ptr_);
delete origin;
return *this;
}
Método 3
copy and swap, muito bom!
Método 4
Mude para passar por valor; igual ao método 3!
C++11 move
Ao big three acrescentamos o move ctor e o move assignment, obtendo o big five.
Agora estendemos mais uma vez o código acima:
// move ctor
smart_ptr(smart_ptr &&rhs) noexcept {
std::cout << "move ctor" << std::endl;
ptr_ = rhs.ptr_;
if (ptr_)
rhs.ptr_ = nullptr;
}
// move assignment
smart_ptr &operator=(smart_ptr &&rhs) noexcept {
std::cout << "move assignment" << std::endl;
smart_ptr tmp(rhs);
swap(rhs);
return *this;
}
Na verdade, não precisamos escrever aquele código extra do move assignment: a técnica copy-and-swap e a técnica move-and-swap compartilham a mesma função. Quando o construtor de cópia é o do método 4 acima, no C++11, o compilador escolhe entre o construtor de cópia e o construtor de move conforme o argumento seja lvalue ou rvalue:
Assim, quando este e aquele escrito acima
existem ao mesmo tempo, ocorre error: ambiguous overload for 'operator='.
A chamada é a seguinte:
A::smart_ptr<std::string> s1("hello"), s2("world");
A::smart_ptr<std::string> s3 = s1;
A::smart_ptr<std::string> s4 = std::move(s1);
- No caso de s3 = s1, o construtor de cópia é chamado para inicializar other (porque s1 é um lvalue); o operador de atribuição troca os dados com o objeto recém-criado — uma cópia profunda. Esta é a definição do idiom copy and swap: construir uma cópia, trocar os dados com a cópia e deixar que a cópia seja destruída automaticamente ao sair de escopo.
- No caso de s4 = std::move(s1), o construtor de move é chamado para inicializar rhs (porque std::move(s1) é um rvalue); portanto, aqui não há cópia profunda, apenas uma transferência de dados eficiente.
Por isso, ele também pode ser chamado de "operador de atribuição unificado", pois funde a "atribuição por cópia" e a "atribuição por move".
Tópicos relacionados¶
- C++ evitar o construtor padrão | C++那些事
- Idiom de lista de inicialização de classes em C++ | C++那些事
- C++: templates de classe em vez de templates de função | C++那些事
- Injeção de dependência em C++ | C++那些事
- C++ enum class enumerações fortemente tipadas | C++那些事
- C++ Header-Only versus separação de implementação | C++那些事
- Idiom de avaliação preguiçosa em C++ | C++那些事
- Idiom de consulta em tabela em C++ | C++那些事
- Ponteiros para membros e type info em C++ | C++那些事
- Idiom Mixin em C++ | C++那些事