Ссылки и move semantics
От: Videoman Россия https://hts.tv/
Дата: 24.07.15 12:15
Оценка:
Добрый день.
В процессе реализации move конструктора возник вопрос который интернет "тактично" обходит.
Есть некая обертка, которая принимает ссылку на некий объект. Обертка не копируется и рассчитана на работу только в текущей области видимости. Теперь допустим мне необходимо создать вектор оберток. При создании вектора как раз и используется move конструктор.
Теперь проблема: при перемещении членов обертки, внутри конструктора, ссылку занулить невозможно.

Вопрос к знатокам:
Как принято действовать в этом случае и вообще кокой best practice для таких классов?

Какие есть варианты:
1. Вместо ссылок использовать указатели
2. Забить и оставить валидную ссылку в объекте который уже перемещен
3. Что-то еще

Что смущает:
После перемещения, теоретически, можно нечаянно через методы объекта который уже перемещен изменить объект на который осталась ссылка.

Кто что посоветует на этот счет?
Re: Ссылки и move semantics
От: watchmaker  
Дата: 24.07.15 12:57
Оценка:
Здравствуйте, Videoman, Вы писали:

V>В процессе реализации move конструктора возник вопрос который интернет "тактично" обходит.

Как же так? Задаёшь поисковику запрос «Reference data members and move constructor» и изучаешь.

V>Есть некая обертка, которая принимает ссылку на некий объект. Обертка не копируется и рассчитана на работу только в текущей области видимости. Теперь допустим мне необходимо создать вектор оберток. При создании вектора как раз и используется move конструктор.


V>Теперь проблема: при перемещении членов обертки, внутри конструктора, ссылку занулить невозможно.

Так и требования чего-то там занулять в move-конструкторе нет. После его работы экземпляр-источник должен остаться в консинстентном состоянии. Например, move-конструктор иногда через swap реализуется — очевидно тут два объекта просто меняются данными, а не зануляют что-то. Так и наличие ссылки в экземпляр-источнике на какой-то объект само по себе не приведёт проблемам пока, как ты заметил, через эту ссылку с объектом не начнут работать несогласованно из нескольких мест.

V>Что смущает:

V>После перемещения, теоретически, можно нечаянно через методы объекта который уже перемещен изменить объект на который осталась ссылка.
Да, но по сути такой же риск есть и для всех других классов после их перемещения. Наличие ссылки тут не делает проблему новой. Хотя, конечно, если использовать вместо ссылок указатели, то можно присваивать им nullptr и падать в segfault при попытке разыменования, что поможет отладке.

Но лучше просто следовать правилу «не использовать объект после std::move». Нарушил — сам виноват.

V>Какие есть варианты:

V>1. Вместо ссылок использовать указатели
V>2. Забить и оставить валидную ссылку в объекте который уже перемещен
V>3. Что-то еще
Например, дополнительный флаг в объекте «я_мёртв».
Но, в общем, первые два варианта самые подходящие. И если так уж не доверяешь пользователю обёртки, то выбирай реализацию через указатели.
Re[2]: Ссылки и move semantics
От: Videoman Россия https://hts.tv/
Дата: 24.07.15 13:21
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Как же так? Задаёшь поисковику запрос «Reference data members and move constructor» и изучаешь.

Смотрел, это первая ссылка в Google. К сожалению не совсем из нее понятно как правильно. Это просто такое же обсуждение проблемы на форуме. Может есть какая-то авторитетная статья по данной проблеме?

W>Так и требования чего-то там занулять в move-конструкторе нет. После его работы экземпляр-источник должен остаться в консинстентном состоянии. Например, move-конструктор иногда через swap реализуется — очевидно тут два объекта просто меняются данными, а не зануляют что-то. Так и наличие ссылки в экземпляр-источнике на какой-то объект само по себе не приведёт проблемам пока, как ты заметил, через эту ссылку с объектом не начнут работать несогласованно из нескольких мест.


Понятно что требований таких в C++ быть не может — на каждый случай своё решение. В том то и дело, что они оба, при таком подходе, легко могут оказаться в неконсистентном состоянии, так как я не планировал менять один и тот же объект из двух разных мест. Я не хочу оставлять на усмотрение внешнего кода, будут ли работать с уже перемещенным объектом. Тем более, что вы сами написали что источник должен остаться в консистентном состоянии, т.е. с ним могут начать работать.

W>Да, но по сути такой же риск есть и для всех других классов после их перемещения. Наличие ссылки тут не делает проблему новой. Хотя, конечно, если использовать вместо ссылок указатели, то можно присваивать им nullptr и падать в segfault при попытке разыменования, что поможет отладке.


У классов без ссылок я могу переинициализировать члены — я так и делаю.

W>Но лучше просто следовать правилу «не использовать объект после std::move». Нарушил — сам виноват.

Ну.... по-моему это противоречит утверждению, что объект должен оставаться в консистентном состоянии. Я не люблю следить за чем, за чем можно не следить.
Отредактировано 24.07.2015 13:40 Videoman . Предыдущая версия .
Re[3]: Ссылки и move semantics
От: watchmaker  
Дата: 24.07.15 14:35
Оценка:
Здравствуйте, Videoman, Вы писали:

V>Тем более что вы сами написал что источник должен остаться к консистентном состоянии, т.е. с ним могут начать работать.

Нет, смысл этого в другом — источник должен остаться в таком состоянии, что его удаление не уронит программу. Если у тебя есть unique_ptr, то в реализации должен изменяться указатель у источника при move — иначе память, на которую будут ссылаться оба указателя, будет освобождена дважды и программа упадёт.
Если другой wrapper_ptr не удаляет память в деструкторе, то указатель можно и не менять — всё будет работать. Если wrapper_ref содержит ссылку — то тем более её нет нужды менять — её «удаление» не уронит программу просто из-за того, что разрушение ссылки в деструкторе — суть отсутствие каких-либо действий.



W>>Но лучше просто следовать правилу «не использовать объект после std::move». Нарушил — сам виноват.

V>Ну.... по-моему это противоречит утверждению, что объект должен оставаться в консистентном состоянии.

В языке C++ есть понятие «implementation-defined behavior» — то есть в каждом конкретном случае поведение определено, но какое оно — зависит от реализации и может меняться.
Тут примерно то же самое — объект остался в консинстентном состоянии, но неизвестно какое это состояние. Так, например, то же перемещение из vector<T> может как сделать объект пустым, так и наполнить его какими-нибудь данными. Оба случая встречаются на практике. В обоих случаях поведение является допустимым, а состояние объекта остаётся согласованным, но вот использовать этот объект просто так не стоит — ведь записав в него через push_back какие-то данные не будет гарантии, что в нём не останется какой-то другой мусор.
Вот именно из-за этого и не стоит использовать объекты после move.

Конечно, это правило не абсолютно — можно ведь просто изучить исходный код и посмотреть что-же там внутри на самом деле происходит. Для переносимых программ и стандартной библиотеки это не вариант, но для собственноручно написанных классов вполне так можно делать. Хотя следуя принципу наименьшего удивления — всё же не стоит.

Да, в примере с vector можно делать ему resize(0) и продолжать использовать дальше. Но тут совсем нет универсального способа сделать это для всех классов. Некоторые объекты просто нельзя переиспользовать.

V>У классов без ссылок я могу переинициализировать члены — я так и делаю.

Ясно, а зачем? Использовал обёртку и выкинул, вместо этого создал другую. Такой вариант тебе не подходит? Ну тогда используй просто указатели вместо ссылок — и вопрос решён.
Re[4]: Ссылки и move semantics
От: Videoman Россия https://hts.tv/
Дата: 24.07.15 15:21
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Нет, смысл этого в другом — источник должен остаться в таком состоянии, что его удаление не уронит программу. Если у тебя есть unique_ptr, то в реализации должен изменяться указатель у источника при move — иначе память, на которую будут ссылаться оба указателя, будет освобождена дважды и программа упадёт.


Это понятно.

W>Если другой wrapper_ptr не удаляет память в деструкторе, то указатель можно и не менять — всё будет работать. Если wrapper_ref содержит ссылку — то тем более её нет нужды менять — её «удаление» не уронит программу просто из-за того, что разрушение ссылки в деструкторе — суть отсутствие каких-либо действий.


А как быть в случае ссылки и movе оператора? Я же не могу переприсвоить ссылку?

W>В языке C++ есть понятие «implementation-defined behavior» — то есть в каждом конкретном случае поведение определено, но какое оно — зависит от реализации и может меняться.

W>Тут примерно то же самое — объект остался в консинстентном состоянии, но неизвестно какое это состояние. Так, например, то же перемещение из vector<T> может как сделать объект пустым, так и наполнить его какими-нибудь данными. Оба случая встречаются на практике. В обоих случаях поведение является допустимым, а состояние объекта остаётся согласованным, но вот использовать этот объект просто так не стоит — ведь записав в него через push_back какие-то данные не будет гарантии, что в нём не останется какой-то другой мусор.
W>Вот именно из-за этого и не стоит использовать объекты после move.

Т.е. у такого объекта, в случае со ссылкой, должен быть организован некий zomby mode ?

V>>У классов без ссылок я могу переинициализировать члены — я так и делаю.

W>Ясно, а зачем? Использовал обёртку и выкинул, вместо этого создал другую. Такой вариант тебе не подходит? Ну тогда используй просто указатели вместо ссылок — и вопрос решён.

Подходит, но не думал что с ссылкой придется возится с тем, чтобы обеспечить контроль на случай, если перенесенный объект кто-то дернет случайно.

Сложность еще в том что до этого класс не имел конструктора по умолчанию и не было логики которая проверяла валидный объект или нет (кроме assert-ов).
Теперь придется везде добавлять проверки. Я правильно понимаю?
Отредактировано 24.07.2015 15:46 Videoman . Предыдущая версия .
Re: Ссылки и move semantics
От: VTT http://vtt.to
Дата: 24.07.15 19:54
Оценка:
Непонятно, что вас так смущает.
Ну да, в старом экземпляре останется ссылка. А также все константные и прочие non-movable поля.
Если вам хочется дополнительно защититься от повторного использования перемещенного объекта то вместо настоящей ссылки в нем можно использовать optional_reference. Или сделать дебаг флаг "moved", assert(!moved) в каждом методе и выставлять его в true при перемещении.
Но обычно это не нужно, так как объекты перемещаются чаще всего либо в нутрях контейнеров либо при возвращении из методов.
Говорить дальше не было нужды. Как и все космонавты, капитан Нортон не испытывал особого доверия к явлениям, внешне слишком заманчивым.
Re[5]: Ссылки и move semantics
От: watchmaker  
Дата: 24.07.15 22:15
Оценка: 4 (1)
Здравствуйте, Videoman, Вы писали:


W>>Если другой wrapper_ptr не удаляет память в деструкторе, то указатель можно и не менять — всё будет работать. Если wrapper_ref содержит ссылку — то тем более её нет нужды менять — её «удаление» не уронит программу просто из-за того, что разрушение ссылки в деструкторе — суть отсутствие каких-либо действий.


V>А как быть в случае ссылки и movе оператора?

Достаточно ничего не делать.

V>Я же не могу переприсвоить ссылку?

Для случаев же, когда без этого не обойтись, есть std::reference_wrapper — эту ссылку можно менять.

W>>Тут примерно то же самое — объект остался в консинстентном состоянии, но неизвестно какое это состояние. Так, например, то же перемещение из vector<T> может как сделать объект пустым, так и наполнить его какими-нибудь данными. Оба случая встречаются на практике. В обоих случаях поведение является допустимым, а состояние объекта остаётся согласованным, но вот использовать этот объект просто так не стоит — ведь записав в него через push_back какие-то данные не будет гарантии, что в нём не останется какой-то другой мусор.

W>>Вот именно из-за этого и не стоит использовать объекты после move.

V>Т.е. у такого объекта, в случае со ссылкой, должен быть организован некий zomby mode ?

Нет же.
То есть сделать так можно, но обычно не нужно. В C++ есть много мест где можно прострелить себе ногу, и это не самое популярное. Проверки и подстраховки — это хорошо, но для них есть и более приоритетные места.

Вот есть три популярные реализации стандартной библиотеки С++11: libstdc++, libc++ и вариант от visual c++. Ни в одной из них ничем подобным не занимаются. Можешь сам открыть реализацию, например, уже упомянутого std::reference_wrapper и убедится, что для перемещения ссылок там не задано никаких дополнительных действий, а ссылка-источник никак при этом не изменяется. И все нормально с этим живут. И даже по стандарту.

А тут ситуация больше похожа на то, что сначала выдумал проблему с перемещением ссылок, а теперь пытаешься её героически решить. Хотя самой проблемы нет — просто не трогай ссылки.
Я понимаю, что ты хочешь обезопасится от пользователя, который внезапно захочет работать с объектом из которого сделали move. Но у такого пользователя будут проблемы не только с твоей библиотекой. И проще считать, что не твоя библиотека должна учить его новым основам C++11



V>>>У классов без ссылок я могу переинициализировать члены — я так и делаю.

W>>Ясно, а зачем? Использовал обёртку и выкинул, вместо этого создал другую. Такой вариант тебе не подходит? Ну тогда используй просто указатели вместо ссылок — и вопрос решён.

V>Подходит, но не думал что с ссылкой придется возится с тем, чтобы обеспечить контроль на случай, если перенесенный объект кто-то дернет случайно.


V>Сложность еще в том что до этого класс не имел конструктора по умолчанию и не было логики которая проверяла валидный объект или нет (кроме assert-ов).

V>Теперь придется везде добавлять проверки. Я правильно понимаю?
Дешевый вариант — переехать на указатели и занулять их из move-коснтруктора. Тогда разыменование такого указателя хотя и будет UB, но достаточно часто всё-таки будет приводить к segfault, что вроде как желаемое поведение. Хотя иногда вместо segfault будет феерически глючить, ибо UB.
Но, да, если тебе нужно определенное поведение и гарантии что с таким объектом никто уже не будет работать, то придётся делать проверки на валидность. Для этого можно либо ввести дополнительный булев флаг (подобно всяким optional<T> и maybe<T> контейнерам), либо, опять же, перейти на указатели и проверть их каждый раз на nullptr перед разыменованием (можно, например, тот же reference_wrapper взять за основу, дописать в него соответствующие конструкторы из r_w<T>&& и засунуть проверку в .get).
Отредактировано 24.07.2015 22:17 watchmaker . Предыдущая версия .
Re: Ссылки и move semantics
От: Abyx Россия  
Дата: 25.07.15 18:05
Оценка: 4 (1) +1
Здравствуйте, Videoman, Вы писали:

V>Есть некая обертка, которая принимает ссылку на некий объект.


не делайте ссылки членами класса, храните указатели.
в private части класса достаточно инкапсуляции чтобы указатель не создавал проблем.

если не нравятся указатели — есть std::reference_wrapper, но лучше просто указатель.

V>Теперь проблема: при перемещении членов обертки, внутри конструктора, ссылку занулить невозможно.


при перемещении ничего не надо удалять.
для объекта-источника надо гарантировать только разрушение/присваивание.
In Zen We Trust
Re[2]: Ссылки и move semantics
От: _NN_ www.nemerleweb.com
Дата: 27.07.15 17:59
Оценка:
Здравствуйте, Abyx, Вы писали:

A>Здравствуйте, Videoman, Вы писали:


V>>Есть некая обертка, которая принимает ссылку на некий объект.


A>не делайте ссылки членами класса, храните указатели.

A>в private части класса достаточно инкапсуляции чтобы указатель не создавал проблем.
Спорное утверждение.
У ссылки есть важная фишка — одноразовая инициализация.
А вот указатель и reference_wrapper можно перенаправить на другой объект.

Вот была бы такая семантика типа, который можно менять только в конструкторе перемещения. Тогда совсем другое дело.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[3]: Ссылки и move semantics
От: Abyx Россия  
Дата: 27.07.15 19:16
Оценка: +1
Здравствуйте, _NN_, Вы писали:

_NN>Спорное утверждение.

_NN>У ссылки есть важная фишка — одноразовая инициализация.
_NN>А вот указатель и reference_wrapper можно перенаправить на другой объект.

в приватной части класса такое ограничение не нужно, инварианты можно обеспечить другими средствами.

не надо стремиться к максимальному использованию возможностей системы типов, если это мешает писать код.
это как с [signed] int vs unsigned — казалось бы, если число не может быть отрицательным, то надо использовать unsigned.
однако из за этого в коде надо постоянно писать касты чтобы успокоить компилятор.
In Zen We Trust
Re[4]: Ссылки и move semantics
От: _NN_ www.nemerleweb.com
Дата: 28.07.15 05:18
Оценка:
Здравствуйте, Abyx, Вы писали:

A>в приватной части класса такое ограничение не нужно, инварианты можно обеспечить другими средствами.

Можно конечно, однако легче становится сломать.
Функции класса стараемся помечать по возможности константными не просто так.

A>не надо стремиться к максимальному использованию возможностей системы типов, если это мешает писать код.

Я бы это списал на недостаток системы типов.

A>это как с [signed] int vs unsigned — казалось бы, если число не может быть отрицательным, то надо использовать unsigned.

A>однако из за этого в коде надо постоянно писать касты чтобы успокоить компилятор.
У меня все размеры беззнаковые числа и никаких постоянных приведений типов не замечаю.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[6]: Ссылки и move semantics
От: Videoman Россия https://hts.tv/
Дата: 05.08.15 09:09
Оценка:
Здравствуйте, watchmaker:

Извините что не среагировал на ответы т.к. скоропостижно ушел в отпуск .

Подход примерно понял. Еще раз добавлю что проблема не выдумана. Я сам не ошибусь и все проконторолирую. Но вот когда над кодом работает несколько человек, какой-нибудь коллега, запросто, может, в пылу разработки, вызвать в деструкторе free, close, unreg, unlock, или еще чего в этом роде. Вообще ссылка в данном случае ничем не отличается от указателя, со всеми его "косяками".
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.