Стоит ли затирать все поля перемещаемого объекта класса

У меня есть класс:

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 шт):

Автор решения: AlexGlebe

При перемещении объекта нужно затирать поля только тогда, когда следует избавиться от проблемы двойного владения. Например поле как указатель указывает на область выделенной памяти. И при вызове одного деструктора второй объект будет лишён данными и удаление памяти второй раз приведёт к ошибке памяти.

Всё зависит от алгоритма и принципа полей. Могут быть варианты, что целочисленные индексы могут служить как знак владения какой-то области памяти. Тогда и эти индексы следует обнулять, чтобы не было двойного владения.

Команда 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 ) {
  }
→ Ссылка
Автор решения: HolyBlackCat

Грубо говоря, есть два вида классов:

  • Еоторым нужны самодельные деструктры, копирующие и/или перемещающие конструкторы и операторы присваивания, потому что они напрямую владеют каким-то ресурсом (или еще почему-то).

  • Которым ничего из этого не нужно, и они ничем не владеют.

Последних большинство.

В вашем классе я не вижу какой-то особой логики в перемещающем конструкторе, значит вам скорее всего стоит последовать правилу нуля и не писать ни перемещающий конструктор, ни остальные перечисленные операции.

Это приведет к тому, то поля объекта, из которого переместили, не будут зануляться, и для класса из второй категории это нормально. Не будете же вы для каждой мелкой структурки писать руками все 5 операций, чтобы добиться этого зануления.

Классам из первой категории обычно нужно все занулять (или делать что-то похожее). Представьте, если бы std::vector при перемещении указатель на буфер занулял (без этого никак), а вот размер - нет. Было бы плохо.

→ Ссылка