3

Currently I am trying out a code that does essentially the following:

void f(int x) { cout << "f("<<x<<")" << endl; }

class C
{
public:
   void m(int x) { cout << "C::m("<<x<<")" << endl; }
};

class C2
{
public:
   void registerCallback(function<void(int)> f)
   {
      v.push_back(f);
   }

private:
   vector<function<void(int)>> v;

   void callThem()
   {
      for (int i=0; i<v.size(); i++)
      {
         v[i](i);
      }
   }
};

int main()
{
   C2 registrar;

   C c;
   registrar.registerCallback(&f); // Static function
   registrar.registerCallback(bind(&C::m, &c, placeholders::_1)); // Method

   return 0;
}

This works pretty well. However I got stuck with this pattern. I would like to check if a callback already has been registered and I would like to be able to unregister a callback by removing it from the vector. I just learned that std::function objects can not be compared which means it is impossible to search for their existence in the container.

So I need an alternative. Of course I would like to keep compile-time type checks and the ability to register methods of arbitrary classes.

How can I achieve a similar solution that allows unregistering the callback and checking for double registration? Are there any trade-offs I need to accept?

Silicomancer
  • 8,604
  • 10
  • 63
  • 130
  • [This](http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible) might help. The member function delegate class implemented there does do far more than you ask for, but in particular, it has ordering implemented. – Pradhan Dec 23 '14 at 20:38

3 Answers3

3

The underlying problem is that most function objects are not comparable. While plain function pointers and user-defined function objects with an equality operator can be compared, lambdas, the result of std::bind(), etc. cannot. Using the address of function objects to identify them is generally not a suitable approach because the objects tend to be copied. It may be possible to use std::reference_wrapper<Fun> to avoid having them copied but the objects stored in a std::function<F> will still have a different address.

With C++11 variadic templates it is reasonably easy to create a custom version of std::function<...> which provides comparison facilities. There may even be two versions thereof:

  • One version which accepts arbitrary function objects but can, obviously, only compare comparable function objects: depending on whether the constructor argument provides an equality operator or not a suitable base class is used.
  • One version which always provides a working comparison and, obviously, fails to be usable with non-equality-comparable objects.

The latter is slightly easier to define and would look something like this:

template <typename...> class comparable_function;
template <typename RC, typename... Args>
class comparable_function<RC(Args...)> {
    struct base {
        virtual ~base() {}
        virtual RC    call(Args... args) = 0;
        virtual base* clone() const = 0;
        virtual bool  compare(base const*) const = 0;
    };
    template <typename Fun>
    struct concrete: base {
        Fun fun;
        concrete(Fun const& fun): fun(fun) {}
        RC call(Args... args) { return this->fun(args...); }
        base* clone() const { return new concrete<Fun>(this->fun); }
        bool compare(base const* other) const {
             concrete const* o = dynamic_cast<concrete<Fun>>(other);
             return o && this->fun == o->fun;
        }
    };
    std::unique_ptr<base> fun;
public:
    template <typename Fun>
    comparable_function(Fun fun): fun(new concrete<Fun>(fun)) {}
    comparable_function(comparable_function const& other): fun(other.fun->clone()) {}
    RC operator()(Args... args) { return this->fun->call(args); }
    bool operator== (comparable_function const& other) const {
        return this->fun->compare(other.fun.get());
    }
    bool operator!= (comparable_function const& other) { return !(this == other); }
};

I guess, I have forgotten (and/or mistyped) something but rather that's what's needed. For the optionally comparable version you'd have two versions of concrete: one which is implemented as above and another one which always returns false. Depending on whether there is an operator == for the Fun in the constructor you'd create one or the other.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • That's very intersting. But I am not sure if this actually solves my problem. I mainly need to register *methods* as callbacks. So AFAIK I am forced to use `bind` which is, as you said, non-compareable. Your approach can not be used in this case, can it? – Silicomancer Dec 23 '14 at 21:35
  • Well, you are certainly not forced to use `std::bind()`! Instead, you _could_ create a version of `std::bind()` which only accepts comparable function objects (pointers to members are comparable) and comparable parameters (you'd probably want to compare these as well) and provides a comparison operator. Depending on your needs you may get away creating a restricted version of `std::bind()`: although I think I roughly know how to implement `std::bind()` I haven't done it and I'm pretty sure it'll require more code than I'm ready to type into an answer of an SO question. – Dietmar Kühl Dec 23 '14 at 21:39
  • I understand. Thanks anyway! Do you know if there is some well-tried code out there that implements this approach? I suppose I am not the first one trying to solve this. – Silicomancer Dec 23 '14 at 21:44
  • You could check [Boost](http://boost.org/) to see if they implemented a comparable version. I may try to create a version later today (I still _think_ that implementing `std::bind()` isn't that hard but I need to do it to find out) but, of course, that wouldn't qualify as _well-tried_ :-) – Dietmar Kühl Dec 23 '14 at 21:54
  • Great to here. I am not experienced with those part of the standard so it would probably take ages for me to implement something like that. – Silicomancer Dec 23 '14 at 22:08
  • It seems that boost function pointers exist and can be compared but they do not support binding. – Silicomancer Dec 23 '14 at 22:17
  • 1
    I put an initial implementation of `bind()` with comparison-aware function objects together on a [branch of my work on a standard C++ library](https://github.com/dietmarkuehl/kuhllib/tree/bind) (see files in `src/nstd/functional`). All comparisons only work when trying to compare identical types, i.e., they should be sufficient for use in `comparable_function`. Currently support of `reference_wrapper` and pointer-like calls as is needed for calls on existing objects is missing, though. – Dietmar Kühl Dec 24 '14 at 04:07
1

Well, what if you did something like:

class C2
{
public:
   void registerCallback(function<void(int)>* f)
   {
      v.push_back(f);
   }

private:
   vector<function<void(int)>*> v;

   void callThem()
   {
      for (int i=0; i<v.size(); i++)
      {
         v[i][0](i);
      }
   }
};

Functions aren't comparable, but pointers are. The reason functions aren't comparable is that there is no way to determine if functions are equal (not just in C++, in computer science). I.e., there is no way to determine if functions have the same value. However, by using pointers, we can at least see if they occupy the same space in memory.

I'm not very familiar with how bind and other std higher-order functions work under-the-hood. Take care when using this, and you may have to perform your own checks when a callback is registered or before you call bind, to make sure that you don't have two duplicate binds of the same function but that occupy different places in memory.

Matt G
  • 1,661
  • 19
  • 32
  • Interesting ! You could even pass f by reference (no need to change outside code) and take its adress while registering ! – Christophe Dec 23 '14 at 20:42
  • And maybe use a set instead of a vector to prevent double registration. This is allowed again because pointers are comparable. – Alex Dec 23 '14 at 20:47
  • If I understand your approach the right way I would need to use exactly the same function object for registering und unregistering, right? But how could this solve the double-registration problem? One would surely not use the same function object for accidentally registering the same function twice. – Silicomancer Dec 23 '14 at 21:02
1

I think there is a fundamental issue with the automatic detection of double-registration. When do you consider two functions to be identical? For normal function pointers, you could use the address, but with std::bind and especially lambda functions, you will be having a problem:

class C2
{
public:
   void registerCallback(??? f)
   {
      if (v.find(f, ???) == v.end())
          v.push_back(f);
   }
private:
   vector<function<void(int)>> v;
};

void f1(int);
void f3(int, int);
void f2(int)
{
   C2 c;
   c.registerCallback(f1);
   c.registerCallback(f1); // could be detected by address
   c.registerCallback([](int x) {});
   c.registerCallback([](int x) {}); // detected ?!? 
   c.registerCallback(std::bind(f3, _1, 1);
   c.registerCallback([](int x) {f3(x,1);}) ; // detected? 
}

It is impossible for the compiler to detect that two lambda functions are semantically identical.

I would change register to return a ID (or connection object like in Boost.Signal2) which can be used by clients to deregister callbacks. This will not prevent double registrations, though.

class C2
{
public:
   typedef ??? ID;
   ID registerCallback(??? f)
   {
      ?? id = new_id();
      v[id] = f;
   }
private:
   map<???, function<void(int)>> v;
};
Jens
  • 9,058
  • 2
  • 26
  • 43
  • I agree. Lambdas can not be compared. So they would need to be treated more basic. But still most important use-cases are methods and static functions. Both can be compared. So there could be a solution that allows double registration check and simple unregistration for these two cases, right? – Silicomancer Dec 23 '14 at 22:06
  • @Silicomancer My experience is that I almost always register lambda functions (or bound functions) as callbacks because most of the time I want to register member functions of some objects. There I need to bind the this pointer. – Jens Dec 23 '14 at 22:22