3

I'm building a publish-subscribe class (called SystermInterface), which is responsible to receive updates from its instances, and publish them to subscribers.

Adding a subscriber callback function is trivial and has no issues, but removing it yields an error, because std::function<()> is not comparable in C++.

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
}

I've came down to five solutions to this error:

  1. Registering the function using a weak_ptr, where the subscriber must keep the returned shared_ptr alive.
    Solution example at this link.
  2. Instead of registering at a vector, map the callback function by a custom key, unique per callback function.
    Solution example at this link
  3. Using vector of function pointers. Example
  4. Make the callback function comparable by utilizing the address.
  5. Use an interface class (parent class) to call a virtual function.
    In my design, all intended classes inherits a parent class called ServiceCore, So instead of registering a callback function, just register ServiceCore reference in the vector.

Given that the SystemInterface class has a field attribute per instance (ID) (Which is managed by ServiceCore, and supplied to SystemInterface by constructing a ServiceCore child instance).

To my perspective, the first solution is neat and would work, but it requires handling at subscribers, which is something I don't really prefer.

The second solution would make my implementation more complex, where my implementation looks as:

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);
}

The second solution would require INFO_SUBTREE to be an extended map, but can be keyed by an ID:

using KEY_T = uint32_t; // or string...
using INFO_SUBTREE = map<INFO_SUB_IMPORTANCE, map<KEY_T,CBF>>;

For the third solution, I'm not aware of the limitations given by using function pointers, and the consequences of the fourth solution.

The Fifth solution would eliminate the purpose of dealing with CBFs, but it'll be more complex at subscriber-side, where a subscriber is required to override the virtual function and so receives all updates at one place, in which further requires filteration of the message id and so direct the payload to the intended routines using multiple if/else blocks, which will increase by increasing subscriptions.

What I'm looking for is an advice for the best available option.

Hamza Hajeir
  • 119
  • 1
  • 8

1 Answers1

4

Regarding your proposed solutions:

  1. That would work. It can be made easy for the caller: have subscribe() create the shared_ptr and corresponding weak_ptr objects, and let it return the shared_ptr.
  2. Then the caller must not lose the key. In a way this is similar to the above.
  3. This of course is less generic, and then you can no longer have (the equivalent of) captures.
  4. You can't: there is no way to get the address of the function stored inside a std::function. You can do &f inside subscribe() but that will only give you the address of the local variable f, which will go out of scope as soon as you return.
  5. That works, and is in a way similar to 1 and 2, although now the "key" is provided by the caller.

Options 1, 2 and 5 are similar in that there is some other data stored in subs that refers to the actual std::function: either a std::shared_ptr, a key or a pointer to a base class. I'll present option 6 here, which is kind of similar in spirit but avoids storing any extra data:

  1. Store a std::function<void()> directly, and return the index in the vector where it was stored. When removing an item, don't std::remove() it, but just set it to std::nullptr. Next time subscribe() is called, it checks if there is an empty element in the vector and reuses it:
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;
}

The code that actually calls the functions stored in subs must now of course first check against std::nullptrs. The above works because std::nullptr is treated as the "empty" function, and there is an operator==() overload that can check a std::function against std::nullptr, thus making std::find() work.

One drawback of option 6 as shown above is that a std::size_t is a rather generic type. To make it safer, you might wrap it in a class SubscriptionHandle or something like that.

As for the best solution: option 1 is quite heavy-weight. Options 2 and 5 are very reasonable, but 6 is, I think, the most efficient.

G. Sliepen
  • 7,637
  • 1
  • 15
  • 31
  • Thank you @g.-sliepen for your answer. Regarding your answer, I think it's a lightweight method too, with respect the subscriber should keep the index around. BUT I think it'll be solving my issue, as the `SystemInterface` instance here already registers the subscriptions inside a map, so instead of having `cbf` as value, I'd be having the `index` as the value. Will try it tomorrow and get back with any comments. – Hamza Hajeir Oct 16 '22 at 19:10
  • For number 4: Can you look at the link provided? It shows how to get the address of the callback function using `template target`. I think it deserves a look. – Hamza Hajeir Oct 16 '22 at 19:13
  • `target()` only works if you know the exact type of the stored function. If you know it points to a regular function, that might be doable, but if you store a lambda in the `std::function` it is impossible. – G. Sliepen Oct 16 '22 at 20:00
  • But isn't registering the function requires an exact match of function type? or there's something isn't so clear to me special in lambdas? – Hamza Hajeir Oct 17 '22 at 08:10
  • 1
    Two things: every lambda has a unique type, so you can't do something like `f.target()` if a lambda is stored in it: its type will not match the type of `void()`. One reason why it is done like that is that a lambda can have additional data associated with it (the captures), so it's not the same as a simple function pointer. – G. Sliepen Oct 17 '22 at 08:43
  • 1
    I see why, thank you. I'm going to implement the solution provided. However, preparing to broadcast an update, I'd be removing any `cbf` duplicates (so a callback function would be called only once); A reason behind this is that a subscriber can be subscribing a whole source (or even wild) with a `PRIMARY` importance, while also subscribing an info with `ALL` importance assigning the same function, so to make sure he receives exactly one call per update, What can limit me utilizing the callback function address _(solution 4)_ for duplicates removal? – Hamza Hajeir Oct 17 '22 at 11:41
  • 1
    Consider that you want to register a function that needs to be passed some data that is not known at compile-time, and you don't want to or cannot use global variables to store that data. Then you either need a lambda with capture or a pointer to a function object (solution 5), and solution 4 wouldn't work. I would just not do any deduplication, this is something the subscriber should be able to do on their own. – G. Sliepen Oct 17 '22 at 19:30
  • I see, I could utilize some advantages of the tools, wherein (I'm also the subscriber), and subscriptions doesn't rely on unique features of lambdas (captures) although I'm passing the class member function through a lambda because of a compiler error otherwise (bug at compiler). Considering no surprises from capturing, would utilizing addresses help me achieve unique-ness safely? – Hamza Hajeir Oct 19 '22 at 15:20
  • No. If you are using `std::function` to store the callback, forget about using its address. Only if you store raw function pointers or pointers-to-base in `subs` is it safe to use the address for uniqueness testing. – G. Sliepen Oct 19 '22 at 19:34
  • Tried to get some of that working, but no success, ignoring the terrible looking of the code, the susbcribing system should know the class type of the callback function (as it's a member function, not an ordinary function), which goes out of the intended purpose. What could solve the issue? – Hamza Hajeir Oct 20 '22 at 12:05
  • I'm thinking of an opitional tag number can be generated using an API, and then supplied by the subscriber, so instead of assigning to a `vector`, I'd be assigning to a `vector>`. Primarily I think it's a suitable solution, any comments? – Hamza Hajeir Oct 20 '22 at 12:11