在一个向量映射的 map 中查找并删除一个 std::function 对象

4

我目前正在尝试为我的游戏引擎实现一个信使系统。它使用以下形式的函数回调:

typedef std::function<void(const Message &)> Callback;

它维护着一条消息列表:

mutable std::vector<std::unique_ptr<Message>> messageList;

回调函数的签名字典如下:

mutable std::map<std::string, std::vector<Callback>> callbackDictionary;

这个方法用于调用与特定消息类型“绑定”的所有回调函数。当调用回调函数时,传递相应的消息。到目前为止还不错。

为了更好地理解,这里是订阅方法,它允许用户添加一个函数方法,该方法针对所订阅的每个消息都会被调用。

void Messenger::Subscribe(Message::Type type, Callback callbackFunction) 
const
{
    callbackDictionary[type].push_back(callbackFunction);
}

以下是可点击组件类的使用示例:

messageBus.Subscribe("Message/Click", [this](const Message & message) {this->OnClick(static_cast<const ClickMessage &>(message)); });

现在我的问题是:
我想要实现一个退订方法,可以在字典的向量中找到函数/方法并将其删除。请注意,该函数可能已经订阅了多种消息类型。我考虑的方法类似于这样:
void Messenger::UnSubscribe(Callback callbackFunction)
{
    for (auto & pair : callbackDictionary)
    {
        auto & functionList = pair.second;
        for (auto function : functionList)
        {
            if (function == callbackFunction)
                functionList.erase(std::remove(functionList.begin(), functionList.end(), function), functionList.end());
            // I am aware of issues that may appear from looping deleting while looping
        }
    }
}

然而,函数对象的比较运算符(==)似乎未定义。我想不出一个简单的方法来解决这个问题。所以任何想法都非常欢迎。我只是试图避免某种id系统,因为从经验来看,管理它们可能会很麻烦。特别是当所有种类的函数和成员可以随处添加到程序中时。

类似于 https://dev59.com/bGMl5IYBdhLWcg3w5KXd#18039824 的解决方案可行吗? - Captain Giraffe
如果注册回调对象的人应该保留其副本(以便注销它),那么为什么不存储指向原始对象的指针呢?指针可以很容易地进行比较。 - Öö Tiib
@MassimilianoJanes 首先,感谢您的回答。(我知道这并没有真正有帮助)但是从概念上讲,如果函数具有相同的行为,则它们应该是相等的。因此,正如您所指出的那样,在使用[&] lambda时会出现问题。更准确地说:具有相同代码并修改相同数据的函数/lambda(无论是相同引用还是此对象)是相等的。顺便说一句:我已经尝试过指向std :: function对象和lambda的指针,但它们给我奇怪的错误。 - Adrian Albert Koch
2
@AdrianKoch 在C++中,“可调用对象”没有自然的相等操作。而std::function是一个“可调用对象”(可以复制的可调用对象),而不是你认为的模型。其次,你的系统似乎需要将单个消息传递给回调函数(或某种动态或不安全的转换),这似乎不太理想。第三,如果你注册了一个回调函数,那么当回调函数所引用的对象生命周期结束时会发生什么? - Yakk - Adam Nevraumont
@Yakk 第二点是正确的,但由于系统的范围有限,这并不是一个大问题(然而,如果您有更好的想法...我很乐意听取)。至于第三点:任何在其构造函数中订阅消息的对象都应该在其析构函数中取消订阅该消息(这就是整个问题的根源^^)。 - Adrian Albert Koch
显示剩余6条评论
2个回答

2

在std::function中没有等号运算符。这个决策的理由可以在旧版的tr1::functionN1667中找到:

operator==对于C++语言中的tr1::function来说是无法实现的,因为我们没有一种可靠的方法来检测给定类型T是否是可比较相等的,除非有用户的帮助。

另外请注意,您正在通过值传递std::function<void(const Message &)>。这意味着您也不能仅比较它们的地址(即使可以,这也不是一个好的解决方案,因为std::function很容易被复制)。

解决方案1:

使用一些用户提供的和std::function作为value并将它们存储在map而不是vector中。

std::map<std::string, std::map<std::string, Callback>> callbackDictionary;
. . .
void Messenger::Subscribe(Message::Type type, const std::string& cbID, Callback cb);

void Messenger::UnSubscribe(const std::string& cbID);

解决方案2:
使用`weak_ptr`来跟踪回调函数。
std::map<std::string, std::vector<std::weak_ptr<Callback>> callbackDictionary;
. . .
void Messenger::Subscribe(Message::Type type, std::shared_ptr<Callback> cb);

无需进行“取消订阅”!只要 weak_ptr 变成 nullptr,回调就会自动取消订阅。前提是通过 shared_ptr 使监听器保持回调的存活状态。

我非常喜欢第二个选项。事实上,我非常喜欢它,以至于我在其他地方也使用了这种共享和弱指针的方法。其中一个地方描述在:https://stackoverflow.com/questions/47317531/using-shared-ptr-for-unique-ownership-kind-of-is-this-good-practice 。现在的问题是,我不太确定这是否仍然是良好的编程实践。因此,如果您能快速查看一下,我将不胜感激。 - Adrian Albert Koch

1

std::function 存储可复制的可调用对象。它不要求其内容是 == 可比较的,而 lambda 表达式不是 == 可比较的。

您可以提取存储的可调用对象的 typeid,如果它们不匹配,则假设为 false,添加类型抹除特性,让您可以在各种类型上存储 == 并在类型相等时进行分派,但这样您就不支持 lambda 表达式,因为 lambda 表达式不支持 ==

如果您说“没有 lambda 表达式”,则可以这样做,或者如果您说“没有带状态的 lambda 表达式”。我稍后会介绍这种可能性,但首先建议尝试以下方法:

using std::shared_ptr<void> token;

template<class...Message>
struct broadcaster {
  using listener = std::function<void(Message...)>;
  using splistener = std::shared_ptr<listener>;
  using wplistener = std::weak_ptr<listener>;

  token listen( listener f ) {
    auto sf = std::make_shared<listener>(std::move(f));
    listeners.push_back(sf); // as weak ptr
    return sf; // as token
  }
  std::size_t operator()( Message... message ) const {
    // remove stale targets:
    targets.erase( std::remove_if( begin(targets), end(targets),
      [](auto&& ptr) { return !ptr.lock(); }
    ), end(targets) );
    auto tmp = targets; // copy, for reentrancy
    for (auto wf : tmp) {
      if(auto sf = wf.lock()) {
        sf( message... );
      }
    }
  }
private:
  mutable std::vector<wplistener> targets;
};

在客户端中,跟踪您正在侦听的令牌。当客户端对象消失时,它将自动从其侦听的每个广播器中取消注册。只需使用std :: vector<token>并将其标记放入其中即可。
如果您有更复杂的逻辑,其中侦听不应绑定到侦听器的生命周期,则必须单独存储所述令牌。
这假定广播大致与注册/注销同样频繁或更频繁。如果广播非常罕见且注册/注销非常常见(例如百万次以上),则侦听器的弱指针可能会积累。您可以在listen中添加一个测试以定期清除陈旧的侦听器。
现在,我们可以进行“无绑定状态的lambda表达式”。然后我们可以单独绑定状态,在那里对==操作进行类型擦除,就完成了。
state( some_state... )
([](some_state&&...){
  return [&](some_args...){
  /* code */
});

像上面这样的结构将让你返回一个函数对象,它的行为很像lambda,但是在其上有一个明智的==操作。
template<class F, class...Args>
struct bound_state {
  std::tuple<Args...> state;
  F f;
  friend bool operator==( bound_state const& lhs, bound_state const& rhs ) {
    return lhs.state==rhs.state;
  }
  template<class...Ts>
  decltype(auto) operator()(Ts&&...ts){
    return std::apply(f, state)( std::forward<Ts>(ts)... );
  }
};
template<class...Args>
auto state( Args&&... args ) {
  auto tup = std::make_tuple( std::forward<Args>(args)... );
  return [tup=std::move(tup)](auto&& f) {
    return bound_state<std::decay_t<decltype(f)>, std::decay_t<Args>...>{
      tup, decltype(f)(f)
    };
  };
}

或者类似的东西。

接下来,我们创建了一个派生类型的 std::function。当它从一个类型构造时,它将为它存储一个类型擦除的 ==(可以是全局或本地位置)(来自一对 std::function)。

它重写了 ==,首先检查 typeid 是否相同,如果相同,则调用两个元素上的类型擦除的 ==

template<class Sig>
struct func_with_equal : std::function<Sig> {
  using Base = std::function<Sig>;
  using Base::operator();
  using equality = bool( func_with_equal const&, func_with_equal const& );      
  equality* pequal = nullptr;

  template<class F>
  static equality* erase_equality() {
    return [](func_with_equal const& lhs, func_with_equal const&rhs)->bool {
      assert( lhs.target_type() == rhs.target_type() );
      assert( lhs.target_type() == typeid(F) );
      return *lhs.target<F>() == *rhs.target<F>();
    };
  }
  // on construction, store `erase_equality<F>()` into `pequal`.

  friend bool operator==( func_with_equal const& lhs, func_with_equal const& rhs ) {
    if (!lhs && !rhs) return true;
    if (!lhs || !rhs) return false;
    if (lhs.target_type() != rhs.target_type()) return false;
    return lhs.pequal( lhs, rhs );
  }
};

这只完成了一半,但我希望你能理解。它很复杂且错综复杂,在每个注册回调点都需要额外的工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接