可以删除绑定函数时如何安全地使用回调
How to safely use callbacks when the bound function could be deleted
在下面的代码中,我们正在创建一个对象,绑定一个函数并在删除对象之后调用它。
这显然会导致分段错误,因为在删除后使用了基础对象。
在为异步数据提供回调的库上下文中,我们应该如何防止回调函数指向 nullptr
?
你可以在cpp上测试。sh/5ubbg
#include <memory>
#include <functional>
#include <iostream>
class CallbackContainer {
public:
std::string data_;
CallbackContainer(std::string data): data_(data) {}
~CallbackContainer() {}
void rawTest(const std::string& some_data);
};
void CallbackContainer::rawTest(const std::string& some_data) {
std::cout << data_ << " " << some_data << std::endl;
}
int main(int /* argc */, char const** /* argv */) {
std::unique_ptr<CallbackContainer> container;
container.reset(new CallbackContainer("Internal data"));
auto callback = std::bind(&CallbackContainer::rawTest, container.get(), std::placeholders::_1);
callback("Before");
std::cout << &callback << std::endl;
container.reset();
std::cout << &callback << std::endl;
callback("After");
return 0;
}
Returns:
> Internal data Before
> 0x7178a3bf6570
> 0x7178a3bf6570
> Error launching program (Segmentation fault)
我在使用 boost asio 时喜欢的方式:
我在使用 boost asio 时遇到了同样的问题。我们需要向 io_service
注册回调,并且很难实现某种 Manager
class 来管理我们可能创建的对象的生命周期。
因此,我实施了 Michael Caisse 在 cppcon2016 中建议的内容。我开始将 shared_ptr
传递给 std::bind
的对象。
我曾经延长对象的生命周期,在回调中,你可以决定是再次延长对象的生命周期(通过再次注册回调)还是让它自动消亡。
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
auto func = std::bind(&MyClass::MyMemFunc, this, ptr);
ptr.reset();
In the context of a library providing callbacks for asynchronous data, how are we supposed to prevent callback functions to point to a nullptr?
我不会说这是最好的解决方案,但使用我的上述方法,您可以检测是否需要在回调中进一步处理。
这可能不是有效的方法,但不会导致任何未定义的行为。
void CallbackContainer::rawTest(const std::string& some_data, std::shared<CallbackContainer> ptr)
{
if (ptr.use_count() == 1) {
// We are the only owner of the object.
return; // and the object dies after this
}
std::cout << data_ << " " << some_data << std::endl;
}
编辑:
显示如何使用 std::enable_shared_from_this:
的示例代码
#include <iostream>
#include <memory>
#include <functional>
class ABCD: public std::enable_shared_from_this<ABCD> {
public:
void call_me_anytime()
{
std::cout << "Thanks for Calling Me" << std::endl;
}
public:
ABCD(void)
{
std::cout << "CONSTRUCTOR" << std::endl;
}
~ABCD(void)
{
std::cout << "DESTRUCTOR" << std::endl;
}
};
int main(void)
{
auto ptr = std::make_shared<ABCD>();
auto cb = std::bind(&ABCD::call_me_anytime, ptr->shared_from_this());
ptr.reset();
std::cout << "RESETING SHARED_PTR" << std::endl;
std::cout << "CALLING CALLBACK" << std::endl;
cb();
std::cout << "RETURNING" << std::endl;
return 0;
}
输出:
CONSTRUCTOR
RESETING SHARED_PTR
CALLING CALLBACK
Thanks for Calling Me
RETURNING
DESTRUCTOR
如果您可以共享所有权,请执行以下操作:
int main(int /* argc */, char const** /* argv */) {
std::shared_ptr<CallbackContainer> container; // shared pointer
container.reset(new CallbackContainer("Internal data"));
// shared with functor
auto callback = std::bind(&CallbackContainer::rawTest, container, std::placeholders::_1);
callback("Before");
std::cout << &callback << std::endl;
container.reset();
std::cout << &callback << std::endl;
callback("After");
return 0;
}
如果不是,您应该以某种方式明确地将无效性传递给函数对象。
这假设您知道容器何时被删除,并且在 before 之前手动显式无效:
int main(int /* argc */, char const** /* argv */) {
std::unique_ptr<CallbackContainer> container;
container.reset(new CallbackContainer("Internal data"));
std::atomic<CallbackContainer*> container_raw(container.get());
auto callback = [&container_raw] (std::string data)
{
if (auto c = container_raw.load())
c->rawTest(data);
};
callback("Before");
std::cout << &callback << std::endl;
container_raw.store(nullptr);
container.reset();
std::cout << &callback << std::endl;
callback("After");
return 0;
}
对于asio情况,通常使用shared_from_this()
,比如std::bind(&MyClass::MyMemFunc, shared_from_this(), ptr);
作为后续,我们决定使用与 Alex Guteniev 的命题相似的 roscpp method。
我们没有显式地执行 std::bind
,而是在内部使用它,并将父级作为 std::weak_ptr<const void>
指针指向 std::shared_ptr<P>
(因为它会与 unique_ptr 冲突)。
API 看起来像:
std::shared_ptr<Container> container;
queue.subscribe(&Container::callback_method, container);
订阅函数如下所示,其中 T 是数据的显式类型(Class-wise),但 P 是父级 class 的隐式 class(在本例中为容器).
template <class P>
std::shared_ptr<ThreadedQueue<T>> subscribe(void (P::*function_pointer)(std::shared_ptr<const T>), std::shared_ptr<P> parent, size_t queue_size = -1) {
callback_ = std::bind(function_pointer, parent.get(), std::placeholders::_1);
parent_ = std::weak_ptr<const void>(parent);
}
调用回调时,我们做以下检查:
if(auto lock = parent_.lock()) {
callback_(data);
}
在下面的代码中,我们正在创建一个对象,绑定一个函数并在删除对象之后调用它。
这显然会导致分段错误,因为在删除后使用了基础对象。
在为异步数据提供回调的库上下文中,我们应该如何防止回调函数指向 nullptr
?
你可以在cpp上测试。sh/5ubbg
#include <memory>
#include <functional>
#include <iostream>
class CallbackContainer {
public:
std::string data_;
CallbackContainer(std::string data): data_(data) {}
~CallbackContainer() {}
void rawTest(const std::string& some_data);
};
void CallbackContainer::rawTest(const std::string& some_data) {
std::cout << data_ << " " << some_data << std::endl;
}
int main(int /* argc */, char const** /* argv */) {
std::unique_ptr<CallbackContainer> container;
container.reset(new CallbackContainer("Internal data"));
auto callback = std::bind(&CallbackContainer::rawTest, container.get(), std::placeholders::_1);
callback("Before");
std::cout << &callback << std::endl;
container.reset();
std::cout << &callback << std::endl;
callback("After");
return 0;
}
Returns:
> Internal data Before
> 0x7178a3bf6570
> 0x7178a3bf6570
> Error launching program (Segmentation fault)
我在使用 boost asio 时喜欢的方式:
我在使用 boost asio 时遇到了同样的问题。我们需要向 io_service
注册回调,并且很难实现某种 Manager
class 来管理我们可能创建的对象的生命周期。
因此,我实施了 Michael Caisse 在 cppcon2016 中建议的内容。我开始将 shared_ptr
传递给 std::bind
的对象。
我曾经延长对象的生命周期,在回调中,你可以决定是再次延长对象的生命周期(通过再次注册回调)还是让它自动消亡。
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
auto func = std::bind(&MyClass::MyMemFunc, this, ptr);
ptr.reset();
In the context of a library providing callbacks for asynchronous data, how are we supposed to prevent callback functions to point to a nullptr?
我不会说这是最好的解决方案,但使用我的上述方法,您可以检测是否需要在回调中进一步处理。
这可能不是有效的方法,但不会导致任何未定义的行为。
void CallbackContainer::rawTest(const std::string& some_data, std::shared<CallbackContainer> ptr)
{
if (ptr.use_count() == 1) {
// We are the only owner of the object.
return; // and the object dies after this
}
std::cout << data_ << " " << some_data << std::endl;
}
编辑:
显示如何使用 std::enable_shared_from_this:
的示例代码#include <iostream>
#include <memory>
#include <functional>
class ABCD: public std::enable_shared_from_this<ABCD> {
public:
void call_me_anytime()
{
std::cout << "Thanks for Calling Me" << std::endl;
}
public:
ABCD(void)
{
std::cout << "CONSTRUCTOR" << std::endl;
}
~ABCD(void)
{
std::cout << "DESTRUCTOR" << std::endl;
}
};
int main(void)
{
auto ptr = std::make_shared<ABCD>();
auto cb = std::bind(&ABCD::call_me_anytime, ptr->shared_from_this());
ptr.reset();
std::cout << "RESETING SHARED_PTR" << std::endl;
std::cout << "CALLING CALLBACK" << std::endl;
cb();
std::cout << "RETURNING" << std::endl;
return 0;
}
输出:
CONSTRUCTOR
RESETING SHARED_PTR
CALLING CALLBACK
Thanks for Calling Me
RETURNING
DESTRUCTOR
如果您可以共享所有权,请执行以下操作:
int main(int /* argc */, char const** /* argv */) {
std::shared_ptr<CallbackContainer> container; // shared pointer
container.reset(new CallbackContainer("Internal data"));
// shared with functor
auto callback = std::bind(&CallbackContainer::rawTest, container, std::placeholders::_1);
callback("Before");
std::cout << &callback << std::endl;
container.reset();
std::cout << &callback << std::endl;
callback("After");
return 0;
}
如果不是,您应该以某种方式明确地将无效性传递给函数对象。 这假设您知道容器何时被删除,并且在 before 之前手动显式无效:
int main(int /* argc */, char const** /* argv */) {
std::unique_ptr<CallbackContainer> container;
container.reset(new CallbackContainer("Internal data"));
std::atomic<CallbackContainer*> container_raw(container.get());
auto callback = [&container_raw] (std::string data)
{
if (auto c = container_raw.load())
c->rawTest(data);
};
callback("Before");
std::cout << &callback << std::endl;
container_raw.store(nullptr);
container.reset();
std::cout << &callback << std::endl;
callback("After");
return 0;
}
对于asio情况,通常使用shared_from_this()
,比如std::bind(&MyClass::MyMemFunc, shared_from_this(), ptr);
作为后续,我们决定使用与 Alex Guteniev 的命题相似的 roscpp method。
我们没有显式地执行 std::bind
,而是在内部使用它,并将父级作为 std::weak_ptr<const void>
指针指向 std::shared_ptr<P>
(因为它会与 unique_ptr 冲突)。
API 看起来像:
std::shared_ptr<Container> container;
queue.subscribe(&Container::callback_method, container);
订阅函数如下所示,其中 T 是数据的显式类型(Class-wise),但 P 是父级 class 的隐式 class(在本例中为容器).
template <class P>
std::shared_ptr<ThreadedQueue<T>> subscribe(void (P::*function_pointer)(std::shared_ptr<const T>), std::shared_ptr<P> parent, size_t queue_size = -1) {
callback_ = std::bind(function_pointer, parent.get(), std::placeholders::_1);
parent_ = std::weak_ptr<const void>(parent);
}
调用回调时,我们做以下检查:
if(auto lock = parent_.lock()) {
callback_(data);
}