4
\$\begingroup\$

This class is mostly an educational exercise for me using some C++11 constructs. I wanted to create something similar to an "invocation list" in C#, i.e., a list of zero or more function objects which should be called when the invocation list as a whole is invoked. I'm certain there's room for improvement here and I'm hoping I didn't mess anything up TOO badly.

Some key implementation notes:

  • Variadic templates are used to specify the parameters passed to the target(s) of the invocation list.
  • The subscribe() function returns an "opaque" subscription object which can be used to unsubscribe() from the invocation list. This is done because std::function is not equality-comparable (so passing in a std::function to unsubscribe is problematic).
  • The subscription object that gets returned can be copied, and is designed so that instances of it are still well-behaved in a "detached" state when the original invocation_list instance has been destroyed; this is accomplished by sharing a pointer to the underlying target list using std::shared_ptr to do the reference counting on that pointer. The pointer is nulled when the invocation list is destroyed, detaching any associated lingering subscription instances.
  • No attempt has been or will be made to make this thread-safe.
#pragma once

#include <functional>
#include <list>
#include <memory>

template <class... Args>
class invocation_list
{
    private:

        typedef std::function<void(Args...)> func_type;
        typedef std::list<func_type> list_type;

    public:

        class subscription
        {
            public:

                void unsubscribe()
                {
                    if (_targets && *_targets)
                    {
                        (*_targets)->erase(_it);
                        _targets.reset();
                    }
                }

            private:

                subscription(
                        std::shared_ptr<list_type *> targets,
                        typename list_type::iterator it)
                    : _targets(targets), _it(it) { }

                std::shared_ptr<list_type *> _targets;
                typename list_type::iterator _it;

            friend class invocation_list;
        };

    public:

        invocation_list() : _token(new list_type *)
        {
            *_token = &_targets;
        }

        ~invocation_list()
        {
            *_token = nullptr;
        }

        invocation_list(const invocation_list&) = delete;
        invocation_list& operator=(const invocation_list&) = delete;

        subscription subscribe(func_type func)
        {
            _targets.push_back(func);
            return subscription(_token, --_targets.end());
        }

        void operator()(Args... args) const
        {
            for (auto it = _targets.begin(); it != _targets.end(); ++it)
                (*it)(args...);
        }

    private:

        list_type _targets;
        std::shared_ptr<list_type *> _token;
};

This would be used like:

invocation_list<int> foo;
auto a = foo.subscribe([](int x) { std::cout << "hello: " << x << std::endl; });
auto b = foo.subscribe([](int x) { std::cout << "world: " << x << std::endl; });
foo(42); // prints "hello: 42" "world: 42"
a.unsubscribe();
foo(54); // prints only "world: 54"

Future changes I've considered and am interested in feedback on:

  • Overload operators for subscribing and unsubscribing instead of an explicit subscribe() function?
  • Extending the subscription type to optionally operate in an RAII mode where the target is automatically unsubscribed when the subscription object goes out of scope?
\$\endgroup\$
1
  • \$\begingroup\$ One improvement I've already made is to replace the explicit iterator traversal in operator() with a range-based for loop: for (auto target : _targets) target(args...); \$\endgroup\$
    – TypeIA
    Jun 29, 2016 at 19:49

1 Answer 1

1
\$\begingroup\$

I mentioned in the comments the following improvement to operator() using range-based loops:

for (auto target : _targets) target(args...);

I've also identified another potential issue: with the code as originally posted, the user must not destroy the invocation_list instance during invocation (i.e., inside a target function). This situation could easily arise if, for example, the invocation list is used to implement a "close" event, and inside the "close" event "handler" the user wants to delete the object that "triggered" the event.

To allow this, the subscribe() function must first make a copy of the std::list of targets before iterating over it. This allows the invocation list itself to be safely destroyed (per the "suicide" rules):

void operator()(Args... args) const
{
    list_type targets(_targets);
    for (auto target : targets) target(args...);
}

This reduces performance since the list of targets has to be copied for every invocation, but allows more robust usage. If anyone knows any alternatives that don't require copying the list but still allow this usage pattern, I'm all ears! Thanks!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.