Найти в Дзене

Идиома Copy-and-Swap в C++: Пишем безопасный оператор присваивания правильно

Поговорим об одной из интересных идиом в C++ — Copy-and-Swap. Если вы когда-нибудь писали класс, который управляет ресурсами (памятью, файлами или сокетами), вы наверняка сталкивались с головной болью под названием «перегрузка оператора присваивания». Хочется сделать его надежным, безопасным и не облажаться с исключениями. Copy-and-Swap — это элегантный способ решить эти проблемы раз и навсегда. Copy-and-Swap (или «копируй и обменяй») — это не просто трюк, а настоящий джентльменский набор для оператора operator=. Идея до безобразия проста: 1. Копируем — создаем временную копию объекта, который присваиваем. 2. Обмениваем — меняем местами внутренности текущего объекта с этой копией. 3. Уничтожаем — копия умирает, унося с собой старые данные (те, что были в текущем объекте до присваивания). Всё гениальное просто. А теперь давайте посмотрим, почему этот подход выигрывает по всем фронтам. - Гарантия безопасности исключений (Strong Exception Safety): Если на этапе копирования вылетит ис

Поговорим об одной из интересных идиом в C++ — Copy-and-Swap. Если вы когда-нибудь писали класс, который управляет ресурсами (памятью, файлами или сокетами), вы наверняка сталкивались с головной болью под названием «перегрузка оператора присваивания».

Хочется сделать его надежным, безопасным и не облажаться с исключениями. Copy-and-Swap — это элегантный способ решить эти проблемы раз и навсегда.

Copy-and-Swap (или «копируй и обменяй») — это не просто трюк, а настоящий джентльменский набор для оператора operator=.

Идея до безобразия проста:

1. Копируем — создаем временную копию объекта, который присваиваем.

2. Обмениваем — меняем местами внутренности текущего объекта с этой копией.

3. Уничтожаемкопия умирает, унося с собой старые данные (те, что были в текущем объекте до присваивания).

Всё гениальное просто. А теперь давайте посмотрим, почему этот подход выигрывает по всем фронтам.

- Гарантия безопасности исключений (Strong Exception Safety): Если на этапе копирования вылетит исключение (например, не хватит памяти), исходный объект останется нетронутым. Он же не изменился, пока копия не создана!

- Чистота кода: Вы пишете меньше кода, а значит, меньше вероятность допустить ошибку.

- Move-семантика «из коробки»: Если передать в operator= временный объект (rvalue), компилятор сам подхватит move-конструктор, и копирования не произойдет. Халява!

Пример 1: Начнем с классики — самописной строки.

#include <cstring>

#include <algorithm>

class MyString {

private:

char* data_;

size_t size_;

public:

// Конструктор

MyString(const char* str = "")

: size_(strlen(str)), data_(new char[size_ + 1]) {

strcpy(data_, str);

}

// Деструктор

~MyString() {

delete[] data_;

}

// Копирующий конструктор

MyString(const MyString& other)

: MyString(other.data_) {} // Делегируем конструктору

// Move-конструктор

MyString(MyString&& other) noexcept

: data_(other.data_), size_(other.size_) {

other.data_ = nullptr;

other.size_ = 0;

}

// Внимание! Золото здесь 👇

MyString& operator=(MyString other) noexcept { // Принимаем по значению!

swap(other);

return *this;

}

// Метод swap — сердце идиомы

void swap(MyString& other) noexcept {

using std::swap;

swap(data_, other.data_);

swap(size_, other.size_);

}

};

Передавая other по значению, мы говорим компилятору: «Слушай, создай мне копию, а если сможешь переместить — перемести». Дальше мы просто меняемся с этой копией данными. Выходя из функции, other умирает и прихватывает с собой нашу старую память. Красота!

Пример 2: Владелец ресурсов

Теперь представим, что у нас есть массив целых чисел. Та же история, только ресурсов побольше.

class ResourceHolder {

private:

int* data_;

size_t count_;

public:

ResourceHolder(size_t n = 0)

: count_(n), data_(new int[n]()) {} // Value-initialization

~ResourceHolder() {

delete[] data_;

}

ResourceHolder(const ResourceHolder& other)

: count_(other.count_), data_(new int[other.count_]) {

std::copy(other.data_, other.data_ + other.count_, data_);

}

ResourceHolder(ResourceHolder&& other) noexcept

: data_(other.data_), count_(other.count_) {

other.data_ = nullptr;

other.count_ = 0;

}

// Оператор присваивания — наш старый друг

ResourceHolder& operator=(ResourceHolder other) noexcept {

swap(other);

return *this;

}

void swap(ResourceHolder& other) noexcept {

using std::swap;

swap(data_, other.data_);

swap(count_, other.count_);

}

};

Пример 3: Stl-like как std::vector

Давайте напишем простенький вектор, чтобы закрепить.

template<typename T>

class SimpleVector {

private:

T* data_;

size_t size_;

size_t capacity_;

public:

SimpleVector(size_t n = 0)

: size_(n), capacity_(n), data_(n ? new T[n] : nullptr) {}

~SimpleVector() {

delete[] data_;

}

SimpleVector(const SimpleVector& other)

: size_(other.size_), capacity_(other.capacity_),

data_(other.capacity_ ? new T[other.capacity_] : nullptr) {

for (size_t i = 0; i < size_; ++i) {

data_[i] = other.data_[i];

}

}

SimpleVector(SimpleVector&& other) noexcept

: data_(other.data_), size_(other.size_), capacity_(other.capacity_) {

other.data_ = nullptr;

other.size_ = other.capacity_ = 0;

}

// И снова он!

SimpleVector& operator=(SimpleVector other) noexcept {

swap(other);

return *this;

}

void swap(SimpleVector& other) noexcept {

using std::swap;

swap(data_, other.data_);

swap(size_, other.size_);

swap(capacity_, other.capacity_);

}

};

Секрет идиомы copy-and-swap — в передаче аргумента по значению. Раньше (в старых учебниках) часто писали так:

MyString& operator=(const MyString& other) {

// 1. Проверка на самоприсваивание (this != &other) — костыль!

// 2. Удалить старые данные

// 3. Выделить новую память и скопировать

// 4. Profit? Нет, страшно и больно.

}

Этот подход требует проверки на самоприсваивание и ручного управления памятью. Ошибка в порядке действий — и вы получите утечку или двойное удаление. Copy-and-swap элегантно обходит все эти грабли.

Пара моментов к сведению все же есть:

1. Производительность. В некоторых случаях (очень большие объекты, которые не поддерживают move-семантику) лишнее копирование может быть дорогим. Но в 99% случаев это оверхед, которым можно пренебречь ради безопасности и простоты.

2. Самоприсваивание. Идиома корректно обрабатывает a = a, хоть и делает лишнюю работу (копирование самого себя). Это плата за простоту, и обычно она невелика.

Подытожим:

Copy-and-Swap — это не просто шаблон проектирования, это философия безопасного управления ресурсами в C++. Если ваш класс управляет памятью, файловым дескриптором или любым другим ресурсом, который нужно освобождать — используйте эту идиому. Она:

- Делает код красивым и понятным.

- Защищает от исключений.

- Бесплатно подключает move-семантику.