std::function принимает разные сигнатуры функций в С++

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

Самое близкое, что я могу получить, это std::function, bind и placeholders, но вот моя проблема.

void Func()
{
    std::cout << "Notified" << std::endl;
}

void FuncWithParam(const std::string& str)
{
    std::cout << str << std::endl;
}

std::function<void()> fn = std::bind(Func); // this works

std::function<void()> fn = std::bind(FuncWithParam, "Hello there"); // this works also

std::function<void()> fn = std::bind(FuncWithParam, _1); // but this doesn't

Действительно ли возможно хранить любую подпись в одном std::function ? Или мне нужно прибегнуть к более сложному решению.


person Manspider    schedule 01.01.2018    source источник
comment
Не любой, нет. Вы указываете подпись прямо в аргументе шаблона. Конвертируемость делает типы согласованными, но количество параметров функции фиксировано.   -  person StoryTeller - Unslander Monica    schedule 01.01.2018
comment
Такое можно построить, но остается вопрос: когда вы можете поместить туда любой вызываемый объект, как вы узнаете, что вызываете его правильно? Возможно, вы ищете что-то вроде типобезопасная гетерогенная система событий.   -  person chris    schedule 01.01.2018
comment
Я надеялся не использовать Boost, но ваш ответ на самом деле дал название тому, что я пытался сделать. Это должно держать меня в движении   -  person Manspider    schedule 01.01.2018
comment
FWIW, Hana существовала независимо до того, как стала частью Boost. Вполне возможно использовать его без Boost.   -  person chris    schedule 01.01.2018
comment
О, это не только о Boost. Я хотел бы иметь возможность решить эту проблему без использования каких-либо внешних зависимостей. Но я обязательно попробую Hana или даже Boost::Signal, если понадобится.   -  person Manspider    schedule 01.01.2018
comment
Если вы можете жить с уменьшенной безопасностью типов, вы всегда можете просто прибегнуть к std::vector<std::any> const& params.   -  person Christian Hackl    schedule 01.01.2018
comment
Я не уверен, какие части событий С# вы хотите имитировать и почему. Вы говорите об оптимизированном хранилище, в котором многие виды событий хранятся вместе, которое существует, потому что элементы управления пользовательским интерфейсом библиотеки C # предоставляют больше событий болота, чем любой здравомыслящий потребитель мог бы слушать? Если да, то пишете ли вы фреймворк для многих тысяч потребителей и публикуете неиспользуемые события на 99%, если пользователи имеют смысл? Или какая-то другая особенность/причина?   -  person Yakk - Adam Nevraumont    schedule 01.01.2018
comment
@ChristianHackl Можете ли вы разработать? Я не уверен, куда вы хотите пойти, но я не знал о std::any, и это звучит интересно.   -  person Manspider    schedule 01.01.2018
comment
@Yakk Я хочу иметь возможность подписываться и отписываться от событий, которые я могу позже вызвать с нужными мне параметрами. Я реализовал эту ссылку, но она не поддерживает обратные вызовы с разными типы параметров. Я могу использовать общий абстрактный класс для решения проблемы, но это звучит очень неправильно   -  person Manspider    schedule 01.01.2018
comment
@LRP: std::any стал доступен в C++17, см. en.cppreference.com/w /cpp/utility/any и примеры на en.cppreference. com/w/cpp/utility/any/any_cast   -  person Christian Hackl    schedule 01.01.2018
comment
@LRP Это было удивительно расплывчато. И связанный код выглядит как код Java/C#, а не код C++. Почему бы не начать с std::function, и это, кажется, не имеет ничего общего с хранением какой-либо подписи. Я понятия не имею, почему вы хотите хранить какие-либо подписи; вы хотите, чтобы типы вызванных подписей соответствовали типам подписей, зарегистрированных, и вы хотите, чтобы это проверялось во время компиляции, если вы в здравом уме.   -  person Yakk - Adam Nevraumont    schedule 02.01.2018
comment
@Yakk Извините за расплывчатый ответ. Связанный код определенно C++, так как я использую std::function и std::bind. Во всяком случае, вариативные шаблоны - это то, что я искал. Смотрите принятый ответ.   -  person Manspider    schedule 04.01.2018
comment
@ChristianHackl std::any во многих ситуациях меняет правила игры. Я обязательно потрачу некоторое время на изучение c++17. Спасибо за советы.   -  person Manspider    schedule 04.01.2018


Ответы (1)


Вот простой вещатель C++:

using token = std::shared_ptr<void>;

template<class...Ts>
struct broadcaster {
  using listen = std::function<void(Ts...)>;
  using sp_listen = std::shared_ptr<listen>;
  using wp_listen = std::weak_ptr<listen>;

  token attach( listen l ) {
    return attach( std::make_shared<listen>(std::move(l)) );
  }
  token attach( sp_listen sp ) {
    listeners.push_back(sp);
    return sp;
  }
  void operator()(Ts...ts)const {
    listeners.erase(
      std::remove_if( begin(listeners), end(listeners),
        [](auto&& wp){return !(bool)wp.lock();}
      ),
      end(listeners)
    );
    auto tmp = listeners;
    for (auto&& l : tmp) {
      if (auto pf = l.lock()) {
        (*pf)(ts...);
      }
    }
  }
private:
  mutable std::vector<wp_listen> listeners;
};

Чтобы прослушать его, вы .attach передаете ему функцию для вызова. attach возвращает token, и функция вызывается до тех пор, пока этот токен (или его копии) продолжает существовать.

Чтобы вызвать сообщение, вы вызываете () в файле broadcaster.

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

Если вы зарегистрируете прослушиватель во время его текущей трансляции, он не получит текущую трансляцию.

Вы можете добавить std::mutex, чтобы это можно было использовать сразу из нескольких потоков, или синхронизировать извне. Если вы синхронизируетесь внутри, я бы не стал удерживать мьютекс, когда вы запускаете цикл for(auto&& в (), чтобы избежать проблем с повторным входом.

Пример использования:

struct location {
  int x, y;
};
struct button {
  broadcaster< location > mouse_click;
  broadcaster<> mouse_enter;
  broadcaster<> mouse_leave;
};

struct dancer {
  std::vector<token> listen_tokens;
  dancer( button& b ) {
    listen_tokens.push_back( b.mouse_enter.attach([this]{ dance(); } ) );
    listen_tokens.push_back( b.mouse_leave.attach([this]{ end_dance(); } ) );
    listen_tokens.push_back( b.mouse_click.attach(
      [this](location l){
        pose(l.x, l.y);
      }
    ) );
  }
  void dance() const {
    std::cout << "start dancing\n";
  }
  void pose( int x, int y ) const {
    std::cout << "struck a pose at " << x << ", " << y << "\n";
  }
  void end_dance() const {
    std::cout << "end dancing\n";
  }
};

Обратите внимание, что virtual методы не использовались. Единственным полиморфизмом было стирание типа на основе std::function.

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

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

Это другой подход, чем вы использовали бы в C#, потому что он опирается на RAII и потому что он не является объектно-ориентированным по своей сути, но остается полиморфным.

person Yakk - Adam Nevraumont    schedule 02.01.2018
comment
Похоже на то, что я искал. У меня были трудности с тем, чтобы заставить его работать с вариативными шаблонами. Мьютекс тоже приятный штрих. Я буду держать вас в курсе, если у меня возникнут трудности с его реализацией. Спасибо. - person Manspider; 04.01.2018