9
\$\begingroup\$

I programmed in C# and I used the Qt framework. Both of them have their own signal slot mechanism which are really powerful. I looked at several implementations in C++ and based on what I learnt from those sources, I started to implement my own version to better understand how these could work under the hood.

My implementation currently has no support for accessing the return values of the slots, and provides no thread safety. I implemented an auto disconnecting mechanism to manage the slots life cycle and disconnect them if those go out of scope. I posted the source code on GitHub. I've used std::function and variadic templates.

You declare a signal this way:

signal<int> test_signal; // test_signal has an int parameter

You can connect slots with the connect function:

test_signal.connect(&slot_function); // void slot_function(int)
// Connecting to a member function, f is the instance of the object
test_signal.connect(&foo::slot_function, f); // void foo::slot_function(int)

You can connect multiple slots to a signal which are stored in a vector. Every connect method returns a connection class which has a disconnect function which disconnects the slot from the signal. The foo object must inherit from auto_disconnect class for life cycle management. This class stores a vector of connections and when it's destructed it disconnect all of the slots. If you want to emit a signal you can use the overloaded operator().

test_signal(1);

I never done anything like this before and I wanted to spot any design errors, make it more efficient, and have a better understanding about this.

Here is the full source code with example usage:

//signal.h
#ifndef SIGNAL_H__
#define SIGNAL_H__

#include <functional>
#include <vector>

namespace szabi
{
  // When a slot is overloaded an ugly cast should be used
  // static_cast<void(T::*)(<parameters>)>(&T::<slot>);
  // This helper simplifies this a bit
  // overload<parameters>::of(&T::slot);
  template <typename... Args>
  struct overload
  {
    template <typename F, typename R>
    static constexpr auto of(R(F::*ptr)(Args...)) -> decltype(ptr)
    {
      return ptr;
    }
  };

  // This class manages a connection between a signal and slot
  class connection
  {
  public:
    connection(std::function<void()>&& disconnector) : disconnector(std::move(disconnector)) {}

    ~connection() {}

    void disconnect() const
    {
      this->disconnector();
    }

  private:
    // This will contain a lambda function which will remove the slot from the signal's vector
    std::function<void()> disconnector;
  };

  // This class is used to disconnect all slots when the object is destroyed
  class auto_disconnect
  {
    // A signal has access to add_connection function
    template<typename... Args> friend class signal;
  public:
    virtual ~auto_disconnect()
    {
      for(const auto& conn : this->connections)
      {
        conn.disconnect();
      }
    }

  protected:
    void add_connection(const connection& conn)
    {
      this->connections.push_back(conn);
    }

  private:
    std::vector<connection> connections;
  };

  template <typename... Args>
  class signal
  {
  public:
    signal() {}
    virtual ~signal() {}

    using slot_type = std::function<void(Args...)>;
    using container_type = std::vector<slot_type>;
    using iterator_type = typename container_type::iterator;


    template <typename S>
    connection connect(S&& slot)
    {
      this->slots.push_back(std::forward<S>(slot));

      connection conn =
      {
        [&]()
        {
          iterator_type it = std::prev(this->slots.end());
          this->slots.erase(it);
        }
      };

      return conn;
    }

    template <typename S, typename T>
    connection connect(S&& slot, T* instance)
    {
      static_assert(std::is_base_of<auto_disconnect, T>::value, "T must inherit auto_disconnect");
      this->slots.emplace_back([&](Args... args)
      {
        // Using a lambda function to call a slot which is a member function
        (static_cast<T*>(instance)->*slot)(std::forward<Args>(args)...);
      });

      connection conn =
      {
        [&]()
        {
          iterator_type it = std::prev(this->slots.end());
          this->slots.erase(it);
        }
      };

      instance->add_connection(conn);

      return conn;
    }

    template <typename S, typename T>
    connection connect(S&& slot, T& instance)
    {
      static_assert(std::is_base_of<auto_disconnect, T>::value, "T must inherit auto_disconnect");
      return this->connect(slot, std::addressof(instance));
    }

    void operator()(Args const&... args)
    {
      for(auto const& slot : this->slots)
      {
        if(slot)
        {
          slot(args...);
        }
      }
    }
  private:
    container_type slots;
  };
}

#endif /* SIGNAL_H__ */

// main.cpp
#include "signal.h"

#include <iostream>
#include <cstdlib>

void func(int x)
{
  std::cout << "void func(" << x << ")" << std::endl;
}

class foo : public szabi::auto_disconnect
{
public:
  foo() {}
  ~foo() {}

  void func(int x)
  {
    std::cout << "void foo::func(" << x << ")" << std::endl;
  }

  void func(int x, int y)
  {
    std::cout << "void foo::func(" << x << ", " << y << ")" << std::endl;
  }

  void func0()
  {
    std::cout << "void foo::func0()" << std::endl;
  }

  void func1(int x)
  {
    std::cout << "void foo::func1(" << x << ")" << std::endl;
  }

  void func2(int x, int y)
  {
    std::cout << "void foo::func2(" << x << ", " << y << ")" << std::endl;
  }
};

class bad : public szabi::auto_disconnect
{
public:
  bad()
  {
    std::cout << "bad constructed" << std::endl;
  }
  ~bad()
  {
    std::cout << "bad destructed" << std::endl;
  }

  void func(int)
  {
    std::cout << std::endl << "void bad::func()" << std::endl << std::endl;
  }
};

int main()
{
  szabi::signal<> signal0;
  szabi::signal<int> signal1;
  szabi::signal<int, int> signal2;
  foo f;
  signal0.connect(&foo::func0, f);
  signal1.connect([](int x)
  {
    std::cout << "Lambda func, x = " << x << std::endl;
  });

  signal1.connect(&func);

  // overloaded slots
  signal1.connect(static_cast<void(foo::*)(int)>(&foo::func), f);
  signal2.connect(static_cast<void(foo::*)(int, int)>(&foo::func), f);
  // using overload<...>::of(&foo::bar) helper
  signal1.connect(szabi::overload<int>::of(&foo::func), f);
  signal2.connect(szabi::overload<int, int>::of(&foo::func), f);

  signal1.connect(&foo::func1, f);
  signal2.connect(&foo::func2, f);

  {
    bad b;
    szabi::connection conn = signal1.connect(&bad::func, b);
  }

  signal0();
  signal1(1);
  signal2(1, 2);

  return EXIT_SUCCESS;
}
\$\endgroup\$
4
\$\begingroup\$

A bit late to the party, but I spotted a few mistakes in this otherwise interesting exercise and I thought I would highlight them.

Trying to run the given code as-is results in crashes. Here's why.

First and foremost, std::vector iterators get invalidated whenever an element is erased from the vectors, unless the erasure happens at the vector's very end. Solution: replacing std::vector with a container that guarantees iterator validity over erasures is a must. To this end, std::list might be used.

Then, you must take into account the fact that the connection's slot destroyer lambda body executes when the lambda itself is invoked, not when it's constructed, therefore the iterator of the slot to be destroyed can't be taken from the end of the container within the lambda's body, because by the time the lambda is invoked, other slots might have been added or removed from the container. Solution: take the slot's iterator before constructing the connection object and capture it in the lambda.

Fixing that, makes the code work as expected.

I would also implement the templated method connect(S&& slot, T* instance) in terms of the non-templated one, so avoid duplicating code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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