2016-10-18 4 views
2

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

struct autoregister { 
    autoregister() { callback_manager.register(this); } 
    ~autoregister() { callback_manager.deregister(this); } 
    virtual void call_me()=0; 
}; 

Но это кажется ненадежным для меня, я подозреваю, что в нем есть несколько условий гонки. 1) Когда callback_manager видит указатель, call_me все еще не поддается удалению, и он может занимать произвольное количество времени, пока объект не завершит конструкцию, 2) к тому времени, когда вызывается дерегистер, вызывается деструктор производного объекта, поэтому обратные вызовы должны не называть.

Одна из вещей, о которых я думал, заключалась в проверке внутри callback_manager, если указатель call_me действителен или нет, но я не могу найти стандартный способ получения адреса call_me или чего-то еще. Я думал о сравнении typeid (указатель) с typeid (autoregister *), но может существовать абстрактный класс inbetween, делая этот ненадежный, полученный: public middle {}; middle: public autoregister {} ;, конструктор середины может потратить час, например. загрузку SQL или поиск google, а обратный вызов видит, что это не базовый класс, и считает, что обратный вызов может быть вызван, и бум. Это можно сделать?

Q1: Есть ли другие условия гонки?

Q2: как это сделать (без условий гонки, неопределенного поведения и других ошибок) без запроса производного класса для регистрации регистра вручную?

Q3: как проверить, может ли виртуальная функция вызываться на указателе?

+0

Хороший вопрос. Я всегда звоню на регистрацию и регистрирую мьютекс уровня экземпляра. – Bathsheba

+0

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

+0

@JerryCoffin, пожалуйста, прочитайте вопрос, я, очевидно, заинтересован в решении на основе базового класса, по каким-либо причинам. До сих пор наиболее релевантный ответ на мой вопрос - единственный без кода, по user634175, другие ответы дают альтернативы, каждый из которых имеет разные компромиссы. Но голосуйте, как вам угодно, вашей прерогативой. Теперь я жду, чтобы увидеть, что-нибудь лучше, и я соглашусь, какой бы ответ ни был лучше. – Martin

ответ

1

В конструкторе вашего авторегистра, поскольку объект еще не был полностью сконструирован, указатель «this» опасен для передачи вызова callback_manager. Я бы порекомендовал немного другой дизайн.

struct callback { 
    virtual void call_me() = 0; 
} 

struct autoregister { 
    callback*const callback_; 

    autoregister(callback*const _callback) 
    : callback_(_callback) { 
    callback_manager.register(callback_); 
    } 

    ~autoregister() { 
    callback_manager.deregister(callback_); 
    } 
}; 

Q1: Есть ли другие условия гонки?

Возможно. Ваш callback_manager должен быть синхронизирован, если несколько потоков могут его использовать. Но моя версия самого авторегистра не имеет состояния гонки.

Q2: как это сделать (без условий гонки, неопределенного поведения и других ошибок) без запроса производного класса для регистрации регистра вручную?

Мой код, как я думаю, может сделать это правильно.

Q3: как проверить, может ли виртуальная функция вызываться на указателе?

В моем коде не обязательно. Но в целом вы можете оставить в классе флаг, который установлен в списке инициализации false и установить true, когда он будет готов к вызову.

+0

, если производный класс передает это в него, что также проблематично, обратный вызов все же может произойти между деструктором производного класса и деструктором авторегистра. Я думаю, что это гонка в вашем авторегистраторе. Как это будет использоваться в производных? – Martin

+0

Первоначально вы использовали необработанные указатели, поэтому я использовал необработанные указатели. Предполагается, что любой объект «обратного вызова» должен пережить объект авторегистра, который сохраняет указатель обратного вызова. Если в другом месте вашего кода у вас нет такой гарантии, один из способов сделать это - изменить 'callback * const callback_' внутри авторегистра с 'shared_ptr callback_'. –

+0

Я получаю от autoregister, callback или обоих? Как я это использовал? – Martin

2

Вы должны отделить обратный вызов и регистрационный дескриптор. Никто, кроме callback_manager, не нуждается в методе call_me, так почему бы его не видеть снаружи? Используйте std::function как обратный вызов, потому что это очень удобно: любые вызываемые могут быть преобразованы в него, а lambdas очень удобны. Верните объект Handle из метода регистрации обратного вызова. Единственный метод Handle будет иметь деструктор, из которого вы удалите обратный вызов.

class Handle { 
public: 
    explicit Handle(std::function<void()> deleter) 
     : deleter_(std::move(deleter)) 
    {} 

    ~Handle() 
    { 
     deleter_(); 
    } 
private: 
    std::function<void()> deleter_; 
}; 

class Manager { 
public: 
    typedef std::function<void()> Callback; 

    Handle subscribe(Callback callback) { 
     // NOTE: use mutex here if this method is accessed from multiple threads 
     callbacks_.push_back(std::move(callback)); 
     auto itr = callbacks_.end() - 1; 
     // NOTE If Handle lifetime can exceed Manager lifetime, store handlers_ in std::shared_ptr and capture a std::weak_ptr in lambda. 
     return Handle([this, itr]{ 
      // NOTE: use mutex here if this method is accessed from multiple threads 
      callbacks_.erase(itr); 
     }); 
    } 

private: 
    std::list<Callback> callbacks_; 
}; 

Q1: есть ли другие условия гонки?

Callback/Handle может пережить callback_manager и попытается отказаться от подписки на удаленный объект. Это может быть исправлено либо политикой (всегда отписывайте все перед удалением менеджера), либо используя слабые указатели.

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

Q2: как сделать это правильно (без условий гонки, неопределенное поведение и другие ошибки), не спрашивая производный класс вызвать регистр вручную?

См. Выше.

Q3: как проверить, можно ли вызвать виртуальную функцию указателем?

Это невозможно.

+0

, похоже, не отвечает на мой вопрос и, кажется, добавляет больше шансов на условия гонки, то опять же, может быть, я не понимаю, что вы имеете в виду. – Martin

+0

Нет, это не так. Но для написания образца кода требуется время. Подожди немного. –

+0

вы также можете добавить пример кода для производного класса? .. также, между прочим, итератор выживает изменений в списке? – Martin

1

Состояние гонки, когда две нити пытаются что-то сделать одновременно, и результат зависит от точного времени. Я не думаю, что в этом фрагменте есть условие гонки, потому что this доступен только для потока, выполняющего конструктор. Однако может быть состояние гонки в callback_manager, но вы не разместили код для этого, поэтому я не могу сказать.

Здесь есть еще одна проблема: объекты строятся от базы до самой производной, поэтому в то время, когда работает конструктор autoregister, виртуальный call_me не может быть вызван. См. this FAQ entry. Невозможно проверить, будет ли вызов виртуальной функции работать отдельно от обеспечения полного построения класса.

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

+0

callback_manager может быть гоновым, но он может вызвать обратный вызов на объект, который не сконструирован, или наполовину разрушенный, что я имел в виду под условием гонки, так как это редко случается нормально. – Martin

0

Я думаю, что @Donghui Zhang находится на правильном пути, , но еще не совсем там еще, так сказать. К сожалению, то, что он сделал, вводит свой собственный набор подводных камней - например, если вы передаете адрес локального объекта в ctor autoregister, вы все равно можете зарегистрировать объект обратного вызова, который сразу выходит из области видимости (но не обязательно немедленно получить регистрацию).

Я также считаю сомнительным (в лучшем случае) определить интерфейс обратного вызова, используя call_me как функцию-член для вызова при обратном вызове. Если вам нужно определить тип, который можно вызвать как функцию, C++ уже определяет имя для этой функции: operator(). Я собираюсь применить это вместо call_me.

Чтобы сделать все это, я думаю, что вы действительно хотите использовать шаблон вместо наследования:

template <class T> 
class autoregister { 
    T t; 
public: 
    template <class...Args> 
    autoregister(Args && ... args) : t(std::forward(args)...) { 
     static_assert(std::is_callable<T>::value, "Error: callback must be callable"); 

     callback_manager.register(t); 
    } 

    ~autoregister() { callback_manager.deregister(t); } 
}; 

Вы бы использовать это что-то вроде этого:

class f { 
public: 
    virtual void operator()() { /* ... */ } 
}; 

autoregister<f> a; 

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

Это также поддерживает передачу аргументов через autoregister в конструктор объекта он содержит, так что вы можете иметь что-то вроде:

class F { 
public: 
    F(int a, int b) { ... } 
    void operator()() {} 
}; 

autoregister<F> f(1,2); 

... и 1, 2 будет пропущен из autoregister в F, когда это построен. Также обратите внимание, что это не пытается обеспечить конкретную подпись для функции обратного вызова. Если бы вы, например, изменили свой менеджер обратного вызова, чтобы сделать обратный вызов как int r = callback(1);, тогда код будет компилироваться, только если объекты обратного вызова, которые вы зарегистрировали, могут быть вызваны с аргументом int и возвращены int (или что-то, что может быть неявно преобразованный в int, во всяком случае). Компилятор выполнит обратный вызов, имеющий подпись, совместимую с тем, как она вызвана. Единственный большой недостаток здесь заключается в том, что если вы передаете тип, который может быть вызван, но (например) не может быть вызван с параметрами (-ами), которые пытается выполнить callback-менеджер, вы получите сообщение об ошибке не так читайте, как хотелось бы.

+0

делает то, что я хочу, но теперь я должен помнить, использовать autoregister вместо производного, я мог бы использовать typedef, тогда, если я хочу второй обратный вызов, это будет register_boom > возможно, это только я, но это не так, Кажется правильным. – Martin

+0

@Martin: В одном случае, когда вы определяете свой тип, вы должны помнить, что он выведен из класса 'autoregister'. В другом случае вы создаете шаблон над классом 'autoregister'. Я не вижу существенной разницы. Не уверен, что я понимаю, о чем вы говорите, с «вторым обратным вызовом». Если вы имеете в виду несколько менеджеров обратного вызова, похоже, вы расширяете вопрос до того, что даже не рассматривалось в вопросе и не поддерживалось его дизайном. Если вы действительно заботитесь об этом, его довольно легко разместить. –

+0

класс объявляется один раз, где вы наследуете и меняете наследование, возможно, одну строку, но ваше решение требует изменения и запоминания регистрации для каждого экземпляра, и ничто не мешает генерировать код, созданный рядом с авторегистратором , что может быть нежелательным и принуждаться к наследованию. Пользователь класса не должен этого делать, класс должен применять свою собственную политику. ведь класс требует обратного вызова, а не пользователя класса. – Martin

Смежные вопросы