从构造函数中调用虚函数和纯虚函数

5
当我从基类构造函数调用虚函数时,编译器不会报错。但是当我从基类构造函数调用纯虚函数时,它会给出编译错误。
考虑下面的示例程序:
#include <iostream>

using namespace std;
class base
{
   public:
      void virtual virtualfunc() = 0;
      //void virtual virtualfunc();
      base()
      {
         virtualfunc();
      }
};

void base::virtualfunc()
{
   cout << " pvf in base class\n";
}

class derived : public base
{
   public:
   void virtualfunc()
   {
      cout << "vf in derived class\n";
   }
};

int main()
{
   derived d;
   base *bptr = &d;
   bptr->virtualfunc();

   return 0;
}

这里可以看到纯虚函数有一个定义。我期望在执行bptr->virtualfunc()时会调用基类中定义的纯虚函数。然而实际上会出现编译错误:

错误:从构造函数调用纯虚函数`virtual void base::virtualfunc()'

这是什么原因呢?

4个回答

10

不要从构造函数中调用纯虚函数,因为这会导致未定义行为

C++03 10.4/6规定

“可以从抽象类的构造函数(或析构函数)中调用成员函数;对于正在从此类构造函数(或析构函数)创建(或销毁)的对象直接或间接地调用纯虚函数时的虚拟调用(10.3)的效果是未定义的。”

由于您没有在基类中定义纯虚函数virtualfunc(),所以会出现编译错误。如果想要调用它,必须给它一个函数体。

无论如何,在构造函数中调用纯虚函数应该避免,因为这是未定义行为。


@Als:我已经定义了纯虚函数,为什么还是编译错误? - nitin_cherian
@LinuxPenseur:你确定编译了正确的代码吗?在gcc-4.3.4中,没有定义会出现错误,但是有定义没有错误,但是没有错误并不意味着它是正确的,它仍然是未定义行为。 - Alok Save
2
@LinuxPenseur:根据标准,这是未定义行为,并且实现的质量应该在运行时潜在崩溃之前告诉您。如果您真的想从构造函数中调用该函数,请禁用虚拟分派机制:base::virtualfunc(),这样就可以消除错误(同时在用户代码中明确调用内容)。请注意,在某些编译器中,即使是纯虚拟的,只要它被定义了,所涉及的代码也会实际调用base::virtualfunc()而不会崩溃,但这只是另一种未定义行为的版本。 - David Rodríguez - dribeas
就纯虚函数情况下潜在的链接错误而言,有两个要考虑的问题。一方面,除了纯虚函数之外的所有虚函数都被认为是odr-used,无论程序是否使用该特定覆盖。这似乎表明,由于未使用纯虚函数,因此不需要定义它。另一方面,纯虚函数可以被显式地odr-used(考虑代码中的base::virtualfunc()),这将导致链接器错误。 - David Rodríguez - dribeas
目前这个编译器的问题在于,它根据之前看到的代码来决定是否调用和使用纯虚函数,这破坏了分离编译模型。编译器会根据单个TU中存在的代码而表现出不同的行为,这至少是令人困惑的。 - David Rodríguez - dribeas
显示剩余2条评论

2
在C++11中,有一种解决方法。
在构造函数委托的过程中,实际上可以调用纯虚方法的实现--只要实现该纯虚方法的类的至少一个构造函数在调用纯虚函数之前已经完成。
您可以/应该使用"final"关键字来确保子类的行为不是不可预测的。
参见:C++ 11 Delegated Constructor Pure Virtual Method & Function Calls — Dangers?
#include <string>

/**************************************/
class Base
{
public:
    int sum;
    virtual int Do() = 0;

    void Initialize()
    {
        Do();
    }
    Base()
    {
    }
};

/**************************************/
// Optionally declare class as "final" to avoid
// issues with further sub-derivations.
class Derived final : public Base
{
public:

    virtual int Do() override final
    {
        sum = 0 ? 1 : sum;
        return sum / 2 ; // .5 if not already set.
    }

    Derived(const std::string & test)
        : Derived() // Ensure "this" object is constructed.
    {
        Initialize(); // Call Pure Virtual Method.
    }
    Derived()
        : Base()
    {
        // Effectively Instantiating the Base Class.
        // Then Instantiating This.
        // The the target constructor completes.
    }
};




/********************************************************************/
int main(int args, char* argv[])
{
    Derived d;
    return 0;
}

1

是的,有绕过这个问题的方法。两阶段初始化不是一个好选择。建议使用PIMP模式。 - Martin York

0

编译器在构造函数完成之前不需要假定指针已经为纯虚函数设置。换句话说,在此时并不需要知道您是否有该函数的定义。因此,行为是未定义的。在某些编译器(如MSVC)上,它将按照您的期望工作,而在其他编译器上,它将给出当前错误。在其他一些编译器上,它将编译,但您将获得分段错误。

从构造函数中调用任何虚函数都是一个非常糟糕的想法,因为它使您的代码意图不清晰和混乱。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接