中级类:调用的纯虚拟方法

intermediate class: pure virtual method called

本文关键字:虚拟 方法 调用      更新时间:2023-10-16

对于许多类C1C2等,初始化看起来是相等的,只是类与类之间略有不同。 因此,我创建了一个承载初始化的基类B,例如:

class B {
  public:
    B()
    {
      // complex initializations
      int a = doSomething();
      // more complex stuff with `a`
    };
    virtual int doSomething()
    {return 2 * doSomethingHelper();}
  protected:
    virtual int doSomethingHelper() = 0;
};
class C: public B {
  protected:
    virtual int doSomethingHelper()
    {return 1;}
};
int main() {
  C c;
  return 0;
}

此代码失败,并显示

pure virtual method called
terminate called without an active exception
Aborted (core dumped)

由于doSomethingHelper()用于初始化B.

我想知道是否有更好的设计。我的目标是:

  1. 使C的用户界面尽可能简单:C没有构造函数参数。

  2. 使C本身尽可能简约,以便B的其他具体化都很短。理想情况下,它只包含 doSomethingHelper .

建议采用更理智的设计将不胜感激。

简短回答:

不要从构造函数中调用虚函数。这样做会给你带来麻烦。


更长的答案:

声明C c分步骤创建和构造class C 的实例。该对象首先构造为class A对象,然后构造为class B对象,最后构造为class C对象。在调用B::B()时,没有概念认为该对象最终将成为class C的实例。

当调用B::B()时,它会调用虚函数doSomething(),在这种情况下意味着调用B::doSomething()。还没有问题;该函数存在。问题在于B::doSomething()体内对doSomethingHelper()的呼吁。该函数在这一点上是纯虚拟的。如上所述,没有迹象表明此对象最终将成为class C的实例。无法调用的函数C::doSomethingHelper()不能从B::B()A::A()调用。没有A::doSomethingHelper(),没有B::doSomethingHelper(),所以函数不存在。你干杯。


我想知道是否有更好的设计。

有很多更好的设计。最简单的方法是不要从类 B 的构造函数中调用doSomething()。 将该调用移动到类 C 的构造函数。即便如此,从构造函数中调用虚函数可能不是一个好主意。如果类 D 继承自类 C 并覆盖C::doSomethingHelper()怎么办?类 D 的实例将通过调用 C::doSomethingHelper() 而不是 D::doSomethingHelper() 来构造。

根据标准:

10.4/6:成员函数可以从抽象类的构造函数(或析构函数(调用;进行虚拟调用的效果(10.3(对一个纯虚函数直接或间接地用于从此类构造函数创建(或销毁(的对象(或析构函数(未定义

这是因为,当您构造 C 时:

  • 首先,使用 B(( 构造函数构造子对象 B。 那时,使用的仍然是 B 的虚函数。 您收到错误是因为此时尚未为其定义doSomethingHelper((。

  • 只有当 B(( 完成时,虚拟将 C 的虚函数变为活动状态。

两阶段初始化

这种情况只能通过两阶段初始化来避免:首先构造,然后调用初始化函数。 没有你想要的那么好和用户友好。

class B {
public:
    B() { /* complex initializations */ }
    ...
protected:
    void init() {  //  the rest of the what you wanted in constructor 
        int a = doSomething();
        // more complex stuff with `a`
    }
};

然后可以通过 C 的构造函数触发两个阶段初始化:

class C {
public: 
    C() : B() {  // first B is constructed
       init();   // then the body of C's constructor is executed
    }   
 ...
 };

两阶段初始化的变体

您可以使用一个小变体来隐藏两阶段方法,并让用户在定义或不定义自己的构造函数方面有更多的自由。

在 B 中定义一个辅助嵌套类:

protected:
    class initialiser {
    public: 
        initialiser(B*b) {b->init();}  // the constructor launches the second phase init
    };

在 C 中,你只需要添加一个受保护的成员变量:

class C:  public  B {
    ...
protected:
    B::initialiser bi{this}; // this triggers automaticcaly the second phase
    ...
};

该标准的规则确保首先构造B,然后构造C的成员。 在这里演示。

不能

对构造函数中的派生类使用动态调度。

B 的构造函数运行时,尚未创建 C 子对象,因此不能使用其任何重写函数。由于B没有为doSomethingHelper提供实现,因此无法做任何明智的事情。

将所有复杂性移到B::doSomething中,并从继承链的末端调用该方法,C

class B {
  public:
    B()
    {};
    virtual int doSomething()
    {
      // complex initializations
      int a = 2 * doSomethingHelper();
      // more complex stuff with `a`
      return a;
    }
  protected:
    virtual int doSomethingHelper() = 0;
};
class C: public B {
  public:
    C(): B()
    {
      int a = doSomething();
    }
  protected:
    virtual int doSomethingHelper()
    {return 1;}
};
int main() {
  C c;
  return 0;
}

这可能需要您将以前B private成员中的一些protected,以便可以通过 C 初始化它们。