Безопасно ли перемещать объект внутри его деструктора?

Я на 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++ и я не делаю ничего глупого?

Связанные вопросы:

Мне это кажется законным, но имейте в виду, что вы больше не можете наследовать от Request с помощью этого.

HolyBlackCat 25.07.2024 13:33

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

Cubic 25.07.2024 13:34

Это может быть проблема XY. Если вы используете std::shared_ptr, почему бы просто не добавить другого фиксированного владельца (возможно, в ваш service), и он сохранит Request в живых, даже если все остальные владельцы его освободит?

wohlstad 25.07.2024 13:36

Единственный источник странностей, который я мог себе представить, был где-то std::enable_shared_from_this, но дизайнеры библиотеки подумали об этом, поэтому для большинства непатологического кода я не вижу проблемы.

StoryTeller - Unslander Monica 25.07.2024 13:37

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

Ahmed AEK 25.07.2024 13:41

@Cubic Вы действительно хотите убедиться, что условие является ложным в состоянии перемещения. Что вы имеете в виду?

Dmytro Ovdiienko 25.07.2024 13:45

@wohlstad, почему бы просто не добавить еще одного фиксированного владельца (возможно, в вашем сервисе), и он будет поддерживать запрос как пул запросов? Мне приходится отслеживать время жизни Request, так как у меня не может быть одновременно живо более одного. Таким образом, я действительно хочу, чтобы он разрушался, когда в этом нет необходимости.

Dmytro Ovdiienko 25.07.2024 13:48

@DmytroOvdiiенко На самом деле я не имел в виду, что у вас есть пул запросов. Одновременно у вас может быть только 1 активный запрос, но с несколькими владельцами (об этом и речь std::shared_ptr). Если вам нужен только 1 владелец, вы можете использовать вместо него std::unique_ptr. Но в любом случае я не уверен, что достаточно понимаю контекст, поэтому мое предложение может быть неактуальным.

wohlstad 25.07.2024 14:01

@Cubic - после запуска деструктора объект не существует; нет ничего разумного, что можно было бы сделать с теми участниками, которые были раньше. С condition ничего делать не нужно.

Pete Becker 25.07.2024 14:03

@DmytroOvdiiенко std::make_shared<Request> -- Лучше это не throw исключение. Если это так, ваш деструктор завершит ваше приложение с исключением std::terminate.

PaulMcKenzie 25.07.2024 14:06

Возможно, вам придется беспокоиться о деструкторе «service». если ~Service() инициирует какие-либо вызовы ~Request() (например, очищая последний из Shared_ptr), то может возникнуть проблема. Возможно, вам придется убедиться, что все запросы уничтожены, прежде чем служба выйдет за рамки.

Neil Butcher 25.07.2024 14:09

Это почти гарантированно будет плохой идеей. 1) Здесь так много ошибок, что у нас возникают дебаты о том, как все делать правильно. 2) Чрезвычайно удивительно, что объект копирует себя в своем деструкторе до такой степени, что это может само по себе вызвать бесконечный цикл. 3) Какую бы проблему вы ни решали, это похоже на непонимание того, каким должно быть время жизни объекта.

Passer By 25.07.2024 14:10

@PeteBecker не после деструктора, но ничто не мешает вам переместить объект до его уничтожения...

Cubic 25.07.2024 14:12

@Cubic - но вопрос в том, как переместить объект в его деструкторе.

Pete Becker 25.07.2024 14:14

Срок жизни объекта заканчивается, когда... если он относится к типу класса, начинается вызов деструктора... Итак, вы формально перемещаете объект, который больше не существует.

Oersted 25.07.2024 14:21

Это, безусловно, удивительный код, который во многом напоминает решение проблемы дизайна. Это не то, чего я ожидал бы от обычного кода. Если бы это был обзор кода, я бы потратил больше времени на понимание того, ЧТО происходит, и на дизайн, а не на этот хрупкий фрагмент кода.

Pepijn Kramer 25.07.2024 14:22

@PepijnKramer Время жизни Request неясно. Вот почему используется std::shared_ptr. Когда я отписываюсь, есть вероятность, что мой объект какое-то короткое время все еще будет использоваться внутри сервиса, и это совершенно нормально. Проблема в том, что я не могу создать еще один запрос, пока предыдущий не будет уничтожен. В противном случае мне придется решать проблемы синхронизации потоков. Я думаю, что это довольно распространенная проблема, люди просто не осознают, что у них может возникнуть гонка данных. В противном случае им придется предоставлять достаточно строгие гарантии синхронизации, что снижает производительность.

Dmytro Ovdiienko 25.07.2024 14:30

Обычно я хорошо осведомлен о гонках данных и вызовах объекта, который также может быть уничтожен другими потоками. Один из возможных шаблонов — передатьshared_ptr объекту, который вы вызываете (например, запросу), в лямбда-выражение и позволить ему продлить время жизни объекта на время вызова. Но я не знаю, впишется ли это как-то в ваш замысел.

Pepijn Kramer 25.07.2024 15:30

@AhmedAEK лучше поместить аналогичную логику в средство удаления общего указателя. Это хороший ответ. Мне не очень нравится идея сделать удаляющую сторону повторной подпиской, поскольку я считаю, что удаляющая сторона должна удалять (или повторно использовать), но не переподписываться. Однако, по моему мнению, ваше решение в целом делает код лучше и решает проблему, упомянутую @HolyBlackCat о невозможности наследования от Request. Поэтому, если вы добавите свой ответ, я тоже проголосую за него. Меня беспокоит только то, что для реализации этого решения мне придется иметь дело с необработанными указателями, которых я бы по возможности избегал.

Dmytro Ovdiienko 25.07.2024 16:27

@PepijnKramer передаетshared_ptr объекту, который вы вызываете (например, запрос), в лямбду: я не уверен, что понимаю эту идею. Не могли бы вы сделать небольшой фрагмент?

Dmytro Ovdiienko 25.07.2024 16:40

Как уже было сказано, я не знаю, близко ли это к вашему варианту использования, но вот что я имею в виду в минимальном примере кода: onlinegdb.com/k3dOu0OLW. (Лямбда-выражения действительно можно использовать творчески и выполнять большую тяжелую работу, иногда вам просто нужно думать так, чтобы вы думали функциями, а не объектами)

Pepijn Kramer 25.07.2024 17:59

@PepijnKramer Понятно. По сути, именно так это работает в реальном коде, и именно поэтому время жизни запроса не является детерминированным, когда я хочу отказаться от подписки. Stackoverflow хочет, чтобы я перенес обсуждение в чат. Вроде тема понятна и обсуждать больше нечего. Ребята, еще раз спасибо всем за помощь.

Dmytro Ovdiienko 25.07.2024 18:39

Пожалуйста. Удачи

Pepijn Kramer 25.07.2024 19:08
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
23
299
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

В конце концов, строка 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 в комментарии, деструкторы были бы фактически бесполезны).

Из фрагмента невозможно определить, нет ли других проблем (см. комментарии к вопросу).

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

Dmytro Ovdiienko 25.07.2024 14:36

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

Dmytro Ovdiienko 25.07.2024 14:37

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

Oersted 25.07.2024 14:41

Разрешено использовать объект во время его уничтожения, даже если это время истекло (см. class.cdtor и Basic.life#6). Если бы это было не так, деструкторы не смогли бы ничего сделать.

interjay 25.07.2024 14:46

Другое прочтение могло бы заключаться в том, что в некоторых случаях применяется 1,4, в других - 1,5, но я не понимаю этого в формулировке пожизненного срока.

Oersted 25.07.2024 14:46

@interjay Если бы это было не так, деструкторы, конечно, не смогли бы ничего сделать... Однако в формулировке четко указано, что запрещено в начале timsong-cpp.github.io/cppwp/n4861/class. cdtor, но неясно, что является законным: «можно использовать, но только ограниченными способами». В конце концов я согласен с вами и поэтому удалю свой ответ или исправлю его, если вы будете так любезны, разрешите мне повторно использовать ваш «материал».

Oersted 25.07.2024 14:53

@Oersted Потому что, если объект встроен в другой, его деструктор вызывается после встраивания, а это означает, что встроенный объект переживет встраивание? Да, вы правильно поняли

Dmytro Ovdiienko 25.07.2024 15:52

Короткий ответ: да. Уничтожение объекта и освобождение памяти объекта происходит только после чистого завершения работы деструктора.

Однако я бы использовал в деструкторе мьютекс, локальный для объекта, чтобы гарантировать, что один и тот же деструктор не будет вызываться дважды, если это разрешено компилятором. Я считаю, что более поздние версии C++ не допускают одновременного уничтожения, но некоторые более ранние версии позволяли.

Операции внутри деструктора должны быть доступны только для чтения для объекта, чтобы не нарушать какие-либо правила доступа, установленные для членов до вызова деструктора.

Копирование самого объекта должно быть допустимым, если только не вызываются функции-члены, которые могут нарушить правило только для чтения.

Что вы подразумеваете под объектно-локальным мьютексом и одновременным уничтожением?

Dmytro Ovdiienko 25.07.2024 16:14

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

Dmytro Ovdiienko 25.07.2024 16:15

Я думаю, ничего страшного, если вы уверены, что исключений не возникнет, или используете try/catch(…)

Другие вопросы по теме