【问题标题】:Unexpectedly able to call derived-class virtual function from base class ctor意外能够从基类ctor调用派生类虚函数
【发布时间】:2017-03-12 01:58:38
【问题描述】:

谁能帮助解释这种意外行为?

前提

我创建了包含成员 std::thread 变量的类 Thread。 Thread 的 ctor 构造成员 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::ThreadStart(...) 是从 Thread ctor 上下文调用的,因此调用纯/虚拟函数不会调用派生类的实现。运行时错误证实了这一点:

pure virtual method called
terminate called without an active exception
Aborted

但是,如果我在 Thread ctor 中删除对 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 ctor 中删除对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 可见的。
  • 很遗憾我们没有额外的 post-ctor。在这里会很有用...

标签: c++ c++11 inheritance pure-virtual stdthread


【解决方案1】:

由于竞态条件,该程序的行为未定义。

但是,如果你想推理它,让我们试试吧。

对于A 的构造,会发生以下情况:

  1. mThread 已初始化。操作系统将其安排在未来某个时间点启动。
  2. std::cout &lt;&lt; __PRETTY_FUNCTION__ &lt;&lt; std::endl; - 从程序的角度来看,这是一个相当缓慢的操作。

  3. A 构造函数运行 - 初始化它的 vtable (这不是标准规定的,但据我所知,所有实现都这样做)。

    如果这发生在 mThread 计划启动之前,您将获得观察到的行为。 否则,您将获得纯虚拟调用。

因为这些操作没有任何顺序,所以行为是未定义的。

您会注意到您从基的构造函数中删除了一个相当慢的操作,因此初始化您的派生 - 及其 vtable - 更快。比如说,在操作系统实际安排mThread 的线程启动之前。话虽如此,这并没有解决问题,只是降低了遇到它的可能性。

如果您稍微修改一下您的示例,您会注意到删除 IO 代码会使比赛更难找到,但没有解决任何问题。

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

【讨论】:

  • 很好的解释!
  • 如果我理解正确:如果Thread 没有cout,它会快速初始化,因此它的vtable可能在操作系统调度std::thread之前被初始化,如果是这样,当std::thread 调用ThreadStart 时,Run() 的vtable 条目将被填充,从而调用A::Run()。我没听错吗?
  • @StoneThrow 是的。
  • @krzaq:谢谢;这是一个很好的解释。所以当我说ThreadStart() 是从Thread ctor 上下文中调用时,我实际上是误会了。它实际上是在一个单独的上下文中调用的:生成的 std::thread
猜你喜欢
  • 1970-01-01
  • 2011-09-27
  • 1970-01-01
  • 2019-04-19
  • 2015-04-16
  • 2011-06-19
  • 1970-01-01
  • 1970-01-01
  • 2021-03-19
相关资源
最近更新 更多