Стоит ли затирать все поля перемещаемого объекта класса
У меня есть класс:
template<class T>
class IdRange
{
T var1_;
...
T var6_;
T start_;
T step_;
public:
IdRange(T start = 0, T step = 1) { ... }
IdRange(IdRange&& other)
: var1_ (std::move(other.var1_)),
...
var6_ (std::move(other.var6_)),
start_(std::move(other.start_)),
step_ (std::move(other.step_))
{
Как сделать здесь?
}
}
(Под словом "затереть" я буду подразумевать присвоение значения по умолчанию. Так короче.)
Ситуация такая. Есть разные поля я обозначил их как T var1_; ... T var6_;. С ними все понятно я переношу их из other в this и затираю в other, но есть еще start_ и step_. Для начала их тоже стоит перенести как и прочие. Но что делать с ними потом. start_ и step_ задаются только при создании объекта, потом их изменить нельзя. Как принято поступать в такой ситуации:
- Затереть прочие поля, но не затирать start_ и step_.
- Затереть все.
- Как таковых правил касательно такого нет, и можно делать как удобно.
И небольшое уточнение.
IdRange(IdRange&& other)
: var1_ (std::move(other.var1_)),
...
var6_ (std::move(other.var6_)),
start_(std::move(other.start_)),
step_ (std::move(other.step_))
}
стоит ли писать std::move? На какой сайт не захожу никто их здесь не использует, а без них вызывается конструктор копирования, так показывает отладчик если что.
Ответы (2 шт):
При перемещении объекта нужно затирать поля только тогда, когда следует избавиться от проблемы двойного владения. Например поле как указатель указывает на область выделенной памяти. И при вызове одного деструктора второй объект будет лишён данными и удаление памяти второй раз приведёт к ошибке памяти.
Всё зависит от алгоритма и принципа полей. Могут быть варианты, что целочисленные индексы могут служить как знак владения какой-то области памяти. Тогда и эти индексы следует обнулять, чтобы не было двойного владения.
Команда std::move это упрощение преобразования типа static_cast<C&&>(..). И его следует использовать если значения внутренней переменной следует перенести в другой объект. Но простые типы обычно не обнуляются при вызове конструктора перемещения, и программисту следует это делать вручную.
Пример с указателями:
class C{
public:
C(C&&c):
// указатель это простой тип, и при перемещении не обнуляется
// использовать std::move не имеет смысла
ar{c.ar},
// то-же самое
si{c.si}{
// затирание у аргумента нужно делать вручную
c.ar = nullptr ;
c.si = 0 ; }
private:
int * ar ;
size_t si ;
};
Если у вас класс с шаблонными внутренними типами, следует использовать std::move для универсального поведения и вручную вызывать функцию очищения от собственного владения внешним источником памяти, если конструктор перемещения был вызван к простым типам, типа указателя.
template<class T>
void IdRangeInit( T & );
IdRange(IdRange&& other)
: var1_ (std::move(other.var1_)),
...
var6_ (std::move(other.var6_)),
start_(std::move(other.start_)),
step_ (std::move(other.step_))
{
IdRangeInit<T>(other.var1_);
...
IdRangeInit<T>(other.var6_);
IdRangeInit<T>(other.start_);
IdRangeInit<T>(other.step_);
}
template<>
void IdRangeInit( size_t & i ){
i = 0 ; }
template<>
void IdRangeInit( int * & i ){
i = nullptr ; }
// Если какой-то класс после перемещения уже пустой,
// то ничего больше делать не надо
template<>
void IdRangeInit( BigClass & b ) {
}
Грубо говоря, есть два вида классов:
Еоторым нужны самодельные деструктры, копирующие и/или перемещающие конструкторы и операторы присваивания, потому что они напрямую владеют каким-то ресурсом (или еще почему-то).
Которым ничего из этого не нужно, и они ничем не владеют.
Последних большинство.
В вашем классе я не вижу какой-то особой логики в перемещающем конструкторе, значит вам скорее всего стоит последовать правилу нуля и не писать ни перемещающий конструктор, ни остальные перечисленные операции.
Это приведет к тому, то поля объекта, из которого переместили, не будут зануляться, и для класса из второй категории это нормально. Не будете же вы для каждой мелкой структурки писать руками все 5 операций, чтобы добиться этого зануления.
Классам из первой категории обычно нужно все занулять (или делать что-то похожее). Представьте, если бы std::vector при перемещении указатель на буфер занулял (без этого никак), а вот размер - нет. Было бы плохо.