#опытным
После того, как в С++11 появилась семантика перемещения владения ресурсами, появились также объекты, которые единолично владеют определенным ресурсом. Они настолько не хотят им делиться с другими, что семантика копирования для них стала неприемлемой. Поэтому между новыми(перемещающими) и старыми(копирующими) специальными методами классов появилось некое противоборство - отказ компилятора автоматически генерировать определенные методы при определенных условиях.
Вам в любом случае стоит придерживаться правил 0 и 5, когда проектируете интерфейс создания и уничтожения объектов. Но все равно полезно знать, что будет если правила не выполнять.
Так когда же компилятор не будет генерировать каждый из специальных методов? Пост в первую очередь про взаимосвязь специальных методов, другие причины упоминаться не будут.
1️⃣ Деструктор
Деструктор компилятор может всегда сгенерировать. Оно в целом понятно: объект может как-то создаваться, поэтому и должен уметь как-то уничтожаться. Если вы не делаете ничего экзотического, компилятор предоставит вам деструктор. Другой вопрос, правильно ли он будет работать.
Банальный пример:
struct Bad {
Bad() : p{new int{5}} {}
Bad(Bad&& other) {
p = other.p;
other.p = nullptr;
}
int * p;
};
{
Bad b;
} // memory leakОчевидно, что мы по коду по-особенному управляем указателем. Но компилятор все равно сам сгенерирует деструктор, который ничего не освободит и мы получим утечку памяти.
2️⃣ Конструктор копирования и копирующий оператор присваивания
Если в классе определен хотя бы один перемещающий специальный метод, то ни один копирующий метод не генерируется.
struct Example {
Example() = default;
Example(Example&&) {}
};
Example a;
Example b = a; // Error: copy ctor is implicitly declared as deleted
Example c;
c = a; // Error: copy assign operator is implicitly declared as deletedНо это не потому что компилятор такой вредный. Если вы своими ручками определили перемещающие операции, но не определили копирующие, то вы скорее всего и не хотите, чтобы объекты можно было копировать. Но даже если и хотите, то компилятор уже понимает, что поверхностное копирование полей вам не подойдет и просто предостерегает вас от проблем.
При этом если вы определили деструктор, то копирующий операции все равно сгенерируются.
struct Bad {
Bad() : p{new int{5}} {}
~Bad() {delete p;}
int * p;
};
{
Bad b;
Bad b1 = b;
} // double freeПростейший пример и сразу же ловим двойное освобождение.
Такое поведение - наследие от более ранних стандартов, когда было правило 3-х, но оно было только на словах. Даже если деструктор делает нетривиальные вещи, то копирующие операции генерировались.
Конечно, это unsafe. Но обратная совместимость заставляет С++ нести эти особенности в новые стандарты.
3️⃣ Конструктор перемещения и перемещающее присваивание
Это те 2 новых специальных метода, которые добавили в С++11. Для новых вещей стандарт может устанавливать новые условия, которые не несут груз ответственности за обратную совместимость.
Поэтому для перемещающих операций все просто: если любой из 4-х оставшихся специальных метода определен пользователем, то компилятор не генерирует данную операцию.
struct Example {
~Example() {}
// or Example(Example&& other) {}
// or Example& operator=(const Example&) {}
// or Example& operator=(Example&&) {}
};
Example a;
Example b;
a = std::move(b); // Error: no move assignОно и понятно: если вы что-то сами определяете, значит хотите чего-то особенного. В этом плане компилятор усиливает правило 5: теперь вы обязаны сами определить перемещающие операций, если определяете другие специальные методы.
Be special. Stay cool.
#cpp11