从C++向量中移除std::function<()>

3

我正在构建一个发布-订阅类(称为 SystermInterface),它负责接收其实例的更新,并将其发布给订阅者。

添加订阅者回调函数很容易且没有问题,但是删除它会产生错误,因为在C++中std::function<()>不可比较。

std::vector<std::function<void()> subs;
void subscribe(std::function<void()> f)
{
    subs.push_back(f);
}
void unsubscribe(std::function<void()> f)
{
    std::remove(subs.begin(), subs.end(), f);  // Error
}

我已经得出了五个解决此错误的方案:
  1. 使用weak_ptr注册函数,订阅者必须保持返回的shared_ptr的存活状态。
    示例解决方案在此链接
  2. 不要在向量中注册,通过一个自定义键将回调函数映射到回调函数特定的唯一值上。
    示例解决方案在此链接
  3. 使用函数指针向��。例子
  4. 使回调函数可比较,利用地址。参考
  5. 使用接口类(父类)来调用虚函数。
    在我的设计中,所有意图类都继承了一个名为ServiceCore的父类,因此不需要注册回调函数,只需要在向量中注册ServiceCore引用即可。

考虑到每个实例(ID)都有一个字段属性的SystemInterface类(由ServiceCore管理,并通过构造ServiceCore子实例提供给SystemInterface)。

在我的看法中,第一种解决方案很清晰且可行,但需要订阅者处理,这是我不喜欢的事情。

第二个解决方案会使我的实现更加复杂,其中我的实现如下:

using namespace std;
enum INFO_SUB_IMPORTANCE : uint8_t
{
    INFO_SUB_PRIMARY,       // Only gets the important updates.
    INFO_SUB_COMPLEMENTARY, // Gets more.
    INFO_SUB_ALL            // Gets all updates
};

using CBF = function<void(string,string)>;
using INFO_SUBTREE = map<INFO_SUB_IMPORTANCE, vector<CBF>>;

using REQINF_SUBS   = map<string, INFO_SUBTREE>; // It's keyed by an iterator, explaining it goes out of the question scope.
using INFSRC_SUBS   = map<string, INFO_SUBTREE>;
using WILD_SUBS     = INFO_SUBTREE;

REQINF_SUBS infoSubrs;
INFSRC_SUBS sourceSubrs;
WILD_SUBS wildSubrs;

void subscribeInfo(string info, INFO_SUB_IMPORTANCE imp, CBF f) {
    infoSubrs[info][imp].push_back(f);
}
void subscribeSource(string source, INFO_SUB_IMPORTANCE imp, CBF f) { 
    sourceSubrs[source][imp].push_back(f);
}
void subscribeWild(INFO_SUB_IMPORTANCE imp, CBF f) {
    wildSubrs[imp].push_back(f);
}

第二种解决方案需要 INFO_SUBTREE 成为一个扩展映射,但可以使用 ID 作为键:
using KEY_T = uint32_t; // or string...
using INFO_SUBTREE = map<INFO_SUB_IMPORTANCE, map<KEY_T,CBF>>;

对于第三种解决方案,我不知道使用函数指针所限制的内容以及第四个解决方案的后果。

第五种解决方案将消除处理CBF的目的,但在订阅者端会更加复杂,需要一个订阅者重写虚函数并在一个地方接收所有更新,进而需要根据消息ID进行过滤,并使用多个if/else块将有效负载定向到预期的例程中,这将随着订阅增加而增加。

我正在寻求关于最佳可用选项的建议。

1个回答

4

关于您提出的解决方案:

  1. 那会起作用。可以让调用者更容易:让subscribe()创建shared_ptr和相应的weak_ptr对象,并让它返回shared_ptr
  2. 然后调用者不能丢失密钥。在某种程度上,这与上面类似。
  3. 当然,这样就不太通用了,然后你就不能再有(相当于)捕获了。
  4. 您不能:没有办法获取存储在std::function内部的函数的地址。您可以在subscribe()中执行&f,但这只会给您本地变量f的地址,该变量将在您返回时超出范围。
  5. 那行得通,并且在某种程度上类似于1和2,尽管现在“密钥”由调用者提供。
选项1、2和5相似,因为在subs中存储了一些引用实际std::function的其他数据:可以是std::shared_ptr、键或基类指针。我将在此介绍选项6,它在精神上有点类似,但避免了存储任何额外的数据:
6. 直接存储std::function<void()>,并返回存储它的向量中的索引。当删除一个项目时,不要使用std::remove(),而只需将其设置为std::nullptr。下次调用subscribe()时,它会检查向量中是否有空元素并重新使用它。
std::vector<std::function<void()> subs;

std::size_t subscribe(std::function<void()> f) {
    if (auto it = std::find(subs.begin(), subs.end(), std::nullptr); it != subs.end()) {
        *it = f;
        return std::distance(subs.begin(), it);
    } else {
        subs.push_back(f);
        return subs.size() - 1;
    }
}

void unsubscribe(std::size_t index) {
    subs[index] = std::nullptr;
}

实际调用存储在subs中的函数的代码现在当然首先要检查std::nullptr。上面的方法可行是因为std::nullptr被视为“空”函数,并且有一个operator==()重载可以将std::functionstd::nullptr进行比较,从而使std::find()起作用。
以上第6种方法的缺点是std::size_t是一种相当通用的类型。为了使其更安全,您可以将其包装在class SubscriptionHandle或类似名称的类中。
至于最佳解决方案:选项1相当繁重。选项2和5非常合理,但我认为选项6最有效。

感谢您的回答@g.-sliepen。关于您的答案,我认为这也是一种轻量级方法,尊重订阅者应该保留索引的方法。 但是我认为它会解决我的问题,因为在这里SystemInterface实例已经在映射中注册了订阅,因此我将拥有索引作为值,而不是cbf。明天将尝试并回复任何评论。 - Hamza Hajeir
1
对于第四个问题:您能看一下提供的链接吗?它展示了如何使用“template target”获取回调函数的地址。我认为这值得一看。 - Hamza Hajeir
1
有两件事情:每个 lambda 都有一个独特的类型,因此如果 lambda 存储在其中,您不能执行类似 f.target<void()>() 的操作:它的类型将不匹配 void() 的类型。之所以这样做的原因之一是 lambda 可能具有与其关联的其他数据(捕获),因此它与简单函数指针不同。 - G. Sliepen
1
我明白了,谢谢。我将实现提供的解决方案。然而,在准备广播更新之前,我将删除任何cbf重复项(这样回调函数将只被调用一次); 其背后的原因是订阅者可以使用具有“PRIMARY”重要性的整个源(甚至是通配符)进行订阅,并同时使用具有“ALL”重要性的信息分配相同的函数,以确保他每次更新仅接收到一个呼叫,那么什么可以限制我利用回调函数地址 (解决方案 4) 来删除重复项? - Hamza Hajeir
1
考虑到您想要注册一个需要传递一些在编译时不知道的数据的函数,并且您不想或不能使用全局变量来存储这些数据。那么您需要使用带有捕获的lambda表达式或函数对象指针(解决方案5),而解决方案4将无法工作。我只是不会进行任何重复项删除,这是订阅者应该能够自行完成的事情。 - G. Sliepen
显示剩余6条评论

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