Зачем нам нужно устанавливать ссылку rvalue на null в конструкторе перемещения?

#c #oop #c 11 #visual-c

#c #c 11 #перемещение-семантика #rvalue-reference #move-constructor

Вопрос:

 //code from https://skillsmatter.com/skillscasts/2188-move-semanticsperfect-forwarding-and-rvalue-references
class Widget {
public:
    Widget(Widgetamp;amp; rhs)
        : pds(rhs.pds) // take source’s value
    { 
        rhs.pds = nullptr;  // why??
    }

private:
    struct DataStructure;
    DataStructure *pds;
};
 

Я не могу понять причину установки rhd.pds nullptr .

Что произойдет, если мы удалим эту строку : rhs.pds = nullptr;

Комментарии:

1. Есть ли удаленный вами деструктор, который попытался бы освободить указатель, если бы он не был очищен?

2. чтобы никакие два объекта-члена не указывали на одни и те же ячейки памяти…

3. Для ссылки требуется платный вход в систему. 🙁 (Кроме того, похоже, что «небольшое количество навыков» — это процесс, который даст вам «небольшое количество навыков».)

4. Если владение не должно быть общим, это ваш способ сообщить источнику операции перемещения , что у него больше нет указателя для управления; this объект делает. И если это относится std::unique_ptr<DataStructure> pds; к члену и pds(std::move(rhs.pds)) было бы предпочтительнее.

5. @Potatoswatter Нет, просто зарегистрируйтесь. Это не требует оплаты…

Ответ №1:

Некоторые детали класса были удалены. В частности, конструктор динамически выделяет DataStructure объект, а деструктор освобождает его. Если во время перемещения вы просто скопировали указатель с одного Widget на другой, оба Widget s будут иметь указатели на один и тот же выделенный DataStructure объект. Затем, когда эти объекты будут уничтожены, они оба попытаются delete это сделать. Это привело бы к неопределенному поведению. Чтобы избежать этого, Widget перемещаемый объект имеет свой внутренний указатель для установки nullptr .

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

Схематически вы начинаете с этой ситуации, желая передать право собственности DataStructure от одного Widget к другому:

     ┌────────┐        ┌────────┐
    │ Widget │        │ Widget │
    └───╂────┘        └────────┘
        ┃
        ▼
 ┌───────────────┐
 │ DataStructure │
 └───────────────┘
 

Если бы вы просто скопировали указатель, у вас было бы:

     ┌────────┐        ┌────────┐
    │ Widget │        │ Widget │
    └───╂────┘        └───╂────┘
        ┗━━━━━━━━┳━━━━━━━┛
                  ▼
         ┌───────────────┐
         │ DataStructure │
         └───────────────┘
 

Если вы затем установите исходный Widget указатель на nullptr , у вас есть:

     ┌────────┐         ┌────────┐
    │ Widget │         │ Widget │
    └────────┘         └───╂────┘
                           ┃
                           ▼
                  ┌───────────────┐
                  │ DataStructure │
                  └───────────────┘
 

Владение успешно передано, и когда оба Widget s могут быть уничтожены, не вызывая неопределенного поведения.

Комментарии:

1. поэтому мне интересно, что для этой ситуации есть ли какая-либо разница между выражениями rhs.pds = nullptr; , и pds = nullptr я думаю, что я могу написать pds = nullptr вместо rhs.pds = nullptr; . могу ли я?

2. @askque Нет, rhs.pds перестанет существовать, поэтому его указателю должно быть присвоено значение null, чтобы его деструктор не освобождал память. pds будет продолжать существовать, поэтому его указатель должен иметь правильное значение.

Ответ №2:

DataStructure Объект, вероятно, «принадлежит» Widget , и сброс указателя предотвращает его случайное удаление при Widget уничтожении.

С другой стороны, принято сбрасывать объекты в «пустое» или «стандартное» состояние при их перемещении, и сброс указателя — это безопасный способ следовать соглашению.

Ответ №3:

 class Widget {
  public:
    Widget(Widgetamp;amp; rhs)
       : pds(rhs.pds) // take source’s value
    { 
        rhs.pds = nullptr;  // why??
    }
    ~Widget() {delete pds}; // <== added this line

private:
    struct DataStructure;
    DataStructure *pds;
};
 

Я добавил деструктор в вышеуказанный класс.

 Widget make_widget() {
    Widget a;
    // Do some stuff with it
    return std::move(a);
}

int main {
    Widget b = make_widget;
    return 0;
}
 

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

Поскольку виджет a выходит из области видимости, вызывается его деструктор, который освобождает память, и вы остаетесь с виджетом b, который указывает на недопустимый адрес памяти.

Если вы назначаете nullptr для rhs, также вызывается деструктор, но поскольку delete nullptr ничего не делает, все хорошо 🙂

Комментарии:

1. Зачем возвращать std::move(a) ?!

2. @omid Чтобы запретить оптимизацию возвращаемого значения 🙂

3. @Blaz, как указывают omid и return move(a); juanchopanza, обычно это плохо. Компилятор (обычно) неявно обрабатывает возвращаемое значение как значение rvalue . Таким образом, вы ничего не получаете от move . Но, что более важно, возможна дальнейшая оптимизация (называемая RVO, быстрее, чем перемещение), и эта оптимизация отключается, если вы выполняете явное move . Бывают случаи, когда это имеет смысл, но если вы будете слепо это делать, вы будете замедлять работу чаще, чем ускорять ее.

4. В C 11 есть что-то (не помню где), в котором говорится, что если возвращаемое значение подходит для RVO, каковым является это значение, то возвращаемое значение должно быть создано путем перемещения из возвращаемого выражения. Этот ход по-прежнему подлежит исключению, хотя я считаю move(a) , что это не так, потому return что выражение больше не является именем переменной.