Я на 99% уверен, что ответ на мой вопрос — «Да». Я просто хочу дважды проверить правильность своего предположения и быть уверенным, что в будущих стандартах C++ нет ничего, что могло бы изменить правила (например, деструктивный ход, если это действительно возможно).
Итак, мой случай следующий. У меня есть объект типа Request
, срок жизни которого управляется std::shared_ptr<Request>
. Когда объект вот-вот умрет (т. е. счетчик ссылок равен нулю и std::shared_ptr
вызывает деструктор ~Request
), мне нужно проверить какое-то условие и создать еще один экземпляр типа Request
с точно такими же данными. По сути мне просто нужно продлить подписку.
Самый дешевый способ, с моей точки зрения, — это просто вызвать оператор перемещения класса Request
:
Request::~Request()
{
if (condition)
{
service.subscribe(std::make_shared<Request>(std::move(*this)));
}
}
Насколько мне известно, все члены данных экземпляра Request
в контексте деструктора все еще живы. Они еще не уничтожены. Значит, их нужно куда-то переместить, верно? Соответствует ли мое предположение идее C++ и я не делаю ничего глупого?
Связанные вопросы:
Вы действительно хотите убедиться, что condition
является ложным в состоянии перемещения, иначе вы создадите здесь некоторые проблемы.
Это может быть проблема XY. Если вы используете std::shared_ptr
, почему бы просто не добавить другого фиксированного владельца (возможно, в ваш service
), и он сохранит Request
в живых, даже если все остальные владельцы его освободит?
Единственный источник странностей, который я мог себе представить, был где-то std::enable_shared_from_this
, но дизайнеры библиотеки подумали об этом, поэтому для большинства непатологического кода я не вижу проблемы.
хотя это выглядит нормально, я думаю, вам лучше поместить аналогичную логику в средство удаления общего указателя вместо деструктора самого объекта, ваш текущий объект действительно сбивает с толку в использовании и не наследуется, в то время как переопределение средства удаления общего/ У уникального указателя таких проблем нет, тогда вам не нужно будет выходить из него, а просто перемонтировать указатель.
@Cubic Вы действительно хотите убедиться, что условие является ложным в состоянии перемещения. Что вы имеете в виду?
@wohlstad, почему бы просто не добавить еще одного фиксированного владельца (возможно, в вашем сервисе), и он будет поддерживать запрос как пул запросов? Мне приходится отслеживать время жизни Request
, так как у меня не может быть одновременно живо более одного. Таким образом, я действительно хочу, чтобы он разрушался, когда в этом нет необходимости.
@DmytroOvdiiенко На самом деле я не имел в виду, что у вас есть пул запросов. Одновременно у вас может быть только 1 активный запрос, но с несколькими владельцами (об этом и речь std::shared_ptr
). Если вам нужен только 1 владелец, вы можете использовать вместо него std::unique_ptr
. Но в любом случае я не уверен, что достаточно понимаю контекст, поэтому мое предложение может быть неактуальным.
@Cubic - после запуска деструктора объект не существует; нет ничего разумного, что можно было бы сделать с теми участниками, которые были раньше. С condition
ничего делать не нужно.
@DmytroOvdiiенко std::make_shared<Request>
-- Лучше это не throw
исключение. Если это так, ваш деструктор завершит ваше приложение с исключением std::terminate
.
Возможно, вам придется беспокоиться о деструкторе «service». если ~Service() инициирует какие-либо вызовы ~Request() (например, очищая последний из Shared_ptr), то может возникнуть проблема. Возможно, вам придется убедиться, что все запросы уничтожены, прежде чем служба выйдет за рамки.
Это почти гарантированно будет плохой идеей. 1) Здесь так много ошибок, что у нас возникают дебаты о том, как все делать правильно. 2) Чрезвычайно удивительно, что объект копирует себя в своем деструкторе до такой степени, что это может само по себе вызвать бесконечный цикл. 3) Какую бы проблему вы ни решали, это похоже на непонимание того, каким должно быть время жизни объекта.
@PeteBecker не после деструктора, но ничто не мешает вам переместить объект до его уничтожения...
@Cubic - но вопрос в том, как переместить объект в его деструкторе.
Срок жизни объекта заканчивается, когда... если он относится к типу класса, начинается вызов деструктора... Итак, вы формально перемещаете объект, который больше не существует.
Это, безусловно, удивительный код, который во многом напоминает решение проблемы дизайна. Это не то, чего я ожидал бы от обычного кода. Если бы это был обзор кода, я бы потратил больше времени на понимание того, ЧТО происходит, и на дизайн, а не на этот хрупкий фрагмент кода.
@PepijnKramer Время жизни Request
неясно. Вот почему используется std::shared_ptr
. Когда я отписываюсь, есть вероятность, что мой объект какое-то короткое время все еще будет использоваться внутри сервиса, и это совершенно нормально. Проблема в том, что я не могу создать еще один запрос, пока предыдущий не будет уничтожен. В противном случае мне придется решать проблемы синхронизации потоков. Я думаю, что это довольно распространенная проблема, люди просто не осознают, что у них может возникнуть гонка данных. В противном случае им придется предоставлять достаточно строгие гарантии синхронизации, что снижает производительность.
Обычно я хорошо осведомлен о гонках данных и вызовах объекта, который также может быть уничтожен другими потоками. Один из возможных шаблонов — передатьshared_ptr объекту, который вы вызываете (например, запросу), в лямбда-выражение и позволить ему продлить время жизни объекта на время вызова. Но я не знаю, впишется ли это как-то в ваш замысел.
@AhmedAEK лучше поместить аналогичную логику в средство удаления общего указателя. Это хороший ответ. Мне не очень нравится идея сделать удаляющую сторону повторной подпиской, поскольку я считаю, что удаляющая сторона должна удалять (или повторно использовать), но не переподписываться. Однако, по моему мнению, ваше решение в целом делает код лучше и решает проблему, упомянутую @HolyBlackCat о невозможности наследования от Request
. Поэтому, если вы добавите свой ответ, я тоже проголосую за него. Меня беспокоит только то, что для реализации этого решения мне придется иметь дело с необработанными указателями, которых я бы по возможности избегал.
@PepijnKramer передаетshared_ptr объекту, который вы вызываете (например, запрос), в лямбду: я не уверен, что понимаю эту идею. Не могли бы вы сделать небольшой фрагмент?
Как уже было сказано, я не знаю, близко ли это к вашему варианту использования, но вот что я имею в виду в минимальном примере кода: onlinegdb.com/k3dOu0OLW. (Лямбда-выражения действительно можно использовать творчески и выполнять большую тяжелую работу, иногда вам просто нужно думать так, чтобы вы думали функциями, а не объектами)
@PepijnKramer Понятно. По сути, именно так это работает в реальном коде, и именно поэтому время жизни запроса не является детерминированным, когда я хочу отказаться от подписки. Stackoverflow хочет, чтобы я перенес обсуждение в чат. Вроде тема понятна и обсуждать больше нечего. Ребята, еще раз спасибо всем за помощь.
Пожалуйста. Удачи
В конце концов, строка service.subscribe(std::make_shared<Request>(std::move(*this)));
может быть допустимой, потому что:
Время жизни объекта o типа T заканчивается, когда:
(1,3) если T не является классовым типом, объект уничтожается, или
(1,4) если T — тип класса, начинается вызов деструктора, или
(1,5) память, которую занимает объект, освобождается или повторно используется объектом, который не вложен в o
Я подчеркнул соответствующую строку.
https://timsong-cpp.github.io/cppwp/n4861/basic.life#1.4
Однако, несколькими строками позже:
6 До начала жизни объекта, но после того, как была выделена память, которую будет занимать объект, или после того, как время существования объекта закончилось и до того, как память, которую занимал объект, будет повторно использована или освобождена, любой указатель, представляющий адрес Место хранения, где будет или находился объект, может быть использовано, но лишь ограниченно. Информацию о строящемся или разрушаемом объекте см. в [class.cdtor].
https://timsong-cpp.github.io/cppwp/n4861/basic.life#6
(еще раз подчеркну — мое)
По ссылке [class.cdtor]:
Для объекта с нетривиальным деструктором обращение к любому нестатическому члену или базовому классу объекта после завершения выполнения деструктора приводит к неопределенному поведению.
https://timsong-cpp.github.io/cppwp/n4861/class.cdtor#1
Напротив, мы можем понимать, что другое использование является законным (в противном случае, как заявил @interjay в комментарии, деструкторы были бы фактически бесполезны).
Из фрагмента невозможно определить, нет ли других проблем (см. комментарии к вопросу).
Возможно, речь идет больше о том, когда заканчивается срок службы объекта. Внутри деструктора вы все еще можете получить доступ к элементам данных, и время их жизни еще не закончилось, поскольку их деструктор не вызывается (пока).
По сути, я могу создать копию объекта вместо его перемещения. Я просто подумал, что перемещение обходится дешевле, чем копирование, и внутри деструктора мне все равно не нужны элементы данных.
Я согласен, что здесь есть некоторое противоречие. Потому что, если объект встроен в другой, его деструктор вызывается после встраивания, а это означает, что встроенный объект переживет встраивание? Я что-то пропустил?
Разрешено использовать объект во время его уничтожения, даже если это время истекло (см. class.cdtor и Basic.life#6). Если бы это было не так, деструкторы не смогли бы ничего сделать.
Другое прочтение могло бы заключаться в том, что в некоторых случаях применяется 1,4, в других - 1,5, но я не понимаю этого в формулировке пожизненного срока.
@interjay Если бы это было не так, деструкторы, конечно, не смогли бы ничего сделать... Однако в формулировке четко указано, что запрещено в начале timsong-cpp.github.io/cppwp/n4861/class. cdtor, но неясно, что является законным: «можно использовать, но только ограниченными способами». В конце концов я согласен с вами и поэтому удалю свой ответ или исправлю его, если вы будете так любезны, разрешите мне повторно использовать ваш «материал».
@Oersted Потому что, если объект встроен в другой, его деструктор вызывается после встраивания, а это означает, что встроенный объект переживет встраивание? Да, вы правильно поняли
Короткий ответ: да. Уничтожение объекта и освобождение памяти объекта происходит только после чистого завершения работы деструктора.
Однако я бы использовал в деструкторе мьютекс, локальный для объекта, чтобы гарантировать, что один и тот же деструктор не будет вызываться дважды, если это разрешено компилятором. Я считаю, что более поздние версии C++ не допускают одновременного уничтожения, но некоторые более ранние версии позволяли.
Операции внутри деструктора должны быть доступны только для чтения для объекта, чтобы не нарушать какие-либо правила доступа, установленные для членов до вызова деструктора.
Копирование самого объекта должно быть допустимым, если только не вызываются функции-члены, которые могут нарушить правило только для чтения.
Что вы подразумеваете под объектно-локальным мьютексом и одновременным уничтожением?
Операции внутри деструктора должны быть доступны только для чтения объекта. Ну... В деструкторе вы можете делать что угодно, кроме выдачи исключений. В деструкторе все элементы данных находятся в допустимом состоянии.
Я думаю, ничего страшного, если вы уверены, что исключений не возникнет, или используете try/catch(…)
Мне это кажется законным, но имейте в виду, что вы больше не можете наследовать от
Request
с помощью этого.