【问题标题】:Pure Virtual Method Called纯虚方法调用
【发布时间】:2011-01-11 03:50:16
【问题描述】:

编辑:已解决

我现在正在处理一个多线程项目,其中我有一个基础工作类,并从它继承了不同的工作类。在运行时,工作类变成线程,然后根据需要执行工作。

现在,我编写了一个 Director,它应该维护一个指向所有工作人员的指针数组,以便它可以从中检索信息,以及稍后修改其中的变量。

我通过创建一个指向基类指针的指针来做到这一点:

baseWorkerClass** workerPtrArray;

然后在 Director 的构造函数中,我动态分配一个指针数组指向基础工作类:

workerPtrArray = new baseWorkerClass*[numWorkers];

在每个worker线程的构造函数中,worker调用director中的一个函数,该函数用于将worker的指针存储在数组中。

director 存储指针的方式如下:

Director::manageWorker(baseWorkerClass* worker)
{
    workerPtrArray[worker->getThreadID()] = worker;
}

这是一个工人变体的例子。每个worker都继承自基础worker类,基础worker类包含应该存在于所有worker变体中的纯虚函数,以及一些在所有worker之间共享的变量。

class workerVariant : protected baseWorkerClass
{
    public:

    workerVariant(int id)
    : id(id)
    {
        Director::manageWorker(this);
    }

    ~workerVariant()
    {
    }

    int getThreadID()
    {
        return id;
    }

    int getSomeVariable()
    {
        return someVariable;
    }

    protected:

    int id;
    int someVariable
};

那么 baseWorkerClass 看起来像这样:

class baseWorkerClass
{
public:

    baseWorkerClass()
    {
    }

    ~baseWorkerClass()
    {
    }

    virtual int getThreadID() = 0;
    virtual int getSomeVariable() = 0;
};

在每个 worker 变体完成初始化后,我应该得到一个指向 baseWorkerClass 对象的指针数组。这意味着我应该能够,例如,使用某个工作人员的 ID 作为数组的索引来获取给定变量的值,如下所示:

workerPtrArray[5]->getSomeVariable(); // Get someVariable from worker thread 5

问题是这段代码导致 Windows 可执行文件崩溃,没有任何解释原因,在 Linux 中,它说:

调用纯虚方法
在没有活动异常的情况下调用终止
中止

我本可以发誓我在某个时候可以正常工作,所以我对自己搞砸了什么感到困惑。


有问题的实际未修改代码:

Worker 变体标头:http://pastebin.com/f4bb055c8
Worker 变体源文件:http://pastebin.com/f25c9e9e3

基础工人类标题:http://pastebin.com/f2effac5
基础工作者类源文件:http://pastebin.com/f3506095b

导演头:http://pastebin.com/f6ab1767a
导演源文件:http://pastebin.com/f5f460aae


编辑:额外信息,在 manageWorker 函数中,我可以从指针“worker”调用任何纯虚函数,它工作得很好。在 manageWorker 函数之外,当我尝试使用指针数组时,它会失败。

编辑:现在我考虑一下,线程的入口点是 operator()。 Director线程是在worker之前创建的,这可能意味着重载的括号运算符在被子类覆盖之前正在调用纯虚函数。我正在研究这个。

【问题讨论】:

    标签: c++ multithreading inheritance pure-virtual


    【解决方案1】:

    问题似乎是在workerVariant实例的构造函数中调用了Director::manageWorker

    Director::manageWorker(baseWorkerClass* worker) {
        workerPtrArray[worker->getThreadID()] = worker;
    }
    

    大概getThreadID() 不是纯虚函数,否则您会(希望!)在workerVariant 中没有覆盖它的编译器错误。但是getThreadID() 可能会调用您应该覆盖但正在抽象类中调用的其他函数。您应该仔细检查 getThreadID() 的定义,以确保在正确初始化子类之前,您没有做任何取决于子类的不良行为。

    更好的解决方案可能是将这种多阶段初始化分离到一个单独的方法中,或者将DirectorbaseWorkerClass 设计为不具有这种初始化时间的相互依赖关系。

    【讨论】:

    • 感谢您的意见。实际上,关于 getThreadID 方法,这只是我的不一致。这不是我正在使用的实际代码,它只是我几分钟前为了清楚简洁地说明我的观点而写的东西。 getThreadID 方法 纯虚拟的,并且在每个worker 变体中 被覆盖。 getThreadID 方法所做的只是返回线程的“id”变量。
    • 当一个对象在构造中(当构造函数被执行时),虚函数机制永远不会调用从该对象类型派生的类的虚函数。这是因为派生类的构造函数尚未执行。所以从构造函数调用派生类的方法是行不通的。解决这个问题的方法是两阶段构造,最好隐藏在包装对象后面。 (顺便说一句,销毁也是如此。)
    • 哦,废话,你说得对,我没想到。让我在这里做一个快速测试,我会报告回来。 (还有一个小时的睡眠时间。)
    • 也许您可以发布一个实际的完整示例来重现问题并合并更改?
    【解决方案2】:

    纯虚函数调用是指在子类的构造函数开始执行之前调用基类中的纯成员。在非多线程程序中,这意味着直接或间接在基类的构造函数中。在多线程程序中,当构造函数在构造函数中启动线程并且系统在终止构造函数之前执行线程时,也会发生这种情况。

    【讨论】:

      【解决方案3】:

      如果没有看到完整的代码,我会冒险猜测您正在走出workerPtrArray 指向的内存块的边界。这肯定是有道理的,因为它抱怨调用纯虚函数。如果被取消引用的内存是垃圾,那么运行时根本无法理解它,就会发生奇怪的事情。

      尝试将断言放在要取消引用数组的关键位置,以确保索引有意义。 IE。限制为 4 个工作人员,并确保 id 小于 4。

      【讨论】:

      • 既然你提到了它,我记得 Visual Studio 调试器抱怨正在访问的数组元素超出范围。我不明白为什么会发生这种情况,因为我已经检查并仔细检查了我的索引,它们都在范围内。我将在几分钟后发布我的实际代码。
      • 您可以尝试使用 std::vector 而不是双指针。我总是需要查找用于分配这样的数组的“新”语法。此外,这将更加清晰,并减轻您管理数组的负担。
      【解决方案4】:

      在初始化期间,类只是部分构造。具体来说,构造函数必须从最祖先的类开始执行,以便每个派生类的构造函数都可以安全地访问其基成员。

      这意味着部分构造的类的 vtable 不能处于其最终状态 - 如果允许虚方法调用派生类,则将在调用类构造函数之前调用这些方法。

      这意味着,在构造过程中,纯虚函数实际上是纯虚函数。现代 c++ 编译器在捕捉这一点方面做得越来越好 - 但在许多情况下,它可能以编译器不会注意到错误的方式“隐藏”非法调用。

      故事的寓意:不要在构造函数中执行任何会调用虚函数的操作。它只是不会做你所期望的。即使它不纯净。

      【讨论】:

        【解决方案5】:

        我没有在您的任何代码示例中看到正在构造的变体类。您确定传递的 id 在工作人员数组的范围内吗?另外,您正在使用“新”构造对象,对吗?如果你在栈上构造了对象,它会向Director注册自己,但是在构造函数返回后,对象会立即销毁,但Director会保留它指向栈上对象的指针。

        此外,您的 baseWorkerClass 析构函数应该与 workerVariant 析构函数一起是虚拟的,以确保在您删除 baseWorkerClass 数组时它们被调用。

        根据我对另一个问题的评论,考虑使用 std::vector 代替双指针。更容易维护和理解,无需维护数组。

        您似乎在这里添加了不必要的抽象层。我不认为 id 真的应该是子类接口的一部分。我认为这样的事情可能更适合你:

        class baseWorkerClass
        {
        public:
        
            baseWorkerClass(int id) :
                id( id )
            {
            }
        
            virtual ~baseWorkerClass()
            {
            }
        
            int getThreadID(){ return id; };
            virtual int getSomeVariable() = 0;
        
        protected:
            int id;
        };
        
        class workerVariant : protected baseWorkerClass
        {
            public:
        
            workerVariant(int id) :
                baseWorkerClass( id )
            {
                Director::manageWorker(this);
            }
        
            virtual ~workerVariant()
            {
            }
        
            int getSomeVariable()
            {
                return someVariable;
            }
        
        protected:
            int someVariable
        };
        

        【讨论】:

          【解决方案6】:

          你不会有机会在对象被破坏后访问它们吗?因为在销毁过程中,vtable 指针逐渐“回滚”,因此 vtable 条目将指向基类的方法,其中一些是抽象的。删除对象后,内存可以在基类的析构过程中保持原样。

          建议你试试内存调试工具,比如valgrind或者MALLOC_CHECK_=2。同样在 unix 上,很容易获得此类致命错误的堆栈跟踪。只需在 gdb 或 TotalView 下运行您的应用程序,当错误发生时它会自动停止,您可以查看堆栈。

          【讨论】:

            【解决方案7】:

            我曾收到此错误消息,虽然它与提问者的具体情况无关,但我添加此消息希望对其他人有用:

            我通过干净的构建解决了这个问题。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2012-05-29
              • 2012-06-14
              • 1970-01-01
              • 2011-09-08
              • 1970-01-01
              • 2014-03-03
              • 2017-05-24
              相关资源
              最近更新 更多