销毁期间派生的会员国

Derived member states during destruction

这是一个与 this 类似的问题,但它侧重于虚拟方法(在问题和答案中),我对派生的非虚拟方法和数据成员更感兴趣 class 以及它如何与相关的类型层次结构交互。在这个测试代码中:

#include <iostream>
#include <vector>

using namespace std;

struct ItemBase
{
    explicit ItemBase(int v) : value(v) {}
    virtual ~ItemBase() {}

    virtual void f() { cout << "In ItemBase::f() for " << value << endl; }
    int value;
};

struct ListBase
{
    virtual ~ListBase() { cout << "In ~ListBase" << endl; iterate(); }

    void add(int v) { items.push_back(new ItemBase(v)); }

    void iterate()
    {
        for (vector<ItemBase*>::iterator it = items.begin(); it != items.end(); ++it)
        {
            (*it)->f();
        }
        items.clear();
    }

    vector<ItemBase*> items;
};

struct ListDerived : public ListBase
{
    struct ItemDerived : public ItemBase
    {
        ItemDerived(int v, ListDerived& p) : ItemBase(v), owner(p) {}
        virtual void f()
        {
            cout << "In ItemDerived::f() for " << value << endl;
            owner.g();
        }
        ListDerived& owner;
    };

    void addSpecial(int v) { items.push_back(new ItemDerived(v, *this)); }

    ListDerived() : destroyed(false) {}
    ~ListDerived() { cout << "In ~ListDerived" << endl; destroyed = true; }
    void g() { cout << "In ListDerived::g(): " << (destroyed ? "dead" : "alive") << endl; }

    bool destroyed;
};

int main()
{
    ListDerived list;
    list.add(1);
    list.addSpecial(2);
    list.iterate();
    list.add(3);
    list.addSpecial(4);
    return 0;
}

(由于针对此问题进行了简化,此代码存在许多已知错误——我知道它会泄漏内存并产生过多内容 public,等等;这不是重点.)

这个测试程序的输出结果如下:

In ItemBase::f() for 1
In ItemDerived::f() for 2
In ListDerived::g(): alive
In ~ListDerived
In ~ListBase
In ItemBase::f() for 3
In ItemDerived::f() for 4
In ListDerived::g(): dead

特别注意,在 class 析构函数中对 iterate() 的调用导致了对 ListDerived::g() 的调用,在 ~ListDerived() 执行其主体之前实际上已经退出了——所以 ListDerived 实例即将退出,但仍然部分存在。请注意 g() 本身不是虚拟的,也不在 ListBase 的方法中。

我怀疑这个输出依赖于 UB,所以这是第一个问题:是这种情况还是定义明确(尽管可能是狡猾的风格)?

(有问题的行为是在部分销毁的 ListDerived 上调用 g() 并随后访问其实际销毁的 destroyed 成员。)

第二个问题是,如果这不是 UB,仅仅是因为 destroyed 有一个普通的析构函数,如果它是更复杂的东西(例如 shared_ptr),它会变成 UB 吗?

第三个问题(假设这是UB),有什么好的方法既能保持同样的流程又能避开UB?我对 Real Code™ 有一些限制:

(可能会有更多的限制条件使我没有想到的替代解决方案变得复杂——这些是受到我在撰写本文时考虑并拒绝的解决方案的启发。)

这是我自己解决这个问题的尝试(假设它确实是 UB),但我很想知道我是否错了并且原始代码没问题,或者是否有更好的解决方案以下:

struct ListDerived : public ListBase
{
    struct ItemDerived : public ItemBase
    {
        ItemDerived(int v, boost::shared_ptr<ListDerived> const& p)
            : ItemBase(v), owner(p) {}
        virtual void f()
        {
            cout << "In ItemDerived::f() for " << value << endl;
            if (boost::shared_ptr<ListDerived> p = owner.lock())
            {
                p->g();
            }
        }
        boost::weak_ptr<ListDerived> owner;
    };
    struct null_deleter
    {
        void operator()(void const *) const {}
    };

    void addSpecial(int v) { items.push_back(new ItemDerived(v, token)); }

    ListDerived() : destroyed(false) { token.reset(this, null_deleter()); }
    ~ListDerived() { cout << "In ~ListDerived" << endl; destroyed = true; }
    void g() { cout << "In ListDerived::g(): " << (destroyed ? "dead" : "alive") << endl; }

    bool destroyed;
    boost::shared_ptr<ListDerived> token;
};

这引入了 token,它在 ListDerived 实例的生命周期内存在(有点像 shared_from_this),但实际上并不 拥有 实例(因此 null_deleter),但仍可用于制作 weak_ptr 项目用于访问其父项而不是直接使用引用。结果输出:

In ItemBase::f() for 1
In ItemDerived::f() for 2
In ListDerived::g(): alive
In ~ListDerived
In ~ListBase
In ItemBase::f() for 3
In ItemDerived::f() for 4

所以对 g() 的第一次调用按预期发生但第二次调用从未进行,因为 token 在到达那里之前已经被销毁(在 ~ListDerived() 中)(在~ListBase())。我认为这现在是安全的。

(当然,禁止并发调用,尤其是因为 p 不是 f() 中的拥有指针。如果 ListDerived 被复制或移动也不安全,但是原始代码也不是;假装它已通过通常的方式被阻止。)

destroyed 现在是多余的,但我保留了它以避免过多更改代码。 (由于 C++03,也使用 boost::shared_ptr;如果有 std::tr1::shared_ptrstd::shared_ptr,请随意交换,应该不会有什么区别。)

创建非拥有 shared_ptr 感觉有点不对(即使它在 official cookbook 中有所介绍)但据我所知,没有任何其他标准类型支持生命周期跟踪。