【问题标题】:Using std::function and std::bind to store callback and handle object deletion.使用 std::function 和 std::bind 存储回调和处理对象删除。
【发布时间】:2016-03-27 15:41:25
【问题描述】:

我想实现一个管理器,它使用 C++11 存储对多态类成员函数的回调。问题是我不知道如何处理成员所属的对象被删除或应该被删除的情况,我想让界面尽可能简单。

所以我想到了以下方法:将std::weak_ptr 存储到对象,并将std::function 存储到成员。

以下似乎有效:

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

要使用这个:

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

现在我想对用户隐藏std::bind部分,这样他只需要调用:

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

所以我想在我的经理职能中实现以下工作:

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

但上面给出了错误:

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

有没有更好的方法来做到这一点,或者有办法让它发挥作用?

我注意到一些有趣的事情。如果我使用std::shared_ptr,则use_count() 会随着原始代码中对std::bind 的调用而递增。因此,除非我取消设置经理上的成员,否则我无法手动重置/销毁对象。这种行为记录在哪里,我通常使用cppreference

我查看了以下问题,但似乎无法解决我的问题:How can I use polymorphism with std::function?

【问题讨论】:

  • 关于 shared_ptr 使用计数,en.cppreference.com/w/cpp/utility/functional/bind 表示“std::bind 的返回类型为每个 args 保存 [...] 一个对象...,[...] 由 std ::forward(arg_i)。”并在同一页面的下方“复制或移动要绑定的参数”。因为你传递了一个 shared_ptr 左值,所以你得到了一个副本。
  • &amp;MyClass::myDouble 无论如何都不会转换为 std::function&lt;void(double)&gt;
  • @T.C 我完全意识到这一点,因此是问题的原因。我不在乎签名是什么样子,我希望调用看起来像 db.setFunction(myClass, &amp;MyClass::myDouble);

标签: c++ c++11 std-function stdbind


【解决方案1】:

模板setFunction 以便您可以接受指向派生成员的指针,并且不必为 cv/ref 限定符的组合编写 12 个重载。

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

显然,在调用m_function.second 之前,您需要确保您lock() m_function.first

或者,只需使用捕获 weak_ptr 和成员函数指针的 lambda:

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

【讨论】:

  • 幸运的是编译器确实抛出了一个错误cannot convert 'random* const' to 'MyBase*' in assignment。出于兴趣,这样的static_assert 会如何进行检查:static_assert(std::is_base_of&lt;MyBase, D&gt;::value == true, "Object must be a derived class of \'MyBase\'");
  • 有没有聪明的方法来处理db.setFunction(myClass, nullptr);。对于示例,它没有意义,但在我的实际代码中它确实如此。
  • @TheBadger 定义“句柄”。
  • 对不起。这样可以使用nullptrvoid(无方法)调用该函数,然后能够在函数内部检测到它。如果我只是调用上面的函数,我会得到编译器错误
  • @TheBadger template&lt;class D&gt; void setFunction(const std::shared_ptr&lt;D&gt; &amp;sp, std::nullptr_t) { .... }
【解决方案2】:

我喜欢将我的监听器/广播器与监听器的实现分离。

这意味着我不能对听众提出要求。它不能要求以特定方式分配侦听器。

我发现的最简单的方法是让广播公司返回一个令牌,其生命周期决定了连接的生命周期。

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

这会迫使注册听众的人保留std::shared_ptr&lt;void&gt;

我们甚至可以让它变得更漂亮,其中最后一个 shared_ptr&lt;void&gt; 的销毁实际上会立即从列表中删除侦听器。但是根据我的经验,上面的惰性注销似乎工作得相当好,而且使它对多线程友好也相对容易。 (一个严重的问题是当广播事件删除或添加内容到侦听器列表时会发生什么:根据广播时添加的侦听器不会收到广播的规则,调整上面的内容以使其工作既好又容易,并且侦听器在广播期间被删除广播没有得到广播。在广播期间删除的侦听器同时可以在我的大多数实现中得到广播......避免这种情况会很昂贵。)


我们可以用不同的方式解耦它。侦听器可以将std::functionstd::weak_ptr 分别传递给广播者,广播者将两者都存储并仅在std::weak_ptr 有效时才调用std::function

【讨论】:

  • start_to_listen()的第一行std::move(f)的生命周期和“分配空间”是什么? (会在堆上分配吗?——我对右值引用atm了解不多。)
  • @acsor 我将构造一个共享 ptr 移动到 std 函数,将其推入弱指针向量中,然后将共享 ptr 返回到 void。共享 ptr 管理的数据在堆上。
  • 感谢@Yakk,实际上我不久前自己解决了这个问题,注意到std::make_shared() 制作了一份数据副本并将其存储在堆上。那是我缺少的部分 ;-)。
  • 看来-&gt; bool { return t.lock(); }应该是-&gt; { return t.expired(); }
【解决方案3】:

我喜欢 Yakk 的做法。这是修复了一些编译问题的更新版本(例如,无法命名函数“注册”)。它还为客户端添加了一个 rm_callback 方法,可以轻松删除他们的注册,而不会强制他们的注册令牌超出范围或了解内部结构。我不喜欢每次广播事件时都扫描列表,所以我在共享指针上添加了一个删除器,它执行清理任务。所有引入的新错误或低效率都是我的。在广播时修改列表时,警报读者应该注意线程问题...

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;

【讨论】:

  • 您的 add_callback 假定广播者通过隐式捕获 this 比侦听器令牌寿命更长。如果您的 lambda 超出本地范围,请不要使用 [&amp;],因为它隐藏了此类依赖项。其次,您可能希望在broadcast复制 targets,就好像广播列表已被监听器修改,for(:) 循环不变量将失败。我的也有这些错误之一;在我的固定它。
猜你喜欢
  • 1970-01-01
  • 2013-04-16
  • 2023-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多