правильный способ создать абстрактный базовый класс, который регистрирует обратный вызов

#c #callback

#c #обратный вызов

Вопрос:

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

 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(pointer) с typeid(autoregister *), но между ними может быть абстрактный класс, что делает это ненадежным, derived : public middle {}; middle : public autoregister {};, конструктор middle может потратить час, например, на загрузку SQL или поиск в Google, иобратный вызов видит, что это не базовый класс, и думает, что обратный вызов может быть вызван, и бум. Можно ли это сделать?

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

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

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

Комментарии:

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

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

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

4. @Martin: один из самых основных моментов объектно-ориентированного программирования — научиться отделять интерфейс от реализации. Интерфейс определяет поведение. Вопрос, который вы должны задать, заключается в том, как получить желаемое поведение чисто, не указывать реализацию и каким-то образом заставить его делать противоположное тому, для чего он предназначен.

5. @JerryCoffin однако я спрашиваю: «Могу ли я сделать это таким образом? Если я могу, какие подводные камни?», Если я получу альтернативы, хорошо, но причина вопроса в том, чтобы узнать, возможен ли другой способ. Все остальные способы были мне известны, может быть, кто-то покажет что-то новое. Чистый код и чистый дизайн, по-видимому, вопрос мнения, я бы не стал использовать оператор для этой очистки, иногда это может быть хорошо, но ИМХО, имея функцию, название которой говорит, для чего она предназначена, лучше, чище, выразительнее. Вы можете не согласиться. Но, пожалуйста, воздержитесь от того, чтобы тратить больше времени на эти касательные. Спасибо

Ответ №1:

Вы должны разделить дескриптор обратного вызова и регистрации. Никому, кроме callback_manager, не нужен call_me метод, так зачем держать его видимым извне? Используйте std::function в качестве обратного вызова, потому что это очень удобно: любой вызываемый объект может быть преобразован в него, и лямбды очень удобны. Возвращает 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_manager и попытается отписаться от удаленного объекта. Это может быть исправлено либо политикой (всегда отменяйте подписку на все перед удалением manager), либо с помощью слабых указателей.

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

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

Смотрите выше.

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

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

Комментарии:

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

2. Нет, это не так. Но написание примера кода требует времени. Подождите немного.

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

4. В моем примере нет производного класса, и это очень удобно. Использовать лямбды : auto handle = manager.subscribe([/* capture needed variables here */]{ /* Write your callback code here */}) . std::list используется именно потому, что его итераторы не становятся недействительными при внесении изменений.

5. Я знаю, что лямбды — это здорово, но в данном случае я откажусь от них из-за чрезмерных накладных расходов (например, миллионы объектов), в противном случае решение — это то, чего я надеялся избежать, автоматизировав регистрацию путем вывода из класса, в основном для сокращения строк кода, каждая из которых имеет ненулевую вероятность ошибок. Кажется, что моя первоначальная идея невозможна. Большое спасибо.

Ответ №2:

В конструкторе вашего авторегистратора, поскольку объект еще не полностью сконструирован, указатель ‘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 должен быть синхронизирован, если его могут использовать несколько потоков. Но моя версия autoregister сама по себе не имеет условия гонки.

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

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

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

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

Комментарии:

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

2. Изначально вы использовали необработанные указатели, поэтому я использовал необработанные указатели. Предполагается, что любой объект ‘обратного вызова’ должен пережить объект автоматической регистрации, который хранит указатель обратного вызова. Если в другом месте вашего кода у вас нет такой гарантии, один из способов сделать это — изменить ‘callback* const callback_’ внутри autoregister на ‘shared_ptr<обратный вызов> обратный вызов_’.

3. я получаю из autoregister, обратного вызова или обоих? Как я могу это использовать?

4. @Martin: Вы производите класс от callback , затем создаете autoregister экземпляр и передаете адрес своего callback производного при его создании. Это зарегистрирует обратный вызов при его создании и отменит его регистрацию при его уничтожении.

5. Спасибо @Jerry Coffin. Это именно то, что я имел в виду.

Ответ №3:

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

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

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

Комментарии:

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

Ответ №4:

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

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

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

 template <class T>
class autoregister {
    T t;
public:
    template <class...Args>
    autoregister(Args amp;amp; ... 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 будет передан через from autoregister to F , когда он будет создан. Также обратите внимание, что это не пытается принудительно применить определенную сигнатуру для функции обратного вызова. Если бы вы, например, модифицировали свой менеджер обратного вызова для выполнения обратного вызова as int r = callback(1); , тогда код компилировался бы только в том случае, если зарегистрированные вами объекты обратного вызова могут быть вызваны с int аргументом и возвращены an int (или что-то, что может быть неявно преобразовано в an int , в любом случае). Компилятор принудительно выполнит обратный вызов, имеющий подпись, совместимую с тем, как он вызывается. Единственным большим недостатком здесь является то, что если вы передаете тип, который может быть вызван, но (например) не может быть вызван с параметрами, которые пытается передать менеджер обратного вызова, сообщения об ошибках, которые вы получаете, могут быть не такими удобочитаемыми, как хотелось бы.

Комментарии:

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

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

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

4. И другой момент, да, кажется, я расширяю вопрос, но на самом деле это не так. Если решение ограничено одним обратным вызовом, оно настолько ограничено, что его можно назвать калекой, извините, действительно не хочу затевать драку, но это похоже на файловую систему, которая допускает существование одного файла, или какой-то другой пример. Смысл хорошего кода в том, чтобы разрешить компоновку, или, по крайней мере, я так думаю, лично мне не нравится такое ограничение. опять же, извините, что не согласен, но я

5. кроме того, ваше решение на самом деле не зависит от использования оператора вызова.