0

To be clear, this is only an experiment, no one should ever write their code like this, but consider this code:

class Base
{
protected:
    std::thread m_thread;
    std::mutex m_mutex;
    std::condition_variable m_condv;

    virtual void DerivedThread() = 0;

public:
    Base()
    {
        m_thread = std::thread(&Base::DerivedThread, this);

        std::unique_lock<std::mutex> lck(m_mutex);
        m_condv.wait(lck);
    }
};

class Derived : Base
{
protected:
     void DerivedThread() override
    {
         m_condv.notify_one();
    }

public:
    Derived() : Base() {}
    ~Derived()
    {
        m_thread.join();
    }
};

int main()
{
    Derived foo;
    return 0;
}

The Base class constructor will start a thread at the pure virtual method DerivedThread. It then blocks execution and waits for the conditional variable signal from that thread.

The code above will crash somewhere in the type_traits library, I haven't succeeded to debug to see what exactly cause the crash, and the stack trace looks weird, this was compiled with MSVC x86_64 in debug mode, /std:c++20 /Od:

    ucrtbased.dll()     Unknown
    ucrtbased.dll()     Unknown
    ucrtbased.dll()     Unknown
    vcruntime140d.dll() Unknown
>   TestMain.exe!std::invoke<void (__cdecl Base::*)(void),Base *>(void(Base::*)() && _Obj, Base * && _Arg1) Line 1573   C++
    TestMain.exe!std::thread::_Invoke<std::tuple<void (__cdecl Base::*)(void),Base *>,0,1>(void * _RawVals) Line 56 C++
    ucrtbased.dll()     Unknown
    kernel32.dll()      Unknown
    ntdll.dll()         Unknown

Replacing the conditional variable with a simple sleep will also cause this behavior:

std::this_thread::sleep_for(std::chrono::milliseconds(100));

It seems like this was caused by std::thread when calling _Cnd_do_broadcast_at_thread_exit (line 6 in the stack trace), but the thread didn't even start yet. Perhaps for some reason, the thread failed to start?

Here is the Godbolt link.

thedemons
  • 1,139
  • 2
  • 9
  • 25
  • Related: [Don't start a thread in a constructor](https://stackoverflow.com/questions/74148690/in-c-how-to-tell-in-base-class-methods-if-derived-class-finished-initializing#comment130915031_74148690). – Jason Oct 22 '22 at 04:37
  • @JasonLiam Yes, that question is the idea for this experiment. – thedemons Oct 22 '22 at 04:38
  • *"but the thread didn't even start yet"* -- what evidence do you have to back up this claim? – JaMiT Oct 22 '22 at 04:58
  • @JaMiT I set a breakpoint at the start of the thread and it was never hit, also, the program crashed as soon as the sleep was executed. I don't know about the inner working of `std::thread` so I'm not sure if by "the thread started" means that it might have started before calling my thread. – thedemons Oct 22 '22 at 05:02
  • @thedemons *"I set a breakpoint at the start of the thread"* -- doubtful. You probably set the breakpoint in `Derived::DerivedThread()` under the mistaken impression that it is the start of the thread. *"the program crashed as soon as the sleep was executed"* -- yes, I would expect that to be the latest point for the crash. – JaMiT Oct 22 '22 at 05:10
  • 1
    Not sure how to deal with this question. Mostly a duplicate? The crash is probably a match for [Where do "pure virtual function call" crashes come from?](https://stackoverflow.com/questions/99552/), but with the extra complications of the `std::thread` making it harder to see that the virtual function call is being made from the base class constructor, and of the condition variable ensuring that the base class constructor will not finish before the spawned thread crashes. Maybe those complications warrant an answer here? – JaMiT Oct 22 '22 at 05:15
  • 1
    @JaMiT You're right, this is indeed a duplicate of that question, I opened up the memory viewer to see that the vtable pointer of `this` in the base constructor point to the vtable of `Base` class, only after constructing the `Derived` class has the pointer point to the vtable of `Derived`. Weirdly enough, `&Base::DerivedThread` address is the same in both constructors, it points to a subroutine that `mov rax, [rcx]; jmp [rax]`, so if the thread is delayed enough, it jumps to the vtabe of the `Derived` class. – thedemons Oct 22 '22 at 05:29
  • The offset from the beginning of the vtable is always the same for the same virtual function. That is how choosing correct function works. – Revolver_Ocelot Oct 22 '22 at 09:37

0 Answers0