意外地能够从基类构造函数调用派生类虚函数

6

有人能帮忙解释一下这种意外的行为吗?

前提条件

我创建了一个包含成员变量std :: thread的Thread类。 Thread的构造函数构造成员std :: thread,并提供一个指向静态函数的指针,该函数调用纯虚函数(由基类实现)。

代码

#include <iostream>
#include <thread>
#include <chrono>

namespace
{

class Thread
{
public:
    Thread()
        : mThread(ThreadStart, this)
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl; // This line commented later in the question.
    }

    virtual ~Thread() { }

    static void ThreadStart(void* pObj)
    {
        ((Thread*)pObj)->Run();
    }

    void join()
    {
        mThread.join();
    }

    virtual void Run() = 0;

protected:
    std::thread mThread;
};

class Verbose
{
public:
    Verbose(int i) { std::cout << __PRETTY_FUNCTION__ << ": " << i << std::endl; }
    ~Verbose() { }
};

class A : public Thread
{
public:
    A(int i)
        : Thread()
        , mV(i)
    { }

    virtual ~A() { }

    virtual void Run()
    {
        for (unsigned i = 0; i < 5; ++i)
        {
            std::cout << __PRETTY_FUNCTION__ << ": " << i << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }

protected:
    Verbose mV;
};

}

int main(int argc, char* argv[])
{
    A a(42);
    a.join();

    return 0;
}

问题

你可能已经注意到了一个微妙的错误:在Thread构造函数上下文中调用Thread::ThreadStart(...),因此调用纯虚函数/虚函数不会调用派生类的实现。这由运行时错误证明:

pure virtual method called
terminate called without an active exception
Aborted

然而,如果我在Thread的构造函数中删除对std::cout的调用,则会出现意外的运行时行为:

virtual void {anonymous}::A::Run(){anonymous}::Verbose::Verbose(int): : 042

virtual void {anonymous}::A::Run(): 1
virtual void {anonymous}::A::Run(): 2
virtual void {anonymous}::A::Run(): 3
virtual void {anonymous}::A::Run(): 4

例如,在Thread的构造函数中删除对std::cout的调用似乎会产生这样的效果:可以在基类构造函数上下文中调用派生类的纯虚/虚函数!这与之前的学习和经验不符。

在Windows 10上的Cygwin x64构建环境中,gcc版本为:

g++ (GCC) 5.4.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

我对这个观察结果感到困惑,对发生的事情非常好奇。有人可以解释一下吗?

没有什么意外的。当对象只被构造到特定的基类A时,可用的虚函数实现只有从A可见的那些。 - user207421
很遗憾我们没有一个后构造函数。如果有的话,在这里会非常有用... - Deduplicator
1个回答

9
这个程序的行为是未定义的,由于竞争条件。但是,如果你想要理解它,让我们试着来看看。
对于 A 的构建,以下是所发生的情况:
1. mThread 被初始化,并在某个时间点被操作系统调度开始。 2. std::cout << __PRETTY_FUNCTION__ << std::endl; - 这从程序的角度来看是一个相当缓慢的操作。 3. A 的构造函数运行-初始化它的vtable(这不是标准规定的,但据我所知,所有实现都这样做)。
如果这个过程在 mThread 调度启动之前发生,你就会得到你观察到的行为。否则,你会得到纯虚函数调用。因为这些操作没有以任何方式排序,所以行为是未定义的。
你可以注意到,你从基类的构造函数中删除了一个相当缓慢的操作,从而更快地初始化了你的派生类及其vtable。 比如,在操作系统实际调度 mThread 的线程开始之前。尽管如此,这并没有解决问题,只是使遇到它的可能性变小。
如果你稍微修改一下你的示例,你会发现删除 I/O 代码使比赛更难找到,但没有修复任何问题。
virtual void Run()
{
    for (unsigned i = 0; i < 1; ++i)
    {
        std::cout << __PRETTY_FUNCTION__ << ": " << i << std::endl;
//      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

主函数:

for(int i = 0; i < 10000; ++i){
    A a(42);
    a.join();
}

demo


1
好的解释! - πάντα ῥεῖ
如果我理解正确:如果Thread没有cout,它会快速初始化,因此它的vtable可能在操作系统调度std::thread之前被初始化,如果是这样,在std::thread调用ThreadStart时,Run()的vtable条目将被填充,从而调用A::Run()。我理解的对吗? - StoneThrow
@StoneThrow 是的。 - krzaq
@krzaq:谢谢,那是一次很好的解释。所以当我说ThreadStart()是从Thread ctor上下文调用的时,我实际上是错了。它实际上在一个单独的上下文中被调用:即被产生的std::thread的上下文。 - StoneThrow

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