【问题标题】:Derived member states during destruction销毁期间的派生成员国
【发布时间】:2023-03-24 09:35:01
【问题描述】:

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

#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;
}

(此代码存在许多已知错误,因为它已针对此问题进行了简化——我知道它会泄漏内存并公开太多内容,等等;这不是重点。)

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

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

特别注意,在基类析构函数中对iterate() 的调用导致在~ListDerived() 执行其主体之后但在它实际退出之前调用ListDerived::g()——所以ListDerived 实例是在出去的路上,但仍然部分活着。请注意g() 本身不是虚拟的,并且不在ListBase 的方法中。

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

(有问题的行为是在部分破坏的ListDerived 上调用g() 以及随后访问其实际破坏的destroyed 成员。)

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

第三个问题是(假设这是UB),有什么好的方法可以保持相同的流量但避免UB? Real Code™ 对此有一些限制:

  • 这是 C++03 代码 (VS2008),遗憾的是不允许使用 C++11。 (话虽如此,我仍然很想知道 C++11 是否会以某种方式改进。)
  • 一旦ListDerived 的析构函数开始执行,就可以跳过对g() 的调用。
  • ListDerived 的析构函数不允许访问 items 中的任何内容(也不允许保留其特殊项目的单独副本),因此它无法以某种方式标记该项目以告诉它避免调用 @987654339 @。
  • ListDerived 本身不能假设它在 shared_ptr 中,所以不能使用 shared_from_this

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

【问题讨论】:

  • 我很难理解您认为代码应该的行为方式。如果你能列出要求,可能会更容易理解。
  • 我已经在底部列出了要求。基本思想是ListBase 有一个项目列表,它通过调用项目上的虚拟方法来累积并定期处理这些项目。在销毁时,它仍然需要处理列表中的任何未处理项目(在 Real Code™ 中,它只是清理处理而不是完整处理,但差异对问题无关紧要)。但是有一个派生类型偶尔需要排队一些需要调用派生类型本身来完成工作的东西,但是由于结构的原因,这在销毁处理时很棘手。

标签: c++ polymorphism destructor c++03


【解决方案1】:

这是我自己解决这个问题的尝试(假设它确实是 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 在到达那里(在~ListBase() 中)之前已经被销毁(在~ListDerived() 中)。我认为这现在是安全的。

(当然,除非并发调用,特别是因为 p 不是 f() 内部的拥有指针。如果复制或移动 ListDerived 也不安全,但原始代码也不是;假装那是通过通常的方式被阻止。)

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

创建一个非拥有的 shared_ptr 感觉有点不对劲(即使它已在 official cookbook 中涵盖)但 AFAIK 没有任何其他支持生命周期跟踪的标准类型。

【讨论】:

    猜你喜欢
    • 2017-05-10
    • 2011-03-09
    • 2012-09-11
    • 2013-07-13
    • 1970-01-01
    • 2011-03-18
    • 1970-01-01
    • 2014-12-16
    • 2016-01-12
    相关资源
    最近更新 更多