Search code examples
c++multithreadingshared-ptr

Pure virtual call from another thread using shared pointer


I am finding it very strange. Please, help me to explain this. I have a class which starts infinite loop in a separate thread, and two classes which inherit it. One of the classes implements the interface to be triggered outside as std::shared_ptr, and another one class hold this interface as std::weak_ptr. Please look at the code below. Sorry for a lot of code, I was trying to be as short as it possible to reproduce the error. Why sometimes have I pure virtual call in Sender::notify function? As far as I know std::shared_ptr is reentrant.

#include <iostream>
#include <memory>
#include <thread>
#include <atomic>
#include <list>
#include <mutex>


class Thread : private std::thread {
    std::atomic_bool run {true};
public:
    Thread() : std::thread([this](){ thread_fun(); }) {}

    void thread_fun() {
        while (run) loop_iteration();
    }

    virtual void loop_iteration() = 0;

    virtual ~Thread() {
        run.exchange(false);
        join();
        std::cout << "Thread released." << std::endl;
    }
};

class Sender : public Thread {
public:
    class Signal{
    public:
        virtual void send() = 0;
        virtual ~Signal(){}
    };

    void add_receiver(std::weak_ptr<Signal> receiver) {
        std::lock_guard<std::mutex> lock(receivers_mutex);
        receivers.push_back(receiver);
    }

    void notify() {
        std::lock_guard<std::mutex> lock(receivers_mutex);
        for (auto r : receivers)
            if (auto shp = r.lock())
                shp->send(); //Somethimes I get the pure virtual call here
    }

private:
    std::mutex receivers_mutex;
    std::list<std::weak_ptr<Signal>> receivers;

    void loop_iteration() override {
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        notify();
    }
};

class Receiver : public Thread, public Sender::Signal {
    std::atomic_bool notified {false};

public:
    void send() override {
        notified.exchange(true);
    }

private:
    void loop_iteration() override {
        std::this_thread::sleep_for(std::chrono::milliseconds(250));
        std::cout << "This thread was " << (notified? " " : "not ") << "notified" << std::endl;
    }
};


int main() {
   std::shared_ptr<Thread>
           receiver = std::make_shared<Receiver>(),
           notifier = std::make_shared<Sender>();

   std::dynamic_pointer_cast<Sender>(notifier)->add_receiver(
               std::dynamic_pointer_cast<Sender::Signal>(receiver));

   receiver.reset();

   notifier.reset();

   return 0;
}

Solution

  • Polymorphism doesn't work as you may expect during construction and destruction. The current type is the most derived type that still exists. When you are in Thread::~Thread the Sender part of your object has already been completely destroyed so it wouldn't be safe to call its overrides.

    When thread_fun tries to run loop_iterator() before the constructor finishes or after the destructor starts, it will not polymorphically dispatch, but instead it will call Thread::loop_iteration which is a pure virtual function (= 0).

    See https://en.cppreference.com/w/cpp/language/virtual#During_construction_and_destruction

    Here is a demonstration of this : https://godbolt.org/z/4vsPGYq97

    The derived object is destroyed after one second, at which point you see the output change indicating that the virtual function being called changes at that point.

    I'm not sure if this code is valid, or if destroying the derived part of the object while one of its member function is being executed is Undefined Behavior.