std::unique_ptr、pimpl 和对象生命周期

Adr*_*oot 2 c++ undefined-behavior c++17

以下示例使用 Linux 上的 gcc 11 (GNU STL) 和 FreeBSD 上的 clang 12 (Clang STL) 进行编译。在 Linux 上,它运行并打印值 1 和 2。在 FreeBSD 上,它打印值 1,然后因 SEGV 崩溃。我不太了解对象的生命周期——所以整个事情可能是 UB 并且运行时行为可能不相关。我确实知道这两个 STL的实现在一个重要方面有所不同:Clang STL在析构函数的开头重置 a 的内部指针,而 GNU STL 则单独保留该指针std::unique_ptrstd::unique_ptrnullptr

#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
}
Run Code Online (Sandbox Code Playgroud)

FreeBSD 上的输出:

Private x=1
Segmentation fault (core dumped)
Run Code Online (Sandbox Code Playgroud)

和一段回溯:

* 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
Run Code Online (Sandbox Code Playgroud)

我认为这可能是 UB 的原因是:

  • return 0c的生命即将结束。
  • 析构函数~C()运行。一旦析构函数的主体(默认)完成,该对象的生命周期就结束了,并且使用该对象是UB。
  • 现在运行对象的子对象(成员对象?)的析构函数。
  • 析构函数~std::unique_ptr<Private>运行。它运行所持有对象的析构函数。
  • 析构函数~Private()使用指向不再存在的对象的指针m_owner来调用成员函数。

我希望有人能指出这种对对象生命周期的理解是否正确。

如果它不是 UB,则存在一个单独的实现质量问题(或者我应该在调用 d 指针上的方法之前检查它,但这对于 pimpl 来说似乎有点迟钝;然后我们得到一个 STL 实现所需的if(d)d->cleanup()内容这在另一个中是无用的检查)。

为了提出一个问题:m_owner->cleanup()在对象销毁期间,这段代码是否在语句(第 9 行)中表现出 UB c

use*_*522 5

是的,所引用的对象的生命周期m_owner已经结束,并且在m_owner->cleanup();调用时它的析构函数调用已完成。因此该呼叫为 UB。