Добрый день.
В процессе реализации move конструктора возник вопрос который интернет "тактично" обходит.
Есть некая обертка, которая принимает ссылку на некий объект. Обертка не копируется и рассчитана на работу только в текущей области видимости. Теперь допустим мне необходимо создать вектор оберток. При создании вектора как раз и используется move конструктор.
Теперь проблема: при перемещении членов обертки, внутри конструктора, ссылку занулить невозможно.
Вопрос к знатокам:
Как принято действовать в этом случае и вообще кокой best practice для таких классов?
Какие есть варианты:
1. Вместо ссылок использовать указатели
2. Забить и оставить валидную ссылку в объекте который уже перемещен
3. Что-то еще
Что смущает:
После перемещения, теоретически, можно нечаянно через методы объекта который уже перемещен изменить объект на который осталась ссылка.
Здравствуйте, 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. Что-то еще
Например, дополнительный флаг в объекте «я_мёртв».
Но, в общем, первые два варианта самые подходящие. И если так уж не доверяешь пользователю обёртки, то выбирай реализацию через указатели.
Здравствуйте, watchmaker, Вы писали:
W>Как же так? Задаёшь поисковику запрос «Reference data members and move constructor» и изучаешь.
Смотрел, это первая ссылка в Google. К сожалению не совсем из нее понятно как правильно. Это просто такое же обсуждение проблемы на форуме. Может есть какая-то авторитетная статья по данной проблеме?
W>Так и требования чего-то там занулять в move-конструкторе нет. После его работы экземпляр-источник должен остаться в консинстентном состоянии. Например, move-конструктор иногда через swap реализуется — очевидно тут два объекта просто меняются данными, а не зануляют что-то. Так и наличие ссылки в экземпляр-источнике на какой-то объект само по себе не приведёт проблемам пока, как ты заметил, через эту ссылку с объектом не начнут работать несогласованно из нескольких мест.
Понятно что требований таких в C++ быть не может — на каждый случай своё решение. В том то и дело, что они оба, при таком подходе, легко могут оказаться в неконсистентном состоянии, так как я не планировал менять один и тот же объект из двух разных мест. Я не хочу оставлять на усмотрение внешнего кода, будут ли работать с уже перемещенным объектом. Тем более, что вы сами написали что источник должен остаться в консистентном состоянии, т.е. с ним могут начать работать.
W>Да, но по сути такой же риск есть и для всех других классов после их перемещения. Наличие ссылки тут не делает проблему новой. Хотя, конечно, если использовать вместо ссылок указатели, то можно присваивать им nullptr и падать в segfault при попытке разыменования, что поможет отладке.
У классов без ссылок я могу переинициализировать члены — я так и делаю.
W>Но лучше просто следовать правилу «не использовать объект после std::move». Нарушил — сам виноват.
Ну.... по-моему это противоречит утверждению, что объект должен оставаться в консистентном состоянии. Я не люблю следить за чем, за чем можно не следить.
Здравствуйте, 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>У классов без ссылок я могу переинициализировать члены — я так и делаю.
Ясно, а зачем? Использовал обёртку и выкинул, вместо этого создал другую. Такой вариант тебе не подходит? Ну тогда используй просто указатели вместо ссылок — и вопрос решён.
Здравствуйте, 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-ов).
Теперь придется везде добавлять проверки. Я правильно понимаю?
Непонятно, что вас так смущает.
Ну да, в старом экземпляре останется ссылка. А также все константные и прочие non-movable поля.
Если вам хочется дополнительно защититься от повторного использования перемещенного объекта то вместо настоящей ссылки в нем можно использовать optional_reference. Или сделать дебаг флаг "moved", assert(!moved) в каждом методе и выставлять его в true при перемещении.
Но обычно это не нужно, так как объекты перемещаются чаще всего либо в нутрях контейнеров либо при возвращении из методов.
Говорить дальше не было нужды. Как и все космонавты, капитан Нортон не испытывал особого доверия к явлениям, внешне слишком заманчивым.
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).
Здравствуйте, Videoman, Вы писали:
V>Есть некая обертка, которая принимает ссылку на некий объект.
не делайте ссылки членами класса, храните указатели.
в private части класса достаточно инкапсуляции чтобы указатель не создавал проблем.
если не нравятся указатели — есть std::reference_wrapper, но лучше просто указатель.
V>Теперь проблема: при перемещении членов обертки, внутри конструктора, ссылку занулить невозможно.
при перемещении ничего не надо удалять.
для объекта-источника надо гарантировать только разрушение/присваивание.
Здравствуйте, Abyx, Вы писали:
A>Здравствуйте, Videoman, Вы писали:
V>>Есть некая обертка, которая принимает ссылку на некий объект.
A>не делайте ссылки членами класса, храните указатели. A>в private части класса достаточно инкапсуляции чтобы указатель не создавал проблем.
Спорное утверждение.
У ссылки есть важная фишка — одноразовая инициализация.
А вот указатель и reference_wrapper можно перенаправить на другой объект.
Вот была бы такая семантика типа, который можно менять только в конструкторе перемещения. Тогда совсем другое дело.
Здравствуйте, _NN_, Вы писали:
_NN>Спорное утверждение. _NN>У ссылки есть важная фишка — одноразовая инициализация. _NN>А вот указатель и reference_wrapper можно перенаправить на другой объект.
в приватной части класса такое ограничение не нужно, инварианты можно обеспечить другими средствами.
не надо стремиться к максимальному использованию возможностей системы типов, если это мешает писать код.
это как с [signed] int vs unsigned — казалось бы, если число не может быть отрицательным, то надо использовать unsigned.
однако из за этого в коде надо постоянно писать касты чтобы успокоить компилятор.
Здравствуйте, Abyx, Вы писали:
A>в приватной части класса такое ограничение не нужно, инварианты можно обеспечить другими средствами.
Можно конечно, однако легче становится сломать.
Функции класса стараемся помечать по возможности константными не просто так.
A>не надо стремиться к максимальному использованию возможностей системы типов, если это мешает писать код.
Я бы это списал на недостаток системы типов.
A>это как с [signed] int vs unsigned — казалось бы, если число не может быть отрицательным, то надо использовать unsigned. A>однако из за этого в коде надо постоянно писать касты чтобы успокоить компилятор.
У меня все размеры беззнаковые числа и никаких постоянных приведений типов не замечаю.
Извините что не среагировал на ответы т.к. скоропостижно ушел в отпуск .
Подход примерно понял. Еще раз добавлю что проблема не выдумана. Я сам не ошибусь и все проконторолирую. Но вот когда над кодом работает несколько человек, какой-нибудь коллега, запросто, может, в пылу разработки, вызвать в деструкторе free, close, unreg, unlock, или еще чего в этом роде. Вообще ссылка в данном случае ничем не отличается от указателя, со всеми его "косяками".