SoulfreezerXP
SoulfreezerXP

Reputation: 596

Creating a callback with std::function as class-member

I have designed a simple callback-keyListener-"Interface" with the help of a pure virtual function. Also I used a shared_ptr, to express the ownership and to be sure, that the listener is always available in the handler. That works like a charme, but now I want to implement the same functionality with the help of std::function, because with std::function I am able to use lambdas/functors and I do not need to derive from some "interface"-classes.

I tried to implement the std::function-variant in the second example and it seems to work, but I have two questions related to example 2:

  1. Why does this example still work, although the listener is out of scope? (It seems, that we are working with a copy of the listener instead of the origin listener?)

  2. How can I modify the second example, to achieve the same functionality like in the first example (working on the origin listener)? (member-ptr to std::function seems not to work! How can we handle here the case, when the listener is going out of scope before the handler? )

Example 1: With a virtual function

#include <memory>

struct KeyListenerInterface
{
    virtual ~KeyListenerInterface(){}
    virtual void keyPressed(int k) = 0;
};

struct KeyListenerA : public KeyListenerInterface
{
    void virtual keyPressed(int k) override {}
};

struct KeyHandler
{
    std::shared_ptr<KeyListenerInterface> m_sptrkeyListener;

    void registerKeyListener(std::shared_ptr<KeyListenerInterface> sptrkeyListener)
    {
        m_sptrkeyListener = sptrkeyListener;
    }

    void pressKey() { m_sptrkeyListener->keyPressed(42); }
};

int main()
{
    KeyHandler oKeyHandler;

    {
        auto sptrKeyListener = std::make_shared<KeyListenerA>();
        oKeyHandler.registerKeyListener(sptrKeyListener);
    }

    oKeyHandler.pressKey();
}

Example 2: With std::function

#include <functional>
#include <memory>

struct KeyListenerA
{
    void operator()(int k) {}
};

struct KeyHandler
{
    std::function<void(int)>  m_funcKeyListener;

    void registerKeyListener(const std::function<void(int)> &funcKeyListener)
    {
        m_funcKeyListener = funcKeyListener;
    }

    void pressKey() { m_funcKeyListener(42); }
};

int main()
{
    KeyHandler oKeyHandler;

    {
        KeyListenerA keyListener;
        oKeyHandler.registerKeyListener(keyListener);
    }

    oKeyHandler.pressKey();
}

Upvotes: 4

Views: 7364

Answers (1)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275500

std::function<Sig> implements value semantic callbacks.

This means it copies what you put into it.

In C++, things that can be copied or moved should, well, behave a lot like the original. The thing you are copying or moving can carry with it references or pointers to an extrenal resource, and everything should work fine.

How exactly to adapt to value semantics depends on what state you want in your KeyListener; in your case, there is no state, and copies of no state are all the same.

I'll assume we want to care about the state it stores:

struct KeyListenerA {
  int* last_pressed = 0;
  void operator()(int k) {if (last_pressed) *last_pressed = k;}
};

struct KeyHandler {
  std::function<void(int)>  m_funcKeyListener;

  void registerKeyListener(std::function<void(int)> funcKeyListener) {
    m_funcKeyListener = std::move(funcKeyListener);
  }

  void pressKey() { m_funcKeyListener(42); }
};

int main() {
  KeyHandler oKeyHandler;      
  int last_pressed = -1;
  {
    KeyListenerA keyListener{&last_pressed};
    oKeyHandler.registerKeyListener(keyListener);
  }

  oKeyHandler.pressKey();
  std::cout << last_pressed << "\n"; // prints 42
}

or

  {
    oKeyHandler.registerKeyListener([&last_pressed](int k){last_pressed=k;});
  }

here we store a reference or pointer to the state in the callable. This gets copied around, and when invoked the right action occurs.

The problem I have with listeners is the doulbe lifetime issue; a listener link is only valid as long as both the broadcaster and reciever exist.

To this end, I use something like this:

using token = std::shared_ptr<void>;
template<class...Message>
struct broadcaster {
  using reciever = std::function< void(Message...) >;

  token attach( reciever r ) {
    return attach(std::make_shared<reciever>(std::move(r)));
  }
  token attach( std::shared_ptr<reciever> r ) {
    auto l = lock();
    targets.push_back(r);
    return r;
  }
  void operator()( Message... msg ) {
    decltype(targets) tmp;
    {
      // do a pass that filters out expired targets,
      // so we don't leave zombie targets around forever.
      auto l = lock();
      targets.erase(
        std::remove_if( begin(targets), end(targets),
          [](auto&& ptr){ return ptr.expired(); }
        ),
        end(targets)
      );
      tmp = targets; // copy the targets to a local array
    }
    for (auto&& wpf:tmp) {
      auto spf = wpf.lock();
      // If in another thread, someone makes the token invalid
      // while it still exists, we can do an invalid call here:
      if (spf) (*spf)(msg...);
      // (There is no safe way around this issue; to fix it, you
      // have to either restrict which threads invalidation occurs
      // in, or use the shared_ptr `attach` and ensure that final
      // destruction doesn't occur until shared ptr is actually
      // destroyed.  Aliasing constructor may help here.)
    }
  }
private:
  std::mutex m;
  auto lock() { return std::unique_lock<std::mutex>(m); }
  std::vector< std::weak_ptr<reciever> > targets;
};

which converts your code to:

struct KeyHandler {
  broadcaster<int> KeyPressed;
};

int main() {
  KeyHandler oKeyHandler;      
  int last_pressed = -1;
  token listen;
  {
    listen = oKeyHandler.KeyPressed.attach([&last_pressed](int k){last_pressed=k;});
  }

  oKeyHandler.KeyPressed(42);

  std::cout << last_pressed << "\n"; // prints 42
  listen = {}; // detach

  oKeyHandler.KeyPressed(13);
  std::cout << last_pressed << "\n"; // still prints 42
}

Upvotes: 2

Related Questions