7
\$\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\$
  • \$\begingroup\$ Hi! Welcome to Code Review. Unfortunately, your question is off-topic as of now, as the code to be reviewed must be present in the question. Code behind links is considered non-reviewable. Please add the code you want reviewed in your question. Thanks! \$\endgroup\$ – TheCoffeeCup Nov 22 '15 at 17:44
  • \$\begingroup\$ Sorry. I thought it won't be okay if I add all of the source code. I will correct my question. \$\endgroup\$ – Szőke Szabolcs Nov 22 '15 at 17:46
  • \$\begingroup\$ @szabolcsx sorry, but your question is still OT. Ask it to stackoverflow.com. CodeReview is made to make a correct (not with errors) code more efficient. \$\endgroup\$ – DrProgrammer Nov 22 '15 at 17:54
  • \$\begingroup\$ This code works. \$\endgroup\$ – Szőke Szabolcs Nov 22 '15 at 17:55
  • 1
    \$\begingroup\$ Sorry, it was ambiguous. I meant design errors. \$\endgroup\$ – Szőke Szabolcs Nov 22 '15 at 17:58

Your Answer

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

Browse other questions tagged or ask your own question.