【问题标题】:Using std::bind and std::function with a class member causes the callback to use an old object reference?将 std::bind 和 std::function 与类成员一起使用会导致回调使用旧对象引用?
【发布时间】:2018-08-20 12:27:29
【问题描述】:

这令人困惑,但基本上我所拥有的是一个使用 c++11 std::functionstd::bind 具有回调函数的类。一切正常。但是当我需要通过重新分配对象来重置所有内容时,似乎新对象并没有让所有引用都正确。这是一个演示我的问题的示例程序:

#include <functional>
#include <list>
#include <iostream>
#include <string>

class OtherClass
{
public:
    OtherClass() = default;

    void RegisterCallback(std::function<void(void)> f) {
        callback = f;
    }

    void PrintThings() {
        callback();
    }

    std::function<void(void)> callback;
};

class MyClass
{
public:
    MyClass() {
        list_of_things.push_back("thing1");
        list_of_things.push_back("thing2");
        list_of_things.push_back("thing3");
        list_of_things.push_back("thing4");

        other_class.RegisterCallback(std::bind(&MyClass::MyFunction, this));
    }

    void PrintThings() {
        MyFunction();
        other_class.PrintThings();
    }

    void MyFunction() {
        auto a = this;
        for (auto& thing: list_of_things)
        {
            std::cout << thing << std::endl;
        }
    }

    OtherClass other_class;
    std::list<std::string> list_of_things;
};

int main()
{
    MyClass my_class;
    my_class.PrintThings();
    my_class = MyClass();
    my_class.PrintThings();
    std::cout << "done" << std::endl;
}

这有点令人困惑,但基本上如果您使用调试标志进行编译并单步执行,您会发现我们第一次调用my_class.PrintThings() 时会打印两次;一次用于MyFunction() 调用,一次用于other_class.PrintThings() 调用,该调用调用MyFunction 作为回调。然后,我们用my_class = MyClass() 替换对象,调用一个新的构造函数等等。当我们单步执行时,我们发现它会在调用MyFunction 时打印列表,但在调用other_class.PrintThings()MyFunction 有一个变量 a 我用来查看对象的地址;第二次通过a 有不同的地址,具体取决于MyFunction 是否作为来自OtherClass 的回调调用。

在这个例子中,有问题的 ghost 对象只有一个空列表(可能是被销毁的结果),但在我遇到的实际程序中,它被垃圾内存填满并导致分段错误。我还注意到我的调试器有一些奇怪的行为;当它到达幽灵对象时,它不会只是介入或越过,它会跳过该函数,除非我在其中放置断点。

发生了什么事?为什么第二次回调没有正确绑定?我需要在析构函数中做些什么特别的事情吗?我是否缺少对函数指针或std::bind 的一些基本理解?

【问题讨论】:

  • my_class = MyClass() 调用副本,但您没有自定义赋值运算符,它将重新连接 other_class 的回调与分配对象。您看到未定义的行为。如果你在同一个对象中需要这种回调机制,你应该使用 pimpl idiom
  • 请添加预期和实际输出,我相信你的例子可以更短
  • @RichardHodges 实际上,编译器生成的复制赋值运算符将复制类的所有成员,从而复制 OtherClass 实例,从而也复制其 std::function 对象。另外,调用未初始化的 std::function 不是未定义的行为,它应该抛出 std::bad_function_call 异常。
  • @fkorsa 它将复制一个 std::function 引用实例的成员函数,该实例的成员函数在复制后将不存在

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


【解决方案1】:

发生了什么事?

未定义的行为

为什么第二次没有正确绑定回调?

因为您在将新 MyClass 分配给 my_class 的过程中创建了新的 OtherClass。你还没有初始化它的回调。

我需要在析构函数中做些什么特别的事情吗?

在析构函数、赋值和复制构造函数中。这是因为您将“我自己”的地址存储在自己身上。这一切都很好,直到对象更改地址,它会在复制时更改。请注意,在下面的代码中,所有三个都由 smart_ptr 处理。

一种解决方案是重构 MyClass 以使用 pimpl 习惯用法。即,该类是其地址永远不会改变的实现的包装器。

#include <functional>
#include <iostream>
#include <list>
#include <string>
#include <memory>

class OtherClass
{
public:
    OtherClass() = default;

    void RegisterCallback(std::function<void(void)> f) {
        callback = f;
    }

    void PrintThings() {
        callback();
    }

    std::function<void(void)> callback;
};


class MyClass
{
    struct Impl
    {
        Impl()
        {
            list_of_things.push_back("thing1");
            list_of_things.push_back("thing2");
            list_of_things.push_back("thing3");
            list_of_things.push_back("thing4");
        }

        void MyFunction() 
        {
            for (auto& thing: list_of_things)
            {
                std::cout << thing << std::endl;
            }
        }

        void PrintThings() {
            MyFunction();
            other_class.PrintThings();
        }

        OtherClass other_class;
        std::list<std::string> list_of_things;
    };

    std::unique_ptr<Impl> impl_;

public:
    MyClass() 
    : impl_(std::make_unique<Impl>())
    {
        impl_->other_class.RegisterCallback(std::bind(&Impl::MyFunction, impl_.get()));
    }

    void PrintThings() {
        impl_->PrintThings();
    }


};

int main()
{
    MyClass my_class;
    my_class.PrintThings();
    my_class = MyClass();
    my_class.PrintThings();
    std::cout << "done" << std::endl;
}

预期输出:

thing1
thing2
thing3
thing4
thing1
thing2
thing3
thing4
thing1
thing2
thing3
thing4
thing1
thing2
thing3
thing4
done

【讨论】:

  • 是的,我在发布这篇文章十分钟后才意识到我在床上的错误。就我而言,我可能会将 RegisterCallback 函数从构造函数中移出并移到 init 函数中,但您的解决方案在其他情况下可能会有用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
  • 2016-03-27
  • 1970-01-01
  • 2015-06-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多