shared_ptr 成员的 C++ 析构函数顺序
C++ destructor order of member with shared_ptr
如果classA
包含classB
,那么在A
析构时,会先调用B
的析构函数,即,它们嵌套关系的相反顺序。
但是如果 A
包含 B
的 shared_ptr
,而 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
的析构函数,并阻塞直到完成。
输出将是:
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;
输出还是一样,没有段错误。
B 有一个指向 A 的指针,但没有释放它的内存(例如没有删除)。所以删除了指针但没有分配内存,这一切都很好。
基本上,指针在堆栈上,它包含堆上一些(假定的)已分配内存的地址。是的,它从堆栈中移除,但分配的内存仍然存在。这就是 delete
的用途。删除堆上分配的内存。但是,在您的情况下,您不希望删除该内存,而您的指针就是我们所说的非拥有指针。它指向某物,但不负责清理(实际上 B 不拥有指针指向的内存)。
If class A contains class B, then when A destruct, B's destructor will be called first, i.e., the reversed order of their nested relationship.
没有。如果您销毁类型为 A
的对象,它会调用 A
的析构函数,因此它会先被调用。
然而,调用 B
的析构函数将首先完成: A
的析构函数首先执行析构函数体,然后继续销毁子对象。析构函数体首先完成,然后是子对象的析构函数,最后 A
的析构函数将完成。
But what if A contains a shared_ptr of B, while B contains a raw pointer to A, how should we handle the destructor to make it safe?
在 A
的析构函数体中,将指向的 B
指向除被销毁对象之外的其他地方:
~A() {
b->a = nullptr;
}
如果您将它指向 null,例如在我显示的示例中,那么您还必须确保 B
可以处理 B::a
可能为 null 的情况,即在通过指针访问之前进行检查.
it seems that A's destructor finished without waiting B to destruct.
这不是我们观察到的。 A
的析构函数的 body 已完成,但析构函数要等到成员析构函数先完成后才能完成。
准确了解正在发生的事情很重要。当 A
被销毁时,以下事件按以下顺序发生:
A::~A()
被调用。
A
对象的生命周期结束。该对象仍然存在,但已不在其生命周期内。 ([basic.life]/1.3)
A::~A()
的主体被执行。
A::~A()
以反向声明顺序隐式调用 A
的直接非静态成员的析构函数 ([class.dtor]/9, [class.base.init]/13.3 )
A::~A()
returns.
A
对象不复存在 ([class.dtor]/16)。它曾经占用的内存变为 "allocated storage" ([basic.life]/6),直到它被释放。
(所有引用均针对 C++17 标准)。
在析构函数的第二个版本中:
~A() {
std::cout << "A destruct done" << std::endl;
}
语句打印后,成员b
被销毁,导致拥有的B
对象被销毁。此时,i
尚未被销毁,因此可以安全访问它。之后,B
析构函数returns。然后,i
是 "destroyed"(请参阅 CWG 2256 了解一些细微之处)。最后是A
returns的析构函数。到那时,尝试访问成员 i
.
将不再合法
只是想指出您的评论不正确:
~A() {
std::cout << "A destruct done" << std::endl;
}
当你离开花括号时,析构函数完成。您可以在调试器中看到,一步一步地进行。那就是 b
将被删除的地方。
如果classA
包含classB
,那么在A
析构时,会先调用B
的析构函数,即,它们嵌套关系的相反顺序。
但是如果 A
包含 B
的 shared_ptr
,而 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
的析构函数,并阻塞直到完成。
输出将是:
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;
输出还是一样,没有段错误。
B 有一个指向 A 的指针,但没有释放它的内存(例如没有删除)。所以删除了指针但没有分配内存,这一切都很好。
基本上,指针在堆栈上,它包含堆上一些(假定的)已分配内存的地址。是的,它从堆栈中移除,但分配的内存仍然存在。这就是 delete
的用途。删除堆上分配的内存。但是,在您的情况下,您不希望删除该内存,而您的指针就是我们所说的非拥有指针。它指向某物,但不负责清理(实际上 B 不拥有指针指向的内存)。
If class A contains class B, then when A destruct, B's destructor will be called first, i.e., the reversed order of their nested relationship.
没有。如果您销毁类型为 A
的对象,它会调用 A
的析构函数,因此它会先被调用。
然而,调用 B
的析构函数将首先完成: A
的析构函数首先执行析构函数体,然后继续销毁子对象。析构函数体首先完成,然后是子对象的析构函数,最后 A
的析构函数将完成。
But what if A contains a shared_ptr of B, while B contains a raw pointer to A, how should we handle the destructor to make it safe?
在 A
的析构函数体中,将指向的 B
指向除被销毁对象之外的其他地方:
~A() {
b->a = nullptr;
}
如果您将它指向 null,例如在我显示的示例中,那么您还必须确保 B
可以处理 B::a
可能为 null 的情况,即在通过指针访问之前进行检查.
it seems that A's destructor finished without waiting B to destruct.
这不是我们观察到的。 A
的析构函数的 body 已完成,但析构函数要等到成员析构函数先完成后才能完成。
准确了解正在发生的事情很重要。当 A
被销毁时,以下事件按以下顺序发生:
A::~A()
被调用。A
对象的生命周期结束。该对象仍然存在,但已不在其生命周期内。 ([basic.life]/1.3)A::~A()
的主体被执行。A::~A()
以反向声明顺序隐式调用A
的直接非静态成员的析构函数 ([class.dtor]/9, [class.base.init]/13.3 )A::~A()
returns.A
对象不复存在 ([class.dtor]/16)。它曾经占用的内存变为 "allocated storage" ([basic.life]/6),直到它被释放。
(所有引用均针对 C++17 标准)。
在析构函数的第二个版本中:
~A() {
std::cout << "A destruct done" << std::endl;
}
语句打印后,成员b
被销毁,导致拥有的B
对象被销毁。此时,i
尚未被销毁,因此可以安全访问它。之后,B
析构函数returns。然后,i
是 "destroyed"(请参阅 CWG 2256 了解一些细微之处)。最后是A
returns的析构函数。到那时,尝试访问成员 i
.
只是想指出您的评论不正确:
~A() {
std::cout << "A destruct done" << std::endl;
}
当你离开花括号时,析构函数完成。您可以在调试器中看到,一步一步地进行。那就是 b
将被删除的地方。