Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using std::function and std::bind to store callback and handle object deletion.

I want to implement a manager that stores callbacks to member functions of polymorphic classes using C++11. The issue is that I am not sure how to handle the case where the object that the member belongs to gets deleted or should be deleted and I want to make the interface as simple as possible.

So I thought of the following: Store a std::weak_ptr to the object as well as a std::function to the member.

The following seems to work:

class MyBase {
public:
    MyBase() {}
    virtual ~MyBase() {}
};
//--------------------------------------------------

class MyClass : public MyBase {
public:
    MyClass() : MyBase() {}
    void myDouble(double val) const { std::cout << "Value is: " << val << std::endl; }
};
//--------------------------------------------------

Class Manager {
public:
    void setFunction(std::weak_ptr<MyBase> base, std::function<void(double)> func) {
        m_function.first  = base;
        m_function.second = func;
    }
private:
    std::pair<std::weak_ptr<MyBase>, std::function<void(double)>> m_function;
};

To use this:

Manager db;
std::shared_ptr<MyClass> myClass = std::make_shared<MyClass>();
db.setFunction(myClass, std::bind(&MyClass::myDouble, myClass, std::placeholders::_1));

Now I want to hide the std::bind part from the user, so that he only needs to call:

db.setFunction(myClass, &MyClass::myDouble);

So I want to get almost the following working in my manager function:

void setFunc2(std::weak_ptr<MyBase> base, std::function<void(double)> func) {
    m_function.first  = base;
    m_function.second = std::bind(func, base, std::placeholders::_1);
}

But the above gives errors:

error: no match for 'operator=' (operand types are 'std::function<void(double)>' and 
'std::_Bind_helper<false, std::function<void(double)>&, std::weak_ptr<MyBase>&, const std::_Placeholder<1>&>::type {aka std::_Bind<std::function<void(double)>(std::weak_ptr<MyBase>, std::_Placeholder<1>)>}')
         m_function.second = std::bind(func, base, std::placeholders::_1);

Is there a better way to do this, or perhaps a way to get this working?

Something interesting that I notice. If I use the std::shared_ptr the use_count() gets incremented with the call to std::bind in the original code. Thus I can not manually reset/destroy the object unless I unset the member on my manager. Where is this behaviour documented, I normally use cppreference?

I have looked at the following question but can't seem to get it working for my problem: How can I use polymorphism with std::function?

like image 439
CJCombrink Avatar asked Dec 21 '15 14:12

CJCombrink


People also ask

What does std :: bind do in C++?

std::bind is for partial function application. That is, suppose you have a function object f which takes 3 arguments: f(a,b,c); You want a new function object which only takes two arguments, defined as: g(a,b) := f(a, 4, b);

How does std:: bind works?

Internally, std::bind() detects that a pointer to a member function is passed and most likely turns it into a callable objects, e.g., by use std::mem_fn() with its first argument.

What is boost bind?

boost::bind is a generalization of the standard functions std::bind1st and std::bind2nd. It supports arbitrary function objects, functions, function pointers, and member function pointers, and is able to bind any argument to a specific value or route input arguments into arbitrary positions.


3 Answers

I like decoupling my listener/broadcaster from the implementation of the listener.

This means I cannot place requirements on the listener. It cannot require the listener be allocated in a particular way.

The easiest method I have found is to have the broadcaster return a token whose lifetime determines the lifetime of the connection.

using token = std::shared_ptr<void>;

template<class...Args>
struct broadcaster {
  using target = std::function<void(Args...)>;
  using wp_target = std::weak_ptr<target>;
  using sp_target = std::shared_ptr<target>;
  static sp_target wrap_target( target t ) {
    return std::make_shared<target>(std::move(t));
  };

  token start_to_listen( target f ) {
    auto t = wrap_target(std::move(f));
    targets.push_back(t);
    return t;
  }
  void broadcast( Args... args ) {
    targets.erase(
      std::remove_if( targets.begin(), targets.end(),
        [&]( wp_target t )->bool { return t.expired(); }
      ),
      targets.end()
    );
    auto targets_copy = targets; // in case targets is modified by listeners
    for (auto wp : targets_copy) {
      if (auto sp = wp.lock()) {
        (*sp)(args...);
      }
    }
  }
  std::vector<wp_target> targets;
};

this forces people who register listeners to keep std::shared_ptr<void> around.

We can even make it fancier, where the destruction of the last shared_ptr<void> actually removes the listener from the list immediately. But the above lazy deregistration seems to work reasonably well in my experience, and it is relatively easy to make it multi-thread friendly. (one serious problem is what happens when a broadcast event removes or adds things to the list of listeners: adapting the above for it to work is nice and easy with the rule that listeners added when broadcasting do not get the broadcast, and listeners removed during broadcasting do not get the broadcast. Listeners removed concurrently during broadcast can get the broadcast in most of my implementations... That gets expensive to avoid.)


We could instead decouple it differently. The listener could pass a std::function and a std::weak_ptr separately to the broadcaster, who stores both and only calls the std::function if the std::weak_ptr is valid.

like image 110
Yakk - Adam Nevraumont Avatar answered Oct 20 '22 02:10

Yakk - Adam Nevraumont


I like Yakk's approach. Here's an updated version that fixes a few compile issues (e.g. cannot name function 'register'). It also adds a rm_callback method for clients to easily remove their registration without forcing their registration token to go out of scope or knowing the internals. I didn't like scanning the list every time an event was broadcast so I added a deleter on the shared pointer which does the cleanup task. All new bugs introduced or inefficiencies are mine. The alert reader should be aware of threading issues when modifying the list while broadcasting...

using token = std::shared_ptr<void>;
template<class...Args>
struct broadcaster {
    using target = std::function<void(Args...)>;
    using wp_target = std::weak_ptr<target>;
    using sp_target = std::shared_ptr<target>;

    token add_callback(target f) {
        sp_target t(new target(std::move(f)), [&](target*obj) { delete obj; cleanup(); });
        targets.push_back(t);
        return t;
    }

    static void rm_callback(token& t)
    {
        t.reset();
    }

    void cleanup()
    {
        targets.erase(
            std::remove_if(targets.begin(), targets.end(),
                [](wp_target t) { return t.expired(); }
            ),
            targets.end()
        );
    }

    void broadcast(Args... args) {
        for (auto wp : targets) {
            if (auto sp = wp.lock()) {
                (*sp)(args...);
            }
        }
    }

    std::vector<wp_target> targets;
};

// declare event taking a string arg
broadcaster<std::string> myEvent;
like image 29
Glenn Avatar answered Oct 20 '22 02:10

Glenn


Template setFunction so that you can accept pointer-to-member-of-derived, and don't have to write 12 overloads for the combinations of cv/ref qualifiers.

template<class D, class D2, class F>
void setFunction(const std::shared_ptr<D> &sp, F D2::* member) {
    // optionally static_assert that D2 is a base of D.
    m_function.first  = sp;
    m_function.second = std::bind(member, sp.get(), std::placeholders::_1);
}

Obviously you need to make sure you lock() m_function.first before calling m_function.second.

Alternatively, just use a lambda that captures both the weak_ptr and the member function pointer:

std::function<void(double)> m_function;

template<class D, class D2, class F>
void setFunction(const std::shared_ptr<D> &sp, F D2::* member) {
    std::weak_ptr<D> wp = sp;
    m_function = [wp, member](double d) {
        if(auto sp = wp.lock()){
             ((*sp).*member)(d);
        }
        else {
             // handle pointer no longer valid case.
        }
    };
}
like image 21
T.C. Avatar answered Oct 20 '22 03:10

T.C.