Call a derived member function from parent class

Viewed 125

I need to call B::f() in the thread member function. What I get is "pure virtual method called". How can I do this?

I assume all this happens because of &A::thread_f in the A initializer list where I explicitly name a scope.

class A {
protected:
    std::thread _thread;

    A() : _thread(&A::thread_f, this) { }

    ~A() { 
        _thread.join();
    }

    virtual void f() = 0;

    void thread_f() {
        f();
    }
};

class B : public A {
protected:
    void f() override {
        std::cout << "B::f()" << std::endl;
    }
};
3 Answers

as @Evg says, by starting a thread,A ctor call member function f of B before constructing B.

The solution is to start thread after B is constructed.

#include<iostream>
#include<vector>
#include <stdlib.h>
#include <thread>

using namespace std;
class A {
public:
    ~A() {
        _thread.join();
    }
    void start()
    {
        _thread = std::thread(&A::thread_f, this);
    }
protected:
    std::thread _thread;
    A()
    {
    }

    virtual void f() = 0;
    void thread_f() {
        f();
    }
};

    class B : public A {
    protected:
        void f() override
        {
            std::cout << "B::f()" << std::endl;
        }
    };

void main() {
    A *pA=new B;
    pA->start();
    delete pA;
}

The reason this doesn't work is due to c++'s order of construction. When a class is constructed, any base classes are constructed before the derived class.

This means that when you call the virtual function f() in your base class, class B has not yet been constructed. This means the only available option is to try and call A's implementation of f(). The problem here is that A doesn't have an implementation of f() and thus the error occurs.

For ways to solve this, take a look here. I would suggest moving your call to f() into a separate initialisation stage that can be called after construction, which will mean both base and derived classes will have been fully constructed by the time of the call.

There are two problems here, both related to the absence of synchronization.

The first problem is that a virtual call could be made on an object that has not yet been fully constructed. A new thread is created before the construction of B, f() is called inside that thread and there is no guarantee that by that time B is fully constructed. The behaviour is undefined.

Going into some implementation details, virtual calls are typically implemented using virtual tables. A pointer to a virtual table is set during B's construction by a compiler, and if you call f() before that pointer is correctly set, you're likely to call A::f() instead of B::f(). Calling A::f() generates the "pure virtual method called" error message. Note that f() is called inside a thread, not inside a constructor, so that the call will be virtual and won't be resolved to A::f() at compile time.

The second problem is that a virtual call could be made on an object that has already been destructed. The destructor of A, in which you join a thread, is called after the destruction of B. Again, making a virtual call on a destructed object is undefined behaviour.

To get some insight into what's going on, you can add two mutexes:

std::mutex m1, m2;

struct A {
    std::thread thread;
    
    A() : thread(&A::thread_f, this) { 
        std::cout << "A()" << std::endl;
    }
    
    ~A() { 
        std::cout << "~A()" << std::endl;
        thread.join();
    }

    virtual void f() = 0;
    
    void thread_f() {
        std::cout << "thread_f()" << std::endl;        
        m1.lock();
        f();
        m2.unlock();
    }
};

struct B : A {
    B() {
        std::cout << "B()" << std::endl;
        m1.unlock();
    }

    ~B() {
        m2.lock();    
        std::cout << "~B()" << std::endl;
    }

    void f() override {
        std::cout << "B::f()" << std::endl;
    }
};

int main() {
    m1.lock();
    m2.lock();
    B b;
}

Now you'll see the expected "B::f()" output. Demo. I would hardly call this a solution, though.

The answer by kenash0625 addresses the first problem, but it doesn't address the second one.

Related