Оригинальное название: Deep vs. Shallow
Узнайте разницу между неглубокими и глубокими копиями, как реализовать правильные операции копирования для классов, владеющих ресурсами, и идиома копирования и обмена.
Почему семантика имеет значение
Когда вы копируете объект в C++, что именно копируется? Для простых типов ответ прост — все участники копируются по битам. Но когда объект владеет ресурсом через указатель, конструктор копий по умолчанию (сгенерированный компилятором) выполняет небольшую копию: он копирует значение указателя, но не данные, на которые он указывает. Это означает, что два объекта теперь используют один и тот же ресурс, что приводит к ошибкам и повреждению данных.
Понимание семантики копирования важно, потому что копии происходят неявно во многих местах: проходя мимо значения, возвращаясь от функций, вставляя в контейнеры и присваивая.
Маленькая проблема копирования
Созданный компилятором конструктор копий копирует каждого участника. Для указателей это копирует адрес, а не указательные данные. Это небольшая копия, и это опасно для классов, владеющих ресурсами.
#include <iostream>
#include <cstring>
class BrokenString {
char* data_;
std::size_t len_;
public:
BrokenString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
~BrokenString() {
delete[] data_; // frees the buffer
}
// No copy constructor defined!
// Compiler generates shallow copy: copies the pointer
void print() const {
std::cout << data_ << '\n';
}
};
int main() {
BrokenString a{"Hello"};
{
BrokenString b = a; // shallow copy: b.data_ == a.data_
b.print(); // "Hello" — works for now
} // b destroyed: delete[] b.data_ (same pointer as a.data_!)
// a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
a.print(); // may crash, print garbage, or appear to work
return 0;
}Реализация Deep Copy
Глубокая копия выделяет новую память и копирует фактические данные. Это гарантирует, что каждый объект имеет собственную независимую копию ресурса.
#include <iostream>
#include <cstring>
#include <algorithm>
class SafeString {
char* data_;
std::size_t len_;
public:
explicit SafeString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
// Deep copy constructor
SafeString(const SafeString& other)
: len_{other.len_}
, data_{new char[other.len_ + 1]}
{
std::strcpy(data_, other.data_);
}
// Deep copy assignment with self-assignment check
SafeString& operator=(const SafeString& other) {
if (this == &other) return *this; // self-assignment guard
// Allocate new resource BEFORE releasing old one
char* new_data = new char[other.len_ + 1];
std::strcpy(new_data, other.data_);
// Release old resource
delete[] data_;
// Adopt new resource
data_ = new_data;
len_ = other.len_;
return *this;
}
~SafeString() { delete[] data_; }
void print() const {
std::cout << data_ << " (len=" << len_ << ")\n";
}
};
int main() {
SafeString a{"Hello"};
SafeString b = a; // deep copy: b gets its own buffer
SafeString c{"World"};
c = a; // deep copy assignment
a.print(); // Hello (len=5)
b.print(); // Hello (len=5)
c.print(); // Hello (len=5)
return 0;
}Оригинальное название: Copy-and-Swap Idiom
copy-and-swap idiom обеспечивает элегантную, безопасную реализацию оператора присвоения копий. Он повторно использует конструктор копий и функцию swap, обеспечивая высокую безопасность исключений с минимальным дублированием кода.
#include <iostream>
#include <cstring>
#include <utility>
class Buffer {
char* data_;
std::size_t size_;
public:
explicit Buffer(std::size_t size)
: size_{size}, data_{new char[size]{}} {}
Buffer(const Buffer& other)
: size_{other.size_}, data_{new char[other.size_]}
{
std::memcpy(data_, other.data_, size_);
}
~Buffer() { delete[] data_; }
// Copy-and-swap: take parameter by VALUE (invokes copy ctor)
Buffer& operator=(Buffer other) { // copy happens here
swap(*this, other); // swap resources
return *this;
} // 'other' destroyed here, freeing our old resource
// Non-throwing swap — essential for strong exception safety
friend void swap(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
char& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
};
int main() {
Buffer a{5};
a[0] = 'H'; a[1] = 'i';
Buffer b{10};
b = a; // copy-and-swap: exception-safe, self-assignment-safe
std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
return 0;
}Отключение копирования
Некоторые типы не должны быть копируемыми, например, типы, которые представляют уникальные ресурсы, такие как ручки файлов, сетевые соединения или потоки. Используйте =delete для четкого общения и обеспечения соблюдения этого:
class UniqueConnection {
public:
UniqueConnection(const UniqueConnection&) = delete;
UniqueConnection& operator=(const UniqueConnection&) = delete;
// Move operations can still be enabled
};До C++11 это было сделано путем объявления операций копирования private без определения — запутанный взлом, который элегантно заменяет =delete.
Наивное присвоение копии, которое удаляет старый ресурс перед выделением нового, нарушается двумя способами: (1) самоназначение (a = a) удаляет данные, а затем пытается скопировать из него, и (2) если new бросает после delete, объект остается в недействительном состоянии. Идиома копирования и свопа решает обе проблемы: копия делается до каких-либо модификаций, а своп — noexcept.
Лучший способ избежать ошибок копирования — никогда не управлять ресурсами напрямую. Используйте std::string вместо char*, std::vector вместо необработанных массивов и std::unique_ptr вместо необработанных указателей. Когда все ваши участники имеют правильную семантику копирования, операции копирования, созданные компилятором, автоматически корректируются. Это правило ноль на практике.
- Сгенерированная компилятором копия shallow — она копирует значения указателей, а не указательные данные
- Ресурс-собственные классы нуждаются в глубокой копии конструктора и оператора назначения
- Идиома копирования и замены дает вам безопасность исключения и безопасность самоназначения бесплатно
- Использование
=deleteдля явного запрета копирования типов уникальных ресурсов - Лучшей стратегией является Правило нуля: используйте обертки RAII, чтобы вам никогда не понадобилась индивидуальная логика копирования.
Тест — проверьте свои знания
(15 XP)1. Что происходит, когда вы неглубоко копируете объект, который владеет кучной памятью через необработанный указатель?
2. Почему идиома копирования и свопа принимает параметр по значению?
3. Какой подход рекомендуется использовать, чтобы избежать необходимости копирования на заказ?