Вызывается чистый виртуальный метод - PullRequest
10 голосов
/ 30 января 2010

РЕДАКТИРОВАТЬ: РЕШЕНО

Я сейчас работаю над многопоточным проектом, где у меня есть базовый рабочий класс с различными рабочими классами, которые наследуются от него. Во время выполнения рабочие классы становятся потоками, которые затем выполняют работу по мере необходимости.

Теперь у меня есть директор, которого я написал, который должен поддерживать массив указателей для всех работников, чтобы он мог получать от них информацию, а также изменять переменные внутри них позже.

Я сделал это, создав указатель на указатель базового класса:

baseWorkerClass** workerPtrArray;

Затем в конструкторе Director я динамически выделяю массив указателей на базовый рабочий класс:

workerPtrArray = new baseWorkerClass*[numWorkers];

В конструкторе каждого рабочего потока рабочий вызывает в директоре функцию, которая предназначена для хранения указателя этого рабочего в массиве.

Вот как директор хранит указатели:

Director::manageWorker(baseWorkerClass* worker)
{
    workerPtrArray[worker->getThreadID()] = 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;
};

После завершения инициализации каждого рабочего варианта я должен получить массив указателей на объекты baseWorkerClass. Это означает, что я должен иметь возможность, например, получить значение заданной переменной у определенного работника, используя его идентификатор в качестве индекса для массива, например:

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

Проблема в том, что этот код вызывает сбой в исполняемом файле Windows без объяснения причин, а в Linux он говорит:

чисто виртуальный метод с именем
завершить вызов без активного исключения
Прервано

Я мог бы поклясться, что в какой-то момент это сработало, так что я запутался в том, что я облажался.


Фактический неизмененный код, имеющий проблему:

Рабочий вариант заголовка: http://pastebin.com/f4bb055c8
Исходный файл варианта работника: http://pastebin.com/f25c9e9e3

Заголовок базового рабочего класса: http://pastebin.com/f2effac5
Исходный файл базового рабочего класса: http://pastebin.com/f3506095b

Заголовок директора: http://pastebin.com/f6ab1767a
Исходный файл директора: http://pastebin.com/f5f460aae


РЕДАКТИРОВАТЬ: Дополнительная информация, в функции manageWorker, я могу вызвать любую из чисто виртуальных функций из указателя «работник», и она работает просто отлично. За пределами функции manageWorker, когда я пытаюсь использовать массив указателей, происходит сбой.

РЕДАКТИРОВАТЬ: Теперь, когда я думаю об этом, точка входа потока является оператор (). Поток Director создается перед рабочими, что может означать, что перегруженный оператор скобок вызывает чисто виртуальные функции, прежде чем они могут быть переопределены дочерними классами. Я смотрю на это.

Ответы [ 7 ]

12 голосов
/ 30 января 2010

Проблема заключается в том, что Director::manageWorker вызывается в конструкторе workerVariant экземпляров:

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

Предположительно getThreadID() не является чисто виртуальной функцией, иначе вы (надеюсь!) Получили бы ошибку компилятора из-за того, что она не была переопределена в workerVariant. Но getThreadID() может вызывать другие функции, которые вы должны переопределить, но которые вызываются в абстрактном классе. Вы должны дважды проверить определение getThreadID(), чтобы убедиться, что вы не делаете ничего плохого, что будет зависеть от дочернего класса, прежде чем он будет правильно инициализирован.

Лучшим решением может быть разделение такого рода многоэтапной инициализации на отдельный метод или создание Director и baseWorkerClass таким образом, чтобы они не имели такого рода взаимозависимости времени инициализации.

3 голосов
/ 30 января 2010

Не видя полного кода, я рискнул бы предположить, что вы выходите за границы блока памяти, на который указывает workerPtrArray. Это, безусловно, имеет смысл, поскольку жалуется на то, что вызывается чисто виртуальная функция. Если разыменовываемая память является мусором, среда выполнения вообще не может ее понять, и происходят странные вещи.

Попробуйте поместить утверждения в критические места, где вы разыменовываете массив, чтобы убедиться, что индексы имеют смысл. То есть ограничить до 4 работников и убедитесь, что идентификатор меньше 4.

2 голосов
/ 30 января 2010

Во время инициализации классы создаются только частично. В частности, конструкторы должны выполняться из самого класса-предка, чтобы каждый конструктор производного класса мог безопасно обращаться к своим базовым членам.

это означает, что виртуальная таблица частично сконструированного класса не может находиться в своем конечном состоянии - если бы виртуальным методам было разрешено вызывать производные классы, эти методы были бы вызваны до вызова конструктора классов.

Это означает, что во время построения чисто виртуальные функции фактически являются чисто виртуальными. Современные компиляторы с ++ лучше справляются с этой задачей, но во многих случаях возможно «похоронить» нелегальный вызов таким образом, чтобы компилятор не заметил ошибку.

Мораль истории: не делайте в конструкторе ничего, что вызовет виртуальную функцию. Это просто не будет делать то, что вы ожидаете. Даже когда это не чисто.

2 голосов
/ 30 января 2010

Чистая виртуальная функция с именем означает, что член, который является чистым в базовом классе, вызывается до того, как начнется выполнение конструктора потомка. В не многопоточных программах это означает прямо или косвенно в конструкторе базового класса. В многопоточной программе это также может произойти, когда конструктор запускает поток в конструкторе, а система выполняет поток перед завершением работы конструктора.

1 голос
/ 31 января 2010

Я не видел, чтобы вариантный класс создавался ни в одном из ваших примеров кода. Вы уверены, что передаваемый идентификатор находится в пределах диапазона для рабочего массива? Кроме того, вы создаете объекты, используя 'new', верно? Если вы сконструировали объект в стеке, он зарегистрирует себя в Director, но после возврата конструктора объект будет немедленно уничтожен, но Director сохранит свой указатель на объект, который был в стеке.

Кроме того, ваш деструктор baseWorkerClass должен быть виртуальным вместе с деструктором workerVariant, чтобы они вызывались при удалении массива baseWorkerClass.

Из моего комментария к другому вопросу рассмотрите возможность использования std :: vector вместо двойного указателя. Проще поддерживать и понимать, и избавляет от необходимости поддерживать массив.

Похоже, вы добавили здесь ненужный слой абстракции. Я не думаю, что идентификатор действительно должен быть частью интерфейса подкласса. Я думаю, что-то вроде этого может быть лучше для вас:

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
};
0 голосов
/ 15 июля 2011

Я получил это сообщение об ошибке один раз, и хотя оно не относится к точному случаю спрашивающего, я добавляю это в надежде, что оно может быть полезно для других:

Я исправил проблему, выполнив чистую сборку.

0 голосов
/ 31 января 2010

Вы случайно не получаете доступ к объектам после того, как они разрушены? Потому что во время уничтожения указатели vtable постепенно «откатываются», так что записи vtable будут указывать на методы базового класса, некоторые из которых являются абстрактными. После удаления объекта память можно оставить такой же, какой она была во время деструктора базового класса.

Я предлагаю вам попробовать средства отладки памяти, например valgrind или MALLOC_CHECK_ = 2 . Также в Unix довольно легко получить трассировку стека для таких фатальных ошибок. Просто запустите ваше приложение под GDB или TotalView , и в тот момент, когда произойдет ошибка, она автоматически остановится, и вы сможете посмотреть на стек.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...