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

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

#include <iostream>
using namespace std;

class ActionBase {
 public:
    ~ActionBase() { } // Comment out and works as expected

    virtual void invoke() const = 0;
};

template <class T>
class Action : public ActionBase {
 public:
    Action( T& target, void (T::*action)())
     : _target( target ), _action( action ) { }

    virtual void invoke() const {
        if (_action) (_target.*_action)();
    }

    T&   _target;
    void (T::*_action)();
};

class View {
 public:
    void foo() { cout << "here" << endl; }
};

class Button : public View {
 public:
    Button( const ActionBase& action )
     : _action( action ) { }

    virtual void mouseDown() {
        _action.invoke();
    }

 private:
    const ActionBase& _action;
};

int main( int argc, char* argv[] )
{
    View view;
    Button button = Button( Action<View>( view, &View::foo ) );
    button.mouseDown();

    return 0;
}

person Community    schedule 01.01.2010    source источник
comment
Вы должны сделать деструктор ActionBase virtual. См. parashift.com/c++-faq-lite/virtual. -functions.html#faq-20.7   -  person Alok Singhal    schedule 01.01.2010
comment
Добавьте деструктор в Action‹›, который распечатывает, когда он уничтожен, и вы увидите, как он умирает до того, как его можно будет использовать, и, таким образом, вызывает неопределенное поведение.   -  person Martin York    schedule 01.01.2010


Ответы (3)


У вас есть неопределенное поведение. Поскольку параметр ctor Button является const& из временного объекта, он уничтожается в конце этой строки, сразу после завершения ctor. Позже вы используете _action, после того как dtor Action уже запущен. Поскольку это UB, реализации разрешено все, что угодно, и, по-видимому, ваша реализация делает что-то немного отличающееся в зависимости от того, есть ли у вас тривиальный dtor в ActionBase или нет. Вы получаете сообщение «чистый виртуальный вызов», потому что реализация обеспечивает поведение для прямого вызова ActionBase::invoke, что происходит, когда реализация изменяет указатель vtable объекта в dtor Action.

Я рекомендую использовать boost.function или аналогичная библиотека «обратных вызовов действий» (boost имеет signals и signals2, например).

person Community    schedule 01.01.2010
comment
Насчет неопределенности... Проверил сборку, вроде бы gcc не разрушал Action иерархию совсем, если бы деструктор был тривиальным --т.е. сгенерированный компилятором. Таким образом, код правильно поймал указатель vtable без деструктора, так как он остался в памяти не перезаписанным. - person P Shved; 01.01.2010
comment
Это невозможно правильно понять; это неопределенное поведение, и все ставки отключены во всей программе (насколько стандартные гарантии). Однако то, что вы нашли, имеет смысл: в случае тривиального dtor указатель vtable не обновляется, поэтому нет сообщения об ошибке. - person ; 01.01.2010
comment
Демоны летающие из носа и все такое. - person greyfade; 01.01.2010
comment
Спасибо, Роджер, теперь я понимаю, что происходит. Я только недавно начал использовать const для нетривиального кода, поэтому область действия мне была не совсем ясна. - person ; 01.01.2010

Поставьте точку останова на деструкторе и станет понятно, что происходит. Да, вы передаете временный экземпляр Action‹> конструктору Button. Он уничтожается после запуска конструкции кнопки. Напишите это так, и проблема исчезнет:

View view;
Action<View> event(view, &View::foo);
Button button = Button( event ); 
button.mouseDown();

Что ж, это непрактичное решение, событие не будет соответствовать реальному вызову mouseDown. Конструктор Button должен будет создать копию аргумента «событие» или управлять указателем на делегат.

person Hans Passant    schedule 01.01.2010
comment
+1. Но почему 'Action‹View› event = Action‹View›(view,&view::foo)'? Почему бы не событие 'Action‹View›(view,&View::foo)'; - person Martin York; 01.01.2010

Класс с виртуальными функциями всегда должен иметь виртуальный деструктор, поэтому ~ActionBase() должен быть виртуальным (как и ~Action()). Если вы включите дополнительное предупреждение компилятора, вы получите предупреждение об этом.

По сути, из-за правил поиска деструктор вызывается для типа, который компилятор знает не может быть создан (чистый виртуальный), поэтому он знает, что что-то пошло не так.

Я уверен, что кто-то другой может объяснить лучше, чем я :)

person James    schedule 01.01.2010
comment
Хотя вы очень правы в своем совете, это не имеет ничего общего с проблемой в теме. - person P Shved; 01.01.2010
comment
Спасибо за быстрый ответ, ребята. С виртуальным деструктором это по-прежнему компилируется и выполняется с помощью чисто виртуального метода, называемого error с gcc-ubuntu-64 4.3.3. Первый выдает ошибку, второй нет. Что принципиально отличается между ними? Второй берет адрес временный, что конечно не есть хорошо. Первый тоже? Кнопка button = Button( Action‹View›( view, &View::foo ) ); Действие‹Просмотр› action( view, &View::foo ); Кнопка button2 = Кнопка (действие); - person ; 01.01.2010
comment
Майк: Проблема не в том, чтобы взять адрес временного объекта, а в том, чтобы использовать ссылку после окончания срока службы объекта, на который она ссылается (см. мой ответ). - person ; 01.01.2010