使用std::function的回调函数替代方案

3

目前我正在尝试一段代码,其基本功能如下:

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;
}

这个方案相当不错。但是我在这个模式上遇到了困难。我想检查回调是否已经注册,并且我想通过将其从向量中移除来取消注册回调。我刚学会了,std::function对象无法进行比较,这意味着无法在容器中搜索它们的存在。
所以我需要一种替代方案。当然,我希望保持编译时类型检查和注册任意类方法的能力。
如何实现类似的解决方案,允许取消注册回调和检查双重注册?我需要接受哪些权衡?

这个链接可能会有所帮助。那里实现的成员函数委托类做得比你要求的要多得多,但特别是它实现了排序。 - Pradhan
3个回答

3
根本问题在于大多数函数对象无法比较。虽然普通函数指针和带有相等运算符的用户定义函数对象可以进行比较,但是lambda表达式、std::bind()的结果等则不能。使用函数对象的地址来标识它们通常不是一种合适的方法,因为这些对象往往会被复制。可能可以使用std::reference_wrapper<Fun>来避免复制它们,但存储在std::function<F>中的对象仍将具有不同的地址。
使用C++11可变参数模板可以相对容易地创建一个自定义版本的std::function<...>,该版本提供比较功能。甚至可能会有两个版本:
- 一个版本接受任意函数对象,但显然只能比较可比较的函数对象:根据构造函数参数是否提供相等运算符,选择适当的基类。 - 一个版本始终提供有效的比较,并且显然无法与非相等可比较对象一起使用。
后者稍微容易定义一些,可能看起来像这样:
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); }
};

我猜,我可能忘记了(或者打错了)某些东西,但这正是所需的。对于可选比较版本,您将拥有两个版本的 concrete :一个实现如上所述,另一个始终返回 false 。根据构造函数中是否存在 Fun 运算符 == ,您将创建一个或另一个版本。


这很有趣。但我不确定这是否真正解决了我的问题。我主要需要将方法注册为回调函数。所以据我所知,我被迫使用bind,就像你说的那样,这是无法比较的。你的方法在这种情况下不能使用,对吗? - Silicomancer
你当然不必使用std::bind()!相反,你可以创建一个版本的std::bind(),它只接受可比较的函数对象(成员指针是可比较的)和可比较的参数(你可能也想要比较这些参数),并提供一个比较运算符。根据你的需求,你可以创建一个受限制的std::bind()版本:虽然我大致知道如何实现std::bind(),但我还没有做过,而且我很确定它需要比我准备在SO问题的答案中输入的代码更多。 - Dietmar Kühl
我明白了。不管怎样谢谢你!你知道有没有一些经过验证的代码实现了这个方法吗?我想我不是第一个尝试解决这个问题的人。 - Silicomancer
你可以查看Boost,看看他们是否实现了一个类似的版本。我今天可能会尝试创建一个版本(我仍然认为实现std::bind()并不难,但我需要去尝试一下才能确定),但是,当然,那不能算作是_经过充分尝试_的 :-) - Dietmar Kühl
1
我在标准C++库的一个分支上(请参见src/nstd/functional中的文件)编写了带有比较感知函数对象的“bind ()”的初始实现。所有比较仅在尝试比较相同类型时才起作用,即它们应足以用于“可比较函数”的使用。但目前缺少对reference_wrapper和类似指针调用的支持,这是对现有对象进行调用所需的。 - Dietmar Kühl
显示剩余2条评论

1

那么,如果你做了类似这样的事情:

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);
      }
   }
};

函数是不可比较的,但指针可以。函数无法比较的原因是无法确定函数是否相等(不仅在C++中,而是在计算机科学中)。也就是说,无法确定函数是否具有相同的值。然而,通过使用指针,我们至少可以看到它们是否占用了内存中的相同空间。
我不太熟悉 bind 和其他 std 高阶函数的底层工作方式。在使用时要小心,并且您可能需要在注册回调或在调用 bind 之前执行自己的检查,以确保您没有两个占据不同内存位置但是相同函数的重复绑定。

有趣!你甚至可以通过引用传递f(无需更改外部代码),并在注册时获取其地址! - Christophe
也许使用集合而不是向量可以防止重复注册。这是允许的,因为指针是可比较的。 - Alex
如果我正确理解您的方法,那么我需要使用完全相同的函数对象进行注册和注销,对吗?但是这如何解决双重注册问题呢?人们肯定不会意外地使用相同的函数对象来注册相同的函数两次。 - Silicomancer

1

我认为自动检测重复注册存在一个根本性问题。何时认为两个函数是相同的?对于普通函数指针,可以使用地址进行比较,但是对于std::bind和特别是lambda函数,你将会遇到问题:

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? 
}

编译器无法检测两个lambda函数在语义上是否相同。
我会将register更改为返回一个ID(或类似于Boost.Signal2中的连接对象),客户端可以使用它来注销回调。但这不会防止重复注册。
class C2
{
public:
   typedef ??? ID;
   ID registerCallback(??? f)
   {
      ?? id = new_id();
      v[id] = f;
   }
private:
   map<???, function<void(int)>> v;
};

我同意。Lambda表达式不能进行比较。因此,它们需要更基本的处理方式。但是,最重要的用例仍然是方法和静态函数。这两者可以进行比较。因此,是否有一种解决方案可以允许这两种情况进行双重注册检查和简单注销呢? - Silicomancer
@Silicomancer 我的经验是,我几乎总是将lambda函数(或绑定函数)注册为回调函数,因为大多数时候我想要注册某些对象的成员函数。在那里,我需要绑定this指针。 - Jens

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接