Что такое идиома copy-and-swap в C++?
Что такое copy-and-swap idiom?
Идиома copy-and-swap (копирование и обмен) — это элегантный способ реализации оператора присваивания, который одновременно решает три задачи:
- Безопасность исключений — объект никогда не остаётся в повреждённом состоянии
- Избежание дублирования кода — использует уже написанный конструктор копирования
- Корректная работа с самоприсваиванием — не нужно явной проверки
if (this != &other)
Основная идея
Идиома состоит из трёх шагов:
- Создаём временную копию объекта с помощью конструктора копирования
- Меняем содержимое текущего объекта с временной копией через функцию
swap - Временная копия уничтожается (в конце области видимости), освобождая старые ресурсы
Ключевой момент — нам нужна noexcept функция обмена (обычно std::swap или своя реализация), которая просто меняет указатели/значения, не выделяя память и не бросая исключений.
Реализация для вашего класса
Добавляем функцию swap и переписываем оператор присваивания:
class String {
public:
// ... конструкторы и деструктор остаются без изменений ...
// Функция обмена (должна быть noexcept!)
friend void swap(String& first, String& second) noexcept {
using std::swap;
swap(first.size, second.size);
swap(first.data, second.data);
}
// Оператор присваивания через copy-and-swap
String& operator=(String other) { // Принимаем по значению!
swap(*this, other); // Меняем текущий объект с копией
return *this;
}
};
Как это работает
Разберём по шагам:
String& operator=(String other)— параметр передаётся по значению. Это означает, что для созданияotherбудет вызван конструктор копирования (или перемещения). Если копирование не удалось (исключение), тоotherпросто не создастся, а наш объект останется нетронутым.swap(*this, other)— мы меняем содержимое*thisиother. Посколькуswapне бросает исключений, это безопасно.- После выхода из функции
otherуничтожается, вызывая деструктор для старых данных, которые теперь принадлежатother.
Преимущества
- Безопасность исключений (strong exception guarantee) — если что-то пошло не так, объект остаётся в исходном состоянии
- Самоприсваивание —
a = aработает корректно: создаётся копия, меняем с собой же, потом копия уничтожается - Минимум кода — не нужно писать логику копирования в операторе присваивания
- Автоматически получаем move-семантику — если добавить конструктор перемещения, оператор присваивания будет работать и для rvalue
Полный пример
#include <cstring>
#include <algorithm>
class String {
char* data;
size_t size;
public:
String(const char* str = "")
: size(strlen(str)), data(new char[size + 1]) {
strcpy(data, str);
}
String(const String& other)
: size(other.size), data(new char[size + 1]) {
strcpy(data, other.data);
}
// Конструктор перемещения (C++11)
String(String&& other) noexcept
: data(nullptr), size(0) {
swap(*this, other);
}
~String() { delete[] data; }
friend void swap(String& first, String& second) noexcept {
using std::swap;
swap(first.size, second.size);
swap(first.data, second.data);
}
String& operator=(String other) {
swap(*this, other);
return *this;
}
};
Идиома copy-and-swap — это мощный инструмент, который делает код безопаснее, проще и короче. Используйте её везде, где класс управляет ресурсами (память, файлы, мьютексы).
<<Альтернативный взгляд: когда copy-and-swap может быть не оптимален
Хотя copy-and-swap — отличная идиома для общего случая, у неё есть потенциальный недостаток: производительность. При каждом присваивании создаётся временная копия, даже если текущий объект мог бы переиспользовать свои ресурсы.
Рассмотрим пример:
String a("Hello world");
String b("C++ programming");
a = b; // Создаётся копия b, потом обмен, потом удаление старой памяти a
В этом случае мы могли бы просто скопировать данные из b в a, переиспользовав уже выделенную память a (если она достаточно велика). Copy-and-swap же всегда выделяет новую память, что может быть дорого для больших строк.
Когда стоит отказаться от copy-and-swap
- Класс управляет очень большими объёмами данных
- Присваивание происходит часто в критическом по производительности коде
- Вы уверены, что переиспользование ресурсов даст выигрыш
Оптимизированная версия для таких случаев
String& operator=(const String& other) {
if (this != &other) {
// Пытаемся переиспользовать память
if (size != other.size) {
delete[] data;
data = new char[other.size + 1];
size = other.size;
}
strcpy(data, other.data);
}
return *this;
}
Но помните: такая оптимизация оправдана только если вы профилировали код и доказали, что copy-and-swap является узким местом. В 95% случаев copy-and-swap — правильный выбор, так как он гарантирует безопасность исключений и простоту поддержки кода.
Мой совет: начинайте с copy-and-swap, и только если профилирование покажет проблемы — рассматривайте альтернативы.
Ответов пока нет.
Войдите чтобы оставить ответ