Введение
«Правило пяти» (Rule of Five) — современная версия классического «правила трёх» (Rule of Three) из C++98, расширенная в C++11 семантикой перемещения. Если ваш класс самостоятельно управляет каким-либо ресурсом (динамической памятью, файловым дескриптором, мьютексом и т. п.) и поэтому нуждается в собственном деструкторе, скорее всего ему также нужны: конструктор копирования, оператор копирующего присваивания, конструктор перемещения и оператор перемещающего присваивания — все пять специальных функций-членов.
Суть концепции
Компилятор C++ автоматически генерирует эти пять функций по умолчанию (с некоторыми условиями), но дефолтная генерация выполняет лишь поверхностное (shallow) копирование/перемещение членов класса. Если класс владеет ресурсом через сырой указатель, дефолтное копирование скопирует указатель, но не сам ресурс — два объекта окажутся «владельцами» одной и той же памяти, и при удалении одного из них второй останется с висящим указателем (dangling pointer), что приведёт к двойному освобождению при удалении второго.
#include <algorithm>
#include <cstring>
#include <iostream>
class Buffer
{
public:
explicit Buffer(size_t size)
: size_(size), data_(new char[size])
{
std::cout << "Конструктор: выделено " << size_ << " байт" << std::endl;
std::fill(data_, data_ + size_, 0);
}
// 1. Деструктор
~Buffer()
{
std::cout << "Деструктор: освобождено " << size_ << " байт" << std::endl;
delete[] data_;
}
// 2. Конструктор копирования: глубокое копирование
Buffer(const Buffer& other)
: size_(other.size_), data_(new char[other.size_])
{
std::cout << "Конструктор копирования" << std::endl;
std::copy(other.data_, other.data_ + size_, data_);
}
// 3. Оператор копирующего присваивания
Buffer& operator=(const Buffer& other)
{
std::cout << "Оператор копирующего присваивания" << std::endl;
if (this == &other) {
return *this; // защита от самоприсваивания
}
char* newData = new char[other.size_]; // сначала выделяем
std::copy(other.data_, other.data_ + other.size_, newData);
delete[] data_; // потом освобождаем старое (exception-safety)
data_ = newData;
size_ = other.size_;
return *this;
}
// 4. Конструктор перемещения: забираем ресурс, не копируя
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_)
{
std::cout << "Конструктор перемещения" << std::endl;
other.data_ = nullptr; // у "донора" больше нет владения
other.size_ = 0;
}
// 5. Оператор перемещающего присваивания
Buffer& operator=(Buffer&& other) noexcept
{
std::cout << "Оператор перемещающего присваивания" << std::endl;
if (this == &other) {
return *this;
}
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
return *this;
}
size_t size() const { return size_; }
private:
size_t size_;
char* data_;
};
Buffer createBuffer()
{
Buffer b(64);
return b; // компилятор использует перемещение, а не копирование
}
int main()
{
Buffer a(128);
Buffer b = a; // конструктор копирования
Buffer c = std::move(a); // конструктор перемещения
Buffer d = createBuffer(); // перемещение (или вообще пропуск копии — copy elision)
b = c; // оператор копирующего присваивания
c = std::move(d); // оператор перемещающего присваивания
return 0;
}
Разбор примера
Класс Buffer владеет сырым указателем data_ на массив в куче. Без явных реализаций всех пяти функций дефолтный конструктор копирования просто скопировал бы значение указателя data_, и оба объекта (оригинал и копия) указывали бы на одну и ту же память — при удалении одного из них второй стал бы держать висящий указатель, а двойной delete[] привёл бы к неопределённому поведению.
Конструктор копирования и оператор копирующего присваивания выполняют глубокое копирование: выделяют собственную память и копируют в неё байты. Обратите внимание на порядок операций в operator=: новая память выделяется до освобождения старой — это защищает от потери данных, если new бросит std::bad_alloc (так называемая exception-safety, безопасность по отношению к исключениям).
Конструктор перемещения и оператор перемещающего присваивания, напротив, не копируют байты, а просто «забирают» указатель у временного объекта (other), оставляя исходный объект в валидном, но пустом состоянии (data_ = nullptr). Перемещение — это операция O(1) независимо от размера буфера, в отличие от копирования, которое всегда O(n).
В createBuffer() компилятор, скорее всего, вообще пропустит и копирование, и перемещение благодаря оптимизации copy elision (гарантированной в C++17 для подобных случаев), но даже если бы оптимизация не сработала, был бы вызван конструктор перемещения, а не копирования — это и есть причина, почему перемещающие операции должны быть эффективными и желательно noexcept.
Подводные камни и советы
- Если вы не управляете ресурсом напрямую — не пишите эти пять функций вообще. Используйте
std::vector,std::string,std::unique_ptr/std::shared_ptrкак члены класса — они уже правильно реализуют Rule of Five, и дефолтные (compiler-generated) специальные функции вашего класса будут работать корректно автоматически. Это называется Rule of Zero — лучшая практика, к которой стоит стремиться. - Помечайте перемещающие операции
noexcept, когда это возможно. Контейнеры стандартной библиотеки (например,std::vectorпри увеличении capacity) предпочитают перемещение копированию только если перемещающий конструктор гарантированно не бросает исключений; иначе они «на всякий случай» используют более медленное копирование. - Объявление любого из пяти специальных методов подавляет автогенерацию некоторых остальных (например, объявление деструктора подавляет неявную генерацию конструктора и оператора перемещения). Если нужен дефолтный вариант — объявите его явно как
= default. - Защита от самоприсваивания обязательна в копирующем присваивании в стиле «вручную управляемых ресурсов» (хотя есть и идиома copy-and-swap, избегающая этой проверки за счёт временной копии).
- Соблюдение Rule of Five — фундамент написания корректных низкоуровневых классов в C++; в большинстве прикладного кода современного C++ оно проявляется через Rule of Zero — просто не пишите эти функции, доверившись членам-обёрткам.
================================================================================