Вызывается чистый виртуальный метод

EDIT: SOLVED

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

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

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

baseWorkerClass** workerPtrArray;

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

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


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

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

Ответ 1

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

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

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

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

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

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

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

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

Ответ 5

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

Кроме того, ваш деструктор baseWorkerClass должен быть виртуальным вместе с destructor 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
};

Ответ 6

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

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

Ответ 7

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

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