std::unique_ptr、pimpl 和对象生命周期
std::unique_ptr, pimpl and object lifetime
以下示例使用 Linux (GNU STL) 上的 gcc 11 和 FreeBSD (Clang STL) 上的 clang 12 进行编译。在 Linux 上,它 运行s 并打印值 1 和 2。在 FreeBSD 上,它打印值 1,然后使用 SEGV 崩溃。我 相当 理解对象的生命周期——所以整个事情可能是 UB,运行time 行为可能不相关。我知道这两个 STL 之间 std::unique_ptr
的 实现 有一个重要的不同:Clang STL 将 std::unique_ptr
的内部指针重置为 nullptr
在析构函数的开头,而 GNU STL 单独保留指针。
#include <iostream>
#include <memory>
struct C {
struct Private {
C* m_owner;
int m_x;
Private(C* owner) : m_owner(owner), m_x(0) {}
~Private() { m_owner->cleanup(); }
void cleanup() { std::cout << "Private x=" << ++m_x << '\n'; }
};
std::unique_ptr<Private> d;
C() { d = std::make_unique<Private>(this); }
~C() = default;
void cleanup() { d->cleanup(); }
};
int main(int argc, char **argv)
{
C c;
c.cleanup(); // For display purposes, print 1
return 0; // Destructors called, print 2
}
FreeBSD 上的输出:
Private x=1
Segmentation fault (core dumped)
和一段回溯:
* thread #1, name = 'a.out', stop reason = signal SIGSEGV: invalid address (fault address: 0x8)
frame #0: 0x00000000002032b4 a.out`C::Private::cleanup() + 52
a.out`C::Private::cleanup:
-> 0x2032b4 <+52>: movl 0x8(%rax), %esi
我认为这可能是 UB 的原因是:
- 在
return 0
,c
的生命周期即将结束。
- 析构函数
~C()
运行s。一旦析构函数的主体(默认)完成,对象的生命周期结束并且使用该对象是 UB。
- 现在是对象 运行.
的子对象(成员对象?)的析构函数
- 析构函数
~std::unique_ptr<Private>
运行s。它 运行 是持有对象的析构函数。
- 析构函数
~Private()
使用指向不再存在的对象的指针m_owner
调用成员函数。
如果您能指出这种对对象生命周期的理解是否正确,我将不胜感激。
如果它不是 UB,那么就会有一个单独的实现质量问题(或者我应该在调用它的方法之前检查 d 指针,但这对于 pimpl 来说似乎有点愚蠢;然后我们得到 if(d)d->cleanup()
这是一个 STL 实现所需要的,而在另一个实现中是无用的检查。
为了提出一个问题:在销毁对象 c
期间,此代码是否在语句 m_owner->cleanup()
(第 9 行)中显示 UB?
是的,m_owner
引用的对象的生命周期已经结束,并且在调用 m_owner->cleanup();
时它的析构函数调用已完成。因此调用是 UB。
以下示例使用 Linux (GNU STL) 上的 gcc 11 和 FreeBSD (Clang STL) 上的 clang 12 进行编译。在 Linux 上,它 运行s 并打印值 1 和 2。在 FreeBSD 上,它打印值 1,然后使用 SEGV 崩溃。我 相当 理解对象的生命周期——所以整个事情可能是 UB,运行time 行为可能不相关。我知道这两个 STL 之间 std::unique_ptr
的 实现 有一个重要的不同:Clang STL 将 std::unique_ptr
的内部指针重置为 nullptr
在析构函数的开头,而 GNU STL 单独保留指针。
#include <iostream>
#include <memory>
struct C {
struct Private {
C* m_owner;
int m_x;
Private(C* owner) : m_owner(owner), m_x(0) {}
~Private() { m_owner->cleanup(); }
void cleanup() { std::cout << "Private x=" << ++m_x << '\n'; }
};
std::unique_ptr<Private> d;
C() { d = std::make_unique<Private>(this); }
~C() = default;
void cleanup() { d->cleanup(); }
};
int main(int argc, char **argv)
{
C c;
c.cleanup(); // For display purposes, print 1
return 0; // Destructors called, print 2
}
FreeBSD 上的输出:
Private x=1
Segmentation fault (core dumped)
和一段回溯:
* thread #1, name = 'a.out', stop reason = signal SIGSEGV: invalid address (fault address: 0x8)
frame #0: 0x00000000002032b4 a.out`C::Private::cleanup() + 52
a.out`C::Private::cleanup:
-> 0x2032b4 <+52>: movl 0x8(%rax), %esi
我认为这可能是 UB 的原因是:
- 在
return 0
,c
的生命周期即将结束。 - 析构函数
~C()
运行s。一旦析构函数的主体(默认)完成,对象的生命周期结束并且使用该对象是 UB。 - 现在是对象 运行. 的子对象(成员对象?)的析构函数
- 析构函数
~std::unique_ptr<Private>
运行s。它 运行 是持有对象的析构函数。 - 析构函数
~Private()
使用指向不再存在的对象的指针m_owner
调用成员函数。
如果您能指出这种对对象生命周期的理解是否正确,我将不胜感激。
如果它不是 UB,那么就会有一个单独的实现质量问题(或者我应该在调用它的方法之前检查 d 指针,但这对于 pimpl 来说似乎有点愚蠢;然后我们得到 if(d)d->cleanup()
这是一个 STL 实现所需要的,而在另一个实现中是无用的检查。
为了提出一个问题:在销毁对象 c
期间,此代码是否在语句 m_owner->cleanup()
(第 9 行)中显示 UB?
是的,m_owner
引用的对象的生命周期已经结束,并且在调用 m_owner->cleanup();
时它的析构函数调用已完成。因此调用是 UB。