Copy And Swap
Идиома "Copy And Swap" пользовалась большой популярностью до выхода C++11. В чем ее суть?
Рассмотрим некий класс, владеющий ресурсом (в нашем случае нужно выделять память). С использованием стандарта С++98/03, а также с учетом правила трех, его код мог бы выглядеть следующим образом:
// myclass.h
#pragma once
class myclass {
public:
myclass();
myclass(const myclass& rhs);
myclass& operator=(const myclass& rhs);
~myclass();
private:
char* buffer;
};
// myclass.cpp
#include "myclass.h"
#include <cstring>
#include <utility>
myclass::myclass() : buffer(0) {
}
myclass::myclass(const myclass& rhs)
{
char* tmp = 0;
if (rhs.buffer) {
size_t len = std::strlen(rhs.buffer);
tmp = new char[len + 1];
std::memcpy(tmp, rhs.buffer, len + 1);
}
buffer = tmp;
}
myclass& myclass::operator=(const myclass& rhs) {
if (this != &rhs) {
char* tmp = 0;
if (rhs.buffer) {
size_t len = std::strlen(rhs.buffer);
tmp = new char[len + 1];
std::memcpy(tmp, rhs.buffer, len + 1);
}
delete[] buffer;
buffer = tmp;
}
return *this;
}
myclass::~myclass() {
delete[] buffer;
}
Заметьте, в этом коде соблюдена строгая гарантия безопасности исключений: если при копировании что-то пойдёт не так, исходный объект останется нетронутым.
Но дублирование кода выглядит печально. Логика копирования повторяется и в конструкторе копирования, и в операторе присваивания. Можно, конечно, вынести её в отдельную вспомогательную функцию и вызывать оттуда.
Однако ещё в конце 90-х был придуман более элегантный подход, который не только убирает дублирование, но и автоматически обеспечивает строгую гарантию безопасности исключений. В класс добавляется функция swap, которая быстро и дёшево обменивает значения членов класса. Стандартная функция std::swap из-за копирования здесь неприменима из-за возникающей рекурсии.
Кроме того добавляется еще свободная функция swap, объявленная в том же пространстве имён, что и класс. Благодаря механизму ADL (поиск имён по типам аргументов) именно эта эффективная версия будет вызываться для нашего класса вместо медленного std::swap (до C++11 выполняла три операции копирования, вместо дешевого перемещения).
// myclass.h
#pragma once
class myclass {
public:
myclass();
myclass(const myclass& rhs);
myclass& operator=(const myclass& rhs);
~myclass();
void swap(myclass& other);
private:
char* buffer;
};
void swap(myclass& a, myclass& b);
// myclass.cpp
#include "myclass.h"
#include <cstring>
#include <utility>
myclass::myclass() : buffer(0) {
}
myclass::myclass(const myclass& rhs) : buffer(0)
{
if (rhs.buffer) {
size_t len = std::strlen(rhs.buffer);
buffer = new char[len + 1];
std::memcpy(buffer, rhs.buffer, len + 1);
}
}
myclass& myclass::operator=(const myclass& rhs) {
myclass tmp(rhs);
swap(tmp);
return *this;
}
void myclass::swap(myclass& other) {
char* tmp = buffer;
buffer = other.buffer;
other.buffer = tmp;
}
myclass::~myclass() {
delete[] buffer;
}
void swap(myclass& a, myclass& b) {
a.swap(b);
}
Эта идиома получила название "Copy And Swap" и решала три проблемы:
- Контроль самоприсваивания (легко забыть)
- Легко реализуемая строгая гарантия безопасности исключений
- Дублирование кода
Минусы:
- Больше операций (Конструктор = выделение + копирование; Обмен через swap; Деструктор), чем в ручной версии (выделение и освобождение). Впрочем разница получается незначительная, а также здесь возможна оптимизация компилятора.
- При копировании самого в себя, происходит безопасное создание копии и обмен с самим собой. Это крайне редкий случай, который нужно пресекать не внутри функции, а до ее вызова.
- Часто новички пытаются использовать std::swap, что приводит к бесконечной рекурсии и stack overflow
С приходом C++11, move-семантики и новой версии std::swap, популярность идиомы сошла на нет. Обновленная версия идиомы выглядит теперь так:
// myclass.h
#pragma once
class myclass final {
public:
myclass() noexcept;
myclass(const myclass& rhs);
myclass(myclass&& rhs) noexcept;
myclass& operator=(myclass rhs) noexcept;
~myclass();
void swap(myclass& other) noexcept;
private:
char* buffer{ nullptr };
};
void swap(myclass& a, myclass& b) noexcept;
// myclass.cpp
#include "myclass.h"
#include <cstring> // std::strlen, std::memcpy
#include <utility> // std::exchange
myclass::myclass(const myclass& rhs)
{
if (rhs.buffer) {
auto len = std::strlen(rhs.buffer);
buffer = new char[len + 1];
std::memcpy(buffer, rhs.buffer, len + 1);
}
}
myclass::myclass(myclass&& rhs) noexcept
: buffer(std::exchange(rhs.buffer, nullptr))
{
}
myclass& myclass::operator=(myclass rhs) noexcept {
swap(rhs);
return *this;
}
void myclass::swap(myclass& other) noexcept {
using std::swap;
swap(buffer, other.buffer);
}
myclass::~myclass() {
delete[] buffer;
}
void swap(myclass& a, myclass& b) noexcept {
a.swap(b);
}
Обратите внимание, что оператор присваивания принимает аргумент по значению! Нет необходимости писать два раздельных оператора:
Когда параметр передаётся по значению, компилятор сам выбирает, как его создать:
- Если передаётся lvalue (
a = b) → вызывается конструктор копирования → внутри функции оказывается копия - Если передаётся rvalue (
a = std::move(b)) → вызывается move-конструктор → внутри функции оказывается перемещённый объект
Затем мы просто вызываем swap с этим параметром, и он забирает его содержимое, а старые данные объекта умирают вместе с параметром при выходе из функции.
Плюсы:
- Меньше кода - одна функция вместо двух
- Нет дублирования - логика обмена пишется один раз
- Автоматическая безопасность - строгая гарантия исключений для копирования, корректная обработка самоприсваивания
- Не нужно писать
if (this != &rhs)- даже для перемещающего присваивания
Минусы:
- Всегда выделяется новая память при копировании - даже если старый буфер достаточно велик и можно было переиспользовать его
- Лишний move при перемещающем присваивании - вместо прямого обмена создаётся промежуточный объект, который сразу уничтожается
Когда имеет смысл применять:
Подход хорош, когда память при присваивании всё равно перевыделяется (как в примере выше) и корректность важнее микрооптимизаций. Не стоит использовать, когда переиспользование существующего буфера даёт значительный выигрыш в производительности.