1

假设我有以下代码:

class BaseMember
{
};

class DerivedMember : public BaseMember
{
};

class Base
{
private:
    BaseMember* mpMember;
protected:
    virtual BaseMember* initializeMember(void)
    {
        return new BaseMember[1];
    }

    virtual void cleanupMember(BaseMember* pMember)
    {
        delete[] pMember;
    }
public:
    Base(void)
        : mpMember(NULL)
    {
    }

    virtual ~Base(void)
    {
        cleanupMember(mpMember);
    }

    BaseMember* getMember(void)
    {
        if(!mpMember)
            mpMember = initializeMember();
        return mpMember;
    }
};

class Derived : public Base
{
protected:
    virtual BaseMember* initializeMember(void)
    {
        return new DerivedMember;
    }

    virtual void cleanupMember(BaseMember* pMember)
    {
        delete pMember;
    }
};

Base 和 BaseMember 是 API 的一部分,可以由该 API 的用户子类化,就像在示例代码中通过 Derived 和 DerivedMember 完成的一样。

Base 通过调用它的虚拟工厂函数 initializeMember() 来初始化 mpBaseMember,以便派生类可以覆盖工厂函数以返回 DerivedMember 实例而不是 BaseMember 实例。

但是,当从基类构造函数中调用虚函数时,会调用基实现而不是派生类覆盖。因此,我正在等待 mpMember 的初始化,直到它第一次被访问(这当然意味着,基类和任何可能自己进一步派生的派生类都不允许从内部访问该成员构造函数)。

现在的问题是:从基基析构函数中调用虚拟成员函数将导致调用该函数的基类实现,而不是派生类覆盖。这意味着我不能简单地从基类析构函数中调用 cleanupMember(),因为这将调用它的基类实现,它可能无法正确清理那些东西,initializeMember() 的派生实现已经初始化。例如,基类和派生类可能使用不兼容的分配器,这可能会在混合时导致未定义的行为(如在示例代码中 - 派生类通过 new 分配成员,但基类使用 delete[] 释放它) .

所以我的问题是,我该如何解决这个问题?我想出的是:a)API 的用户必须在 Derived 实例被破坏之前显式调用一些清理函数。这很可能会被遗忘。b)(大多数)派生类的析构函数必须调用清理函数来清理由基类触发的初始化内容。这感觉很难看,而且设计得不好,因为所有权责任混合在一起:基类触发分配,但派生类必须触发解除分配,这是非常违反直觉的,除非他阅读派生类的作者,否则他无法知道API 文档足够彻底,可以找到该信息。我真的很想以一种比依靠用户的记忆力或他的可靠性来彻底阅读文档更可靠的方式来做到这一点。

有没有其他方法?

注意:由于派生类在基类的编译时可能不存在,因此静态多态在这里不是一个选项。

4

6 回答 6

1

对包含清理方法的工厂模式进行修改呢?意思是,添加一个属性memberFactory,例如,一个类的实例,提供创建、清理以及对成员的访问。虚拟初始化方法将提供并初始化正确的工厂,析构函数~Base将调用工厂的清理方法并销毁它。

(嗯,这与工厂模式相去甚远......也许它以另一个名字而闻名?)

于 2013-09-26T16:15:58.520 回答
1

如果你真的想做这种事情,你可以这样做:

class Base {
    BaseMember* mpMember;

  protected:
    Base(BaseMember *m) : mpMember(m) {}

    virtual void doCleanupMember(BaseMember *m) { delete [] m; }

    void cleanupMember() {
      // This gets called by every destructor and we only want
      // the first call to do anything. Hopefully this all gets inlined.
      if (mpMember) {
        doCleanupMember(pmMember);
        mpMember = nullptr;
      }
    }

  public:
    Base() : mpMember(new BaseMember[1]) { }
    virtual ~Base(void) { cleanupMember(); }
};

class Derived : public Base {
  virtual void doCleanupMember(BaseMember *m) override { delete m; }

  public:
    Derived() : Base(new DerivedMember) {}
    ~Derived() { cleanupMember(); }
};

然而,这是一个坏主意是有原因的。

首先是该成员应由 Base 独家管理。试图将 Base 成员的责任划分到派生类中很复杂,而且只会自找麻烦。

其次,您初始化 mpMember 的方式意味着该成员具有不同的接口,具体取决于初始化它的人。您已经遇到的部分问题是有关谁初始化成员的信息已被您访问的类型破坏了~Base()。同样,尝试为同一个变量使用不同的接口只是自找麻烦。

我们至少可以通过使用shared_ptr允许指定删除器的方法来解决第一个问题:

class Base {
    std::shared_ptr<BaseMember> mpMember;
  public:
    Base(std::shared_ptr<BaseMember> m) : mpMember(m) { }
    Base() : mpMember(std::make_shared<BaseMember>()) { }
    virtual ~Base() {}
};

class Derived : virtual public Base {     
  public:
    Derived()
      : Base(std::shared_ptr<BaseMember>(new DerivedMember[1],
                                         [](BaseMember *m){delete [] m;} ) {}
};

这只隐藏了成员界面的破坏部分的差异。如果您有更多元素的数组,则成员的不同用户仍然必须能够确定是否mpMember[2]合法。

于 2013-09-26T17:10:50.747 回答
0

永远不要在构造函数/析构函数中调用虚方法,因为它会产生奇怪的结果(编译器会产生你看不到的黑暗和奇怪的东西)。

析构函数调用顺序是子元素,然后是父元素

你可以这样做(但可能有更好的方法):

private:
    // private destructor for prevent of manual "delete"
    ~Base() {}

public:
    // call this instead use a manual "delete"
    virtual void deleteMe()
    {
        cleanupMember(mpMember);
        delete this; // commit suicide
    }

有关自杀的更多信息: https ://stackoverflow.com/a/3150965/1529139和http://www.parashift.com/c++-faq-lite/delete-this.html

PS:为什么析构函数是虚拟的?

于 2013-09-26T16:12:53.973 回答
0

首先,在 C++ 中开发时必须使用 RAII 成语。当然,如果您不想与内存泄漏作斗争,则必须在析构函数中释放所有资源。

你可以创建一些cleanupMember()函数,但是你应该在析构函数中检查你的资源,如果它们没有被删除则释放它们(因为cleanupMember永远不会被调用,例如因为异常)。因此,将析构函数添加到您的派生类中:

virtual ~Derived()
{
    Derived::cleanupMember(mpMember);
}

并管理类本身的成员指针。

我还建议您在这里使用智能指针。

于 2013-09-26T16:15:59.320 回答
0

让我们mpMember受到保护并让它在派生类构造函数中初始化并在派生析构函数中释放。

于 2013-09-26T16:51:39.217 回答
0

受到来自https://stackoverflow.com/a/19033431/404734的想法的启发,我想出了一个可行的解决方案 :-)

class BaseMember
{
};

class DerivedMember : public BaseMember
{
};

class BaseMemberFactory
{
public:
    virtual ~BaseMemberFactory(void);

    virtual BaseMember* createMember(void)
    {
        return new BaseMember[1];
    }

    virtual void destroyMember(BaseMember* pMember)
    {
        delete[] pMember;
    }
};

class DerivedMemberFactory : public BaseMemberFactory
{
    virtual BaseMember* createMember(void)
    {
        return new DerivedMember;
    }

    virtual void destroyMember(BaseMember* pMember)
    {
        delete pMember;
    }
};

class Base
{
private:
    BaseMemberFactory* mpMemberFactory;
    BaseMember* mpMember;
protected:
    virtual BaseMemberFactory* getMemberFactory(void)
    {
        static BaseMemberFactory fac;
        return &fac;
    }
public:
    Base(void)
        : mpMember(NULL)
    {
    }

    virtual ~Base(void)
    {
        mpMemberFactory->destroyMember(mpMember);
    }

    BaseMember* getMember(void)
    {
        if(!mpMember)
        {
            mpMemberFactory = getMemberFactory();
            mpMember = mpMemberFactory->createMember();
        }
        return mpMember;
    }
};

class Derived : public Base
{
protected:
    virtual BaseMemberFactory* getMemberFactory(void)
    {
        static DerivedMemberFactory fac;
        return &fac;
    }
};
于 2013-09-26T20:19:30.467 回答