如何修复称为运行时错误的纯虚拟函数



我明白为什么我得到我得到的错误(调用纯虚函数)。我正在尝试从如下所示的基类的析构函数中调用纯虚函数。但是,我不知道如何返工我的代码以防止这种情况发生。以下是基类和派生类(无论如何是相关部分):

基类:

TailFileManager::TailFileManager(const std::string &filename, const int fileOpenPeriod_ms)
: m_Stop(false)
{
    m_WorkerThread.reset(new boost::thread(boost::bind(&TailFileManager::TailFile, this, filename, fileOpenPeriod_ms)));
}
TailFileManager::~TailFileManager()
{
    m_Stop = true;
    m_WorkerThread->join();
}
void TailFileManager::TailFile(const std::string &filename, const int fileOpenPeriod_ms)
{
    std::ifstream ifs(filename.c_str());
    while (! ifs.is_open())
    {
        boost::this_thread::sleep(boost::posix_time::milliseconds(fileOpenPeriod_ms));
    ifs.open(filename.c_str());
    }
    ifs.seekg(0, std::ios::end);
    while (! m_Stop)
    {
        ifs.clear();
        std::string line;
        while (std::getline(ifs, line))
        {
            OnLineAdded(line);
        }
        OnEndOfFile();
    }
    ifs.close();
}

派生类:

ETSLogTailFileManager::ETSLogTailFileManager(const std::string &filename, const int heartbeatPeriod_ms)
: TailFileManager(filename, heartbeatPeriod_ms),
  m_HeartbeatPeriod_ms(heartbeatPeriod_ms),
  m_FoundInboundMessage(false),
  m_TimeOfLastActivity(0)
{
}
ETSLogTailFileManager::~ETSLogTailFileManager()
{
}
void ETSLogTailFileManager::OnLineAdded(const std::string &line)
{
    // do stuff...
}
void ETSLogTailFileManager::OnEndOfFile()
{
    // do stuff...
}

你不应该在构造或破坏过程中调用虚函数,因为这些调用不会按照你的想法去做,即使它们做了,你仍然会不高兴。如果你是一个正在康复的 Java 或 C# 程序员,请密切注意这个项目,因为这是这些语言曲折的地方,而C++曲折的地方。

重新设计您的设计,即您可以在对象被销毁之前调用一些清理函数,如果您正在使用C++,那么想法只是在 const/dest 期间避免虚拟函数(如果有的话!

虚拟调用的规则不同。2003 C++,第12.7节"建造和破坏"说:

让我们刷新一些旧的记忆...

成员函数,包括虚函数 (10.3),可以在构造或销毁 (12.6.2) 期间调用。当从构造函数(包括从数据成员的 mem 初始值设定项)或析构函数直接或间接调用虚函数时,并且调用适用的对象是正在构造或销毁的对象,则调用的函数是在构造函数或析构函数自己的类或其基之一中定义的函数, 但不是函数在派生自构造函数或析构函数类的类中重写它,或者在派生最多的对象 (1.8) 的其他基类之一中重写它。如果虚函数调用使用显式类成员访问 (5.2.5),并且对象表达式引用正在构造或销毁的对象,但其类型既不是构造函数或析构函数自己的类,也不是其基之一,则调用的结果是未定义的。

由于这种行为差异,建议您在构造或销毁对象的虚函数时永远不要调用该对象的虚函数。

切勿在施工或破坏期间调用虚拟函数有效C++节选,第三版作者:斯科特·迈耶斯2005年6月6日

http://www.artima.com/cppsource/nevercall.html

就C++标准而言:

  • 如果在构造函数或析构函数中调用虚函数,则该函数是动态调度,就好像其动态类型是正在执行的当前构造函数/析构函数的类型一样 (§12.7/4)
  • 如果该函数发生在纯虚上,那么这是未定义的行为(§10.4/6); Itanium ABI 定义了行为: 调用__cxa_pure_virtual

所以,你有一个棘手的问题...

<小时 />

解决这个问题的一个可能办法是把它分成两部分,以便把破坏分成两部分。这可以通过Strategy模式来实现:

  • 提供可定制的界面,您的策略
  • 提供一个管理器类,该类封装功能并遵循可自定义部件的策略

让我们说得更清楚:

class Interface {
public:
    friend class Manager;
private:
    virtual void finalize() = 0;
}; // class Interface

class Manager {
public:
    explicit Manager(std::unique_ptr<Interface>&&);
    ~Manager();
private:
    std::unique_ptr<Interface> _interface;
}; // class Manager
Manager::~Manager() {
    _interface->finalize();
}

诀窍 ?在finalize()被称为_interface的毁灭还没有开始!稍后将调用析构函数;因此,您不会遭受半死不活的物体的命运。

我现在将通过有关在析构函数中join线程的警告来结束这个答案。请注意,在堆栈展开的情况下会自动调用析构函数,因此在失败时无限期等待可能是危险的;特别是如果线程正在等待当前正在展开的线程应该提供的数据......死锁的经典案例。

<小时 />

参考文献 (n3337):

§12.7/4 成员函数,包括虚函数(10.3),可以在构造或销毁期间调用(12.6.2)。当从构造函数或析构函数直接或间接调用虚函数时,包括在构造或销毁类的非静态数据成员期间,并且调用适用的对象是正在构造或销毁的对象(称为 x),则调用的函数是构造函数或析构函数类中的最终重写器,而不是在派生更多类中重写它

§10.4/6 成员函数可以从抽象类的构造函数(或析构函数)调用;对于从此类构造函数(或析构函数)创建(或销毁)的对象,直接或间接对纯虚函数进行虚拟调用 (10.3) 的效果是不确定的。

你写道,

"我正在尝试从如下所示的基类的析构函数中调用纯虚函数。"

有问题的代码是

TailFileManager::~TailFileManager()
{
    m_Stop = true;
    m_WorkerThread->join();
}

令人高兴的是,在单线程执行中,这不可能调用纯虚函数。但是,您join的线程可能会通过非虚拟成员函数调用此对象上的纯虚函数。如果是这样,则问题出在线程上,特别是此对象的生存期管理。

不幸的是,您没有显示相关代码。尝试将事情简化为一个小的、完整的、工作的例子。在"工作"的意义上,它重现了问题。

根据您使用的系统,这可能有效:

在纯函数调用处理程序上设置断点:__cxa_pure_virtual

这使用 gdb 对我有用:break __cxa_pure_virtual然后,当命中断点时,up显示我的代码被破坏了。(就我而言,我正在引入第二个线程,并且在使用该对象时,当前正在另一个线程上进行破坏。

__cxa_pure_virtual文件:https://www.swag.uwaterloo.ca/acd/docs/ItaniumC++ABI.htm#pure-virtual

最新更新