Ретрансляция сигнала с помощью boost.signals2

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

#include <boost/bind.hpp>
#include <boost/signals2.hpp>
#include <iostream>

class inner {
       public:
    template <class T>
    void register_callback(boost::function<void(T *)> cb, T *obj)
    {
        sig_inner_.connect(boost::bind(cb, boost::ref(obj)));
    }

    void trigger()
    {
        std::cout << "inner" << std::endl;
        sig_inner_();
    }

       private:
    boost::signals2::signal<void()> sig_inner_;
};

class mid {
       public:
    mid() { inner_obj.register_callback<mid>(&mid::handle_sig_mid, this); }
    template <class T>
    void register_callback(boost::function<void(T *)> cb, T *obj)
    {
        sig_mid_.connect(boost::bind(cb, boost::ref(obj)));
    }

    void trigger() { sig_mid_(); }
    void inner_trigger() { inner_obj.trigger(); }
    void handle_sig_mid()
    {
        std::cout << "mid" << std::endl;
        trigger();
    }

       private:
    boost::signals2::signal<void()> sig_mid_;
    inner inner_obj;
};

class outer {
       public:
    outer() { mid_obj.register_callback<outer>(&outer::handle_sig_outer, this); }
    void inner_trigger() { mid_obj.inner_trigger(); }
       private:
    mid mid_obj;
    void handle_sig_outer() { std::cout << "outer" << std::endl; }
};

int main()
{
    outer outer_obj;
    outer_obj.inner_trigger();
    return 0;
}

Вместо желаемого результата:

inner
mid
outer

При запуске программы на самом деле происходит следующее:

inner
mid
mid

Вслед за аварией.

Я уже заметил, что адрес this в обработчике отличается от того, что я ожидал бы в обычном методе, но я не знаю, как это обойти.

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

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


person ytm    schedule 14.12.2017    source источник
comment
en.cppreference.com/w/cpp/memory/enable_shared_from_this   -  person Blacktempel    schedule 14.12.2017
comment
Если кому-то интересно, я превратил решение в своего рода наблюдатель и наблюдаемые классы, которые можно подключать с помощью наследования: coliru.stacked-crooked.com/a/1ed3265edfa47ba1 Бьюсь об заклад, это можно было бы сделать намного лучше, но я подумал, что должен поделиться конечным результатом того, что я изначально пытался сделать.   -  person ytm    schedule 15.12.2017


Ответы (2)


Две вещи:

  • Когда вы привязываетесь к boost::ref(obj), вы заставляете выражение привязки содержать ссылку на параметр функции, который выходит за рамки при выходе из register_callback. (См. Копирует ли boost::bind() параметры по ссылке или по значению?)

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

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

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

    Однако, если что-то копируется/перемещается, это ломается. Обычный способ борьбы с этим — использовать scoped_connections.

Позвольте мне показать мои предложения в два этапа:

Упрощение: раннее связывание, Signals2 печатает Erasure за вас

Нет необходимости в шаблоне register_callback, потому что вы печатаете-стираете тип объекта T немедленно, используя bind и пустой слот сигнала.

Итак, вместо этого заставьте его принимать произвольный nullary и выполнять привязку в вызывающем объекте? На самом деле, лучше использовать лямбду у вызывающего абонента.

template <class F> void register_callback(F&& f) {
    sig_inner_.connect(std::forward<F>(f));
}

А потом

mid() { inner_obj.register_callback([this] { handle_sig_mid(); }); }

Время жизни и сигналы2: связи

Вместо того, чтобы использовать тяжеловесный вариант и везде использовать enable_shared_from_this() с динамическим размещением, используйте возможности библиотеки: http://www.boost.org/doc/libs/1_65_1/doc/html/boost/signals2/scoped_connection.html

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

Мое предложение:

template <class F> boost::signals2::scoped_connection register_callback(F&& f) {
    return sig_inner_.connect(std::forward<F>(f));
}

А потом

mid() { _connection = inner_obj.register_callback([this] { handle_sig_mid(); }); }

Сделать _connection участником:

boost::signals2::scoped_connection _connection;

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

Полная демонстрация

Жить на Coliru

#include <boost/bind.hpp>
#include <boost/signals2.hpp>
#include <iostream>

class inner {
  public:
    template <class F> boost::signals2::scoped_connection register_callback(F&& f) {
        return sig_inner_.connect(std::forward<F>(f));
    }

    void trigger() {
        std::cout << "inner" << std::endl;
        sig_inner_();
    }

  private:
    boost::signals2::signal<void()> sig_inner_;
};

class mid {
  public:
    mid() { _connection = inner_obj.register_callback([this] { handle_sig_mid(); }); }

    template <class F> boost::signals2::scoped_connection register_callback(F&& f) {
        return sig_mid_.connect(std::forward<F>(f));
    }

    void trigger() { sig_mid_(); }
    void inner_trigger() { inner_obj.trigger(); }
    void handle_sig_mid() {
        std::cout << "mid" << std::endl;
        trigger();
    }

  private:
    boost::signals2::scoped_connection _connection;
    boost::signals2::signal<void()> sig_mid_;
    inner inner_obj;
};

class outer {
  public:
    outer() { _connection = mid_obj.register_callback([this] { handle_sig_outer(); }); }
    void inner_trigger() { mid_obj.inner_trigger(); }

  private:
    boost::signals2::scoped_connection _connection;
    mid mid_obj;
    void handle_sig_outer() { std::cout << "outer" << std::endl; }
};

int main() {
    outer outer_obj;
    outer_obj.inner_trigger();
    return 0;
}

Отпечатки

inner
mid
outer
person sehe    schedule 14.12.2017
comment
В качестве бонуса: вот версия, которая пишет то же самое без единого объявления шаблона: coliru.stacked-crooked .com/a/a937c9f2ceb3c7e8 (теоретически он может быть немного менее эффективным, но я держу пари, что он на уровне/лучше, чем исходный код, который задерживал привязку до полного завершения) - person sehe; 14.12.2017
comment
Спасибо! Это было чрезвычайно информативно. Вы также значительно увеличили мой список для чтения на данный момент. - person ytm; 14.12.2017
comment
Это правильный способ учиться. На личном опыте могу подтвердить, что усилия окупаются. Ваше здоровье! - person sehe; 14.12.2017

Везде, где у вас есть обработчик для вызова, передайте ему экземпляр std::shared_ptr текущего экземпляра класса. Этого можно добиться путем публичного наследования enable_shared_from_this. Таким образом, ваш экземпляр mid будет поддерживаться до тех пор, пока обработчик не завершит работу.

class mid : public std::enable_shared_from_this<mid>
{
    mid()
    {
        inner_obj.register_callback<mid>(&mid::handle_sig_mid, shared_from_this());
    }

    //...
};
person Blacktempel    schedule 14.12.2017
comment
Shared-from-this не является ответом на все вопросы, и я бы сказал, что это нетипично для Signals2. - person sehe; 14.12.2017
comment
Кроме того, код, который вы показываете, является неопределенным поведением: en.cppreference.com/w/cpp/memory/enable_shared_from_this/ (c++ 17 определяет его, но гарантированно кинет) - person sehe; 14.12.2017