C++ lambda 函数调用纯虚函数

5

我正在尝试创建一个std::thread的包装类。该类提供了一个kick方法,以启动线程并调用一个纯虚函数。我使用一个派生类来调用这个kick方法,派生类也已经实现了这个虚函数。

class Executor
{
public:
    // constructor
    Executor();

    // destructor
    ~Executor();

    // kick thread execution
    void Kick();

private:
    // thread execution function
    virtual void StartExecution() = 0;

    // thread handle
    std::thread mThreadHandle;
};

以下是执行器类的实现
Executor::Executor()
{
    // Nothing to be done here
}

Executor::~Executor()
{
    if (mThreadHandle.joinable())
        mThreadHandle.join();
}

void Executor::Kick()
{
    // mThreadHandle = std::thread(&Executor::StartExecution, this);
    mThreadHandle = std::thread([this] {this->StartExecution();});
}

我正在使用一个继承了该类并实现了 StartExecution 方法的 Consumer 类。当我使用 kick 方法时,会显示纯虚函数调用,并且程序终止。

std::unique_ptr<Consumer> consumer = std::make_unique<Consumer>();
consumer->Kick();

在执行器的Kick方法中,我添加了一个断点并开始查找问题。它会两次到达以下代码行:

mThreadHandle = std::thread([this] {this->StartExecution();});

第一次是由于Kick方法,第二次是为了执行lambda函数。第一次我发现this指向Consumer类。但是当它到达lambda函数时,它变得混乱了,vptr指向了纯虚函数。
我对此很感兴趣,希望能够找到问题所在,而不仅仅是简单的答案。

1
你的Consumer类中有一个错误。由于你没有展示代码,所以无法提供更多信息。 - Pete Becker
请您添加Consumer类好吗? - Coral Kashri
通常情况下,只有在构造函数中调用虚函数时才会出现“pure virtual function called”的错误。您是否在构造函数中调用了“Kick()”函数? - kiloalphaindia
问题不在消费者类中,而是基本上我使用它的方式。抱歉,朋友们,我应该在这里添加整个实现。但由于那太长了,所以我认为我没有正确理解lambda函数,只添加了那一部分。 - theadnangondal
2个回答

5

根据我尝试过的,我猜测你的Consumer在线程执行之前被销毁了。

我将~Executor设置为虚函数,并添加了一些有关函数调用的打印语句。

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>

class Executor
{
public:
    // constructor
    Executor();

    // destructor
    virtual ~Executor();

    // kick thread execution
    void Kick();

private:
    // thread execution function
    virtual void StartExecution() { std::cout << "Executor::Kick\n"; }

    // thread handle
    std::thread mThreadHandle;
};

Executor::Executor()
{
    // Nothing to be done here
}

Executor::~Executor()
{
    std::cout << "~Executor\n";
    if (mThreadHandle.joinable())
        mThreadHandle.join();
}

void Executor::Kick()
{
    // mThreadHandle = std::thread(&Executor::StartExecution, this);
    mThreadHandle = std::thread([this] {this->StartExecution();});
}


class Consumer: public Executor {
public:
    ~Consumer() {
        std::cout << "~Consumer\n";
    }
private:
    virtual void StartExecution() { std::cout << "Consumer::Kick\n"; }
};

int main() {
    {
        std::cout << "1:\n";
        std::unique_ptr<Consumer> consumer = std::make_unique<Consumer>();
        consumer->Kick();
    }
    {
        std::cout << "2:\n";
        std::unique_ptr<Consumer> consumer = std::make_unique<Consumer>();
        consumer->Kick();
        std::cout << "Sleeping for a bit\n";
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return 0;
}

输出:

1:
~Consumer
~Executor
Executor::Kick
2:
Sleeping for a bit
Consumer::Kick
~Consumer
~Executor

查看这里运行

在销毁消费者之前休眠,可以让线程运行并调用正确的函数。一个“真正”的解决方法是确保消费者至少和线程本身一样长寿。由于线程存在于基类Executor中,这不能保证,因为派生类在基类之前被销毁。

cppreference(重点在我)中:

当从构造函数或析构函数(包括在成员初始化列表中构造或销毁类的非静态数据成员时)直接或间接调用虚函数,并且应用调用的对象是正在构建或销毁的对象时,所调用的函数是该构造函数或析构函数类中的最终覆盖程序,而不是在更派生类中覆盖它的一个。 换句话说,在构造或析构期间,更派生的类不存在。

这似乎适用于在构建/销毁过程中在不同线程中调用成员函数。


这表明依赖析构函数进行线程同步通常是一个不好的想法,因为在用户代码运行之前,析构函数中的一些编译器生成的代码可能已经产生了数据竞争。 - Arne Vogel

1
程序在线程有机会调用函数之前就已经耗尽了内存。如果你像这样更改代码。
void Executor::Kick()
{
    mThreadHandle = std::thread([this] {this->StartExecution();});
    this_thread::sleep_for(chrono::seconds(1)); // any number
}

这会起作用。

这正是为什么无法通过引用在捕获列表中传递this的确切原因。

现在谈谈你具体的问题

我对这个有问题,希望知道错在哪里,而不是简单的答案。

vPTR指向VTable,当类离开内存时,vPTR指向基类VTable,因此会发生这种情况。您可以通过在调用函数之前打印vTable地址来检查是否相同。


没错。谢谢!但是我已经投票选择了第一个答案为正确答案。 - theadnangondal
没问题。谢谢。 - Daksh Gupta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接