Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpectedly able to call derived-class virtual function from base class ctor

Can anyone help explain this unexpected behavior?

The Premise

I've created class Thread that contains a member std::thread variable. Thread's ctor constructs the member std::thread providing a pointer to a static function that calls a pure virtual function (to be implemented by base classes).

The Code

#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;
}

The Problem

As you may have already noticed, there's a subtle bug here: Thread::ThreadStart(...) is called from the Thread ctor context, therefore calling a pure/virtual function will not call the derived class' implementation. This is borne out by the runtime error:

pure virtual method called
terminate called without an active exception
Aborted

However, there is unexpected runtime behavior if I remove the call to std::cout in the Thread ctor:

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

I.e. removing the call to std::cout in the Thread ctor seems to have the effect of being able to call a derived class' pure/virtual function from the base class` constructor context! This doesn't align with prior learning and experience.

Build environment in Cygwin x64 on Windows 10. gcc version is:

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.

I'm baffled by this observation and am burning with curiosity about what's going on. Can anyone shed light?

like image 491
StoneThrow Avatar asked Oct 29 '16 09:10

StoneThrow


1 Answers

The behavior of this program is undefined, due to race condition.

But, if you want to reason about it, let's try.

For A's construction, here's what happens:

  1. mThread is initialized. The OS schedules it to start at some point in the future.
  2. std::cout << __PRETTY_FUNCTION__ << std::endl; - this is a fairly slow operation from the program's perspective.

  3. A constructor runs - initializing its vtable (this is not mandated by the stanard, but as far as I know, all implementations do this).

    If this happens before mThread is scheduled to start, you get the behaviour you observed. Otherwise, you get the pure virtual call.

Because those operations aren't in any way sequenced, the behaviour is undefined.

You can notice that you removed a fairly slow operation from your base's constructor, thus initializing your derived - and its vtable - much faster. Say, before the OS actually scheduled mThread's thread to start. That being said, this did not fix the problem, just made encountering it less likely.

If you modify your example a bit, you'll notice that removing the IO code made the race harder to find, but fixed nothing.

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));
    }
}

main:

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

demo

like image 190
krzaq Avatar answered Sep 20 '22 09:09

krzaq