Если вы определяете или `=delete` какую-либо функцию копирования, перемещения или деструктора — определите или `=delete` их все
Причина
Семантика копирования, перемещения и уничтожения тесно связана, поэтому если одна из них должна быть объявлена, вероятно, остальные также требуют рассмотрения.
Объявление любой функции копирования/перемещения/деструктора, даже как =default или =delete, подавит неявное объявление конструктора перемещения и оператора перемещающего присваивания. Объявление конструктора перемещения или оператора перемещающего присваивания, даже как =default или =delete, приведёт к тому, что неявно сгенерированный конструктор копирования или неявно сгенерированный оператор копирующего присваивания будут определены как удалённые. Поэтому как только любая из этих функций объявлена, остальные также должны быть объявлены, чтобы избежать нежелательных эффектов, таких как превращение всех потенциальных перемещений в более дорогостоящие копирования или создание класса только для перемещения.
Пример (плохой)
struct M2 { // плохо: неполный набор операций копирования/перемещения/деструктора
public:
// ...
// ... нет операций копирования или перемещения ...
~M2() { delete[] rep; }
private:
pair<int, int>* rep; // завершающийся нулём набор пар
};
void use()
{
M2 x;
M2 y;
// ...
x = y; // присваивание по умолчанию
// ...
}
Учитывая, что деструктору требовалось «особое внимание» (здесь для освобождения памяти), вероятность того, что неявно определённые операторы копирующего и перемещающего присваивания окажутся корректными, невелика (здесь мы получим двойное удаление).
Примечание
Это известно как «правило пяти».
Примечание
Если вы хотите реализацию по умолчанию (при определении другой), напишите =default, чтобы показать намеренность. Если вы не хотите сгенерированную функцию по умолчанию, подавите её с помощью =delete.
Пример (хороший)
Когда деструктор нужно объявить только для того, чтобы сделать его virtual, его можно определить как defaulted.
class AbstractBase {
public:
virtual void foo() = 0; // хотя бы один абстрактный метод, чтобы класс стал абстрактным
virtual ~AbstractBase() = default;
// ...
};
Чтобы предотвратить срезание согласно C.67, сделайте операции копирования и перемещения защищёнными или =delete-нными и добавьте clone:
class CloneableBase {
public:
virtual unique_ptr<CloneableBase> clone() const;
virtual ~CloneableBase() = default;
CloneableBase() = default;
CloneableBase(const CloneableBase&) = delete;
CloneableBase& operator=(const CloneableBase&) = delete;
CloneableBase(CloneableBase&&) = delete;
CloneableBase& operator=(CloneableBase&&) = delete;
// ... другие конструкторы и функции ...
};
Определение только операций перемещения или только операций копирования дало бы тот же эффект, но явное указание намерения для каждого специального члена делает код более понятным для читателя.
Примечание
Компиляторы обеспечивают соблюдение большей части этого правила и в идеале предупреждают о любом нарушении.
Примечание
Опора на неявно сгенерированную операцию копирования в классе с деструктором считается устаревшей.
Примечание
Написание этих функций может быть подвержено ошибкам. Обратите внимание на типы аргументов:
class X {
public:
// ...
virtual ~X() = default; // деструктор (виртуальный, если X — базовый класс)
X(const X&) = default; // конструктор копирования
X& operator=(const X&) = default; // оператор копирующего присваивания
X(X&&) noexcept = default; // конструктор перемещения
X& operator=(X&&) noexcept = default; // оператор перемещающего присваивания
};
Небольшая ошибка (например, опечатка, пропущенный const, & вместо && или пропущенная специальная функция) может привести к ошибкам или предупреждениям. Чтобы избежать утомительности и возможных ошибок, старайтесь следовать правилу нуля.
Контроль
(Простой) Класс должен иметь объявление (даже =delete) либо для всех, либо ни для одной из функций копирования/перемещения/деструктора.