C ++ порядок деструкторов члена с shared_ptr - PullRequest
1 голос
/ 12 февраля 2020

Если класс A содержит класс B, то при уничтожении A сначала будет вызван деструктор B, т. Е. Обратный порядок их вложенных отношений.

Но что если A содержит shared_ptr из B, а B содержит необработанный указатель на A, как нам обращаться с деструктором, чтобы сделать его безопасным?

Рассматривая следующий пример:

#include <iostream>
#include <memory>
#include <unistd.h>

struct B;
struct A {
  int i = 1;
  std::shared_ptr<B> b;

  A() : b(std::make_shared<B>(this)) {}

  ~A() {
    b = nullptr;
    std::cout << "A destruct done" << std::endl;
  }
};

struct B {
  A *a;

  B(A *aa) : a(aa) {}

  ~B() {
    usleep(2000000);
    std::cout << "value in A: " << a->i << std::endl;
    std::cout << "B destruct done" << std::endl;
  }
};

int main() {
  std::cout << "Hello, World!" << std::endl;
  {
    A a;
  }
  std::cout << "done\n";
  return 0;
}

Вы можете видеть в деструкторе A, я явно установил b на nullptr, что немедленно вызовет деструктор B и блокирует его до конца sh. Вывод будет:

Hello, World!
value in A: 1
B destruct done
A destruct done
done

, но если я закомментирую эту строку

  ~A() {
    // b = nullptr; // <---
    std::cout << "A destruct done" << std::endl;
  }

Вывод будет:

Hello, World!
A destruct done
value in A: 1
B destruct done
done

кажется, что A Деструктор закончил, не дожидаясь разрушения B. Но в этом случае я ожидал ошибки сегмента, поскольку, когда A уже уничтожен, B пытался получить доступ к члену A, что недопустимо. Но почему программа не выдает ошибку сегмента? Это нормально (т.е. undefined behavior)?

Кроме того, когда я изменяю

 {
    A a;
 }

на

  A * a = new A();
  delete a;

, выход остается прежним , нет ошибки сегмента.

Ответы [ 4 ]

3 голосов
/ 12 февраля 2020

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

  • A::~A().
  • Время жизни объекта A заканчивается. Объект все еще существует, но больше не существует. ([basi c .life] /1.3)
  • Тело A::~A() выполнено.
  • A::~A() неявно вызывает деструкторы непосредственных нестационарных c членов A в обратном порядке объявления ([class.dtor] / 9, [class.base.init] /13.3)
  • A::~A() возвращает.
  • Объект A прекращает существует ([class.dtor] / 16). Память, которую он занимал, становится «выделенной памятью» ([basi c .life] / 6) до тех пор, пока она не будет освобождена.

(Все ссылки относятся к стандарту C ++ 17) .

Во второй версии деструктора:

~A() {
    std::cout << "A destruct done" << std::endl;
}

после того, как оператор напечатан, член b уничтожается, что приводит к уничтожению принадлежащего объекта B. На данный момент, i еще не был уничтожен, поэтому доступ к нему безопасен. После этого деструктор B возвращается. Затем i «уничтожается» (см. CWG 2256 для некоторых тонкостей). Наконец, деструктор A возвращается. В этот момент больше не будет законным пытаться получить доступ к члену i.

1 голос
/ 13 февраля 2020

Просто хотел указать, что ваш комментарий неверен:

~A() {
  std::cout << "A destruct done" << std::endl;
}

Деструктор ГОТОВ , когда вы выйдете из фигурных скобок. Вы можете увидеть это в отладчике, делая шаг за шагом. Вот где b будет удалено.

1 голос
/ 12 февраля 2020

Если класс A содержит класс B, то при разрушении A сначала будет вызван деструктор B, т. Е. Обратный порядок их вложенных отношений.

Нет. Если вы уничтожите объект типа A, который вызывает деструктор A, то есть он вызывается первым.

Однако вызов деструктора B сначала завершится sh: Деструктор A начинается с выполнения тела деструктора, а затем переходит к уничтожению подчиненных объектов. Сначала завершается тело деструктора, затем деструкторы подобъектов и, наконец, деструктор A будет завершен.

Но что, если A содержит shared_ptr из B, а B содержит необработанный указатель на A, как мы должны обращаться с деструктором, чтобы сделать его безопасным?

В теле деструктора A, сделайте указанную B точку где-нибудь еще, кроме объекта, который разрушается:

~A() {
    b->a = nullptr;
}

Если вы укажете на ноль, как в моем показанном примере, то вы также должны убедиться, что B может обработать ситуацию, в которой B::a может быть нулем, т.е. проверить перед доступом через указатель.

кажется, что деструктор А закончил, не дожидаясь, пока Б разрушится.

Это не то, что мы наблюдаем. тело деструктора A завершено, но деструктор не завершает sh, пока деструктор-член не завершится sh first.

1 голос
/ 12 февраля 2020

B имеет указатель на A, но не освобождает память о нем (например, не удалять). Таким образом, указатель удаляется, но не выделяется память, что вполне нормально.

По сути, указатель находится в стеке и содержит адрес некоторой (предполагаемой) выделенной памяти в куче. Да, он удаляется из стека, но выделенная память остается. Вот для чего delete. Убрать выделенную память в куче. Однако в вашем случае вы не хотите, чтобы эта память была удалена, а ваш указатель - это то, что мы называем не владеющим указателем. Он указывает на что-то, но не несет ответственности за очистку (на самом деле B не владеет памятью, на которую указывает указатель).

...