#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
что выражение больше не является именем переменной.