谈谈std::bind当绑定参数为值时可能会发生的问题,std::ref的恰当使用;

目录

在bind中采用值绑定时,会创建值的副本,进行绑定后可能会发生以下后果:

1. 值的不变性

2. 对象的深拷贝与浅拷贝问题

3. 回调时访问了已被销毁的对象

4. 无法修改原始对象

5. 性能开销

6. 解决方法

总结


今天找BUG+调试真的整得抑郁了快,原本以为简单得不得了的,绝对不可能出错的东西,就这么困扰了我大半天。现在是不到凌晨1点,代码也不想敲了,就写篇“反思录”记录博主今天踩到的坑。且听博主细细道来......

故事源于使用std::bind进行参数绑定,需要绑定的函数如下,由于是测试代码,比较简单,但是很能反映问题!!!

这里博主对socket做了封装,socket中包含一个当前套接字的文件描述符。Poller类则是对epoll的封装,包含了epoll的一系列操作。

在这里,我们明确一下listen套接字EPOLLIN事件就绪的条件:首先服务端设置了监听套接字,此时还没调用accept函数,当客户端发起连接时,由于上层并没有调用accept接受连接,所以此时客户端的连接会被维持在下层的TCP全连接队列当中。而accept的作用就是将连接提取上来,并为其设置一个struct file文件(struct file结构体中有个void*指针指向描述套接字的结构体,这也就是为什么“一切皆文件”)指向套接字,由此返回一个描述连接的文件描述符。可见,对于listen套接字EPOLLIN事件的就绪条件就是:全连接队列中有连接!

相信只要大家写过epoll和Reactor模式就大致了解下面这段代码是在做什么,其实很简单:创建一个服务端套接字(监听套接字),将监听套接字的文件描述符挂载到epoll中,并设置其EPOLLIN事件回调为调用accept接收新连接,就是如此。。。简单。。。

void acceptor(Socket &sock, Poller *epoller)
{
    int newfd = sock.Accept();
    std::cout << newfd << std::endl;
    // ......
}

int main()
{
    Poller epoller;
    Socket s;
    s.CreateServer(8889); // 创建监听套接字

    Channel channel(&epoller, s.Fd());
    channel.EnableRead();
    
    // 方式1 : 使用std::ref 正确
    channel.SetReadCallback(std::bind(acceptor, std::ref(s), &epoller));

    // 方式2 : 使用值绑定,bind创建了一个s的副本,错误
    // channel.SetReadCallback(std::bind(acceptor, s, &epoller)); // 进行回调函数绑定
    
    while(1)
    {
        std::vector<Channel*> actives;
        epoller.Poll(&actives);
        for(auto t : actives){
            std::cout << t->Fd() << std::endl;
            t->HandleEvent();
        }
        sleep(1);
        std::cout << "sleep" << std::endl;
    }
    return 0;
}

 方式 2 乍一看是不是没什么问题,参数都绑定好了,可偏偏它就是有问题!当使用方式2后,当客户端对服务端进行连接时发现服务端竟然拒绝连接!说明什么?说明监听套接字文件描述符的EPOLLIN回调函数根本就没有调用,也就是服务端压根就没启动accept函数!ceb86a09600d40ea860106ee7ccc4a50.png

哎呀,那你让客户端怎么连接嘛。。。一定是accept的问题吗,难道就不能是listen套接字的监听状态没有设置成功的原因吗?博主当前绑定的端口是8889,通过netstat命令查看了一下,端口8889上的listen套接字好像还真没被设置成功。。。

4d7b193635924da3a5e6b9bdc2197082.png

这下我也蒙了 ,但是设置监听套接字的代码部分一定是没问题的,这点博主测试过了。唯一的问题,可能还是bind进行值绑定的连锁反应。因为当把bind给注释掉之后,端口8889的监听套接字的监听状态又成功设置了。

后来博主分别作了如下改变:1、将acceptor的第一个参数改为int fd,进行套接字文件描述符的绑定,成功了; 2、将acceptor的第一个参数改为Socket * 指针类型,进行地址的绑定,也成功了3、最后就是方式1,在bind中使用std::ref()告诉bind我要进行引用绑定,也成功了。。。

4a8b42d03df84d0f8e6838f341fb9321.png

这下客户端也能连接上了,服务端也能接收到连接了,Reactor也能正常工作了。虽然还是不知道为什么,但这次的教训让我注意到了以后绑定引用的时候最好使用std::ref。

那为何需要绑定的参数是引用时,在上述代码中使用值绑定会出现问题呢?

其实这个问题跟bind并没有太大的关系,但也有一点小关系,主要还是Socket类的析构函数,在析构函数中我调用了close关闭连接。bind函数本质是创建了一个_Bind的类,对于传入的回调函数和其参数进行保存,而如果是传值传参的话,那么其实就是生成了一份Socket对象的拷贝,但由于执行函数时离开了main函数的作用域,这份拷贝会在后续调用传参中就会被释放掉,从而使得触发Socket类的析构函数,但函数内部调用close将连接释放了,监听套接字被关闭了,所以客户端连接会失败。

所以在对自定义类对象进行绑定时,我们最好使用指针、shared_ptr,或者std::ref/std::cref来修饰绑定对象。如此,便可使被绑定对象的生命周期与预期一致,且不会出现直接拷贝对象副本的情况。


在bind中采用值绑定时,会创建值的副本,进行绑定后可能会发生以下后果:

1. 值的不变性

  • 现象:回调函数中使用的值是绑定时的值,而不是调用时的值。
  • 示例
    #include <iostream>
    #include <functional>
    
    void foo(int x) {
        std::cout << "x: " << x << std::endl;
    }
    
    int main() {
        int a = 5;
        auto func = std::bind(foo, a);  // 绑定值
        a = 10;  // 修改 a
        func();  // 输出 x: 5,而不是 10
        return 0;
    }
    
  • 结果:即使 a 在绑定时被修改为 10,回调函数中仍然输出 5,因为 std::bind 复制了 a 的值。

2. 对象的深拷贝与浅拷贝问题

  • 现象:如果被绑定的对象包含资源(如指针、文件描述符等),复制对象可能会导致资源的深拷贝或浅拷贝问题。
  • 深拷贝:如果对象实现了深拷贝(即复制资源而不是共享资源),可能会有不必要的资源占用或性能开销。
  • 浅拷贝:如果对象没有实现深拷贝,可能会导致共享资源时的竞态条件或双重释放问题。
  • 示例
    class Resource {
    public:
        Resource() : data(new int(0)) {}
        ~Resource() { delete data; }
        // 假设没有实现深拷贝
        Resource(const Resource &other) : data(other.data) {}
        Resource &operator=(const Resource &other) {
            data = other.data;
            return *this;
        }
        int *data;
    };
    
    void bar(Resource r) {
        *r.data = 42;  // 修改资源
    }
    
    int main() {
        Resource r1;
        auto func = std::bind(bar, r1);  // 绑定值
        func();  // 修改 r1 的资源
        std::cout << *r1.data << std::endl;  // 这里可能会输出 42,也可能导致未定义行为
        return 0;
    }
    
  • 结果:如果 Resource 类没有正确实现深拷贝,r1 和回调函数中的副本会共享同一个资源,导致未定义行为。

3. 回调时访问了已被销毁的对象

  • 现象:如果被绑定的对象的生命周期在回调函数执行前结束,副本对象仍然存在,但可能访问到无效的状态或资源。
  • 示例
    #include <iostream>
    #include <functional>
    
    void foo(int x) {
        std::cout << "x: " << x << std::endl;
    }
    
    int main() {
        int *a = new int(5);
        auto func = std::bind(foo, *a);  // 绑定值
        delete a;  // 删除 a
        func();  // 访问已被释放的内存,未定义行为
        return 0;
    }
    
  • 结果:回调函数中尝试访问已释放的内存,导致未定义行为,可能崩溃或输出错误值。

4. 无法修改原始对象

  • 现象:如果需要在回调函数中修改原始对象的状态,但绑定的是副本,则无法实现。
  • 示例
    #include <iostream>
    #include <functional>
    
    void increment(int &x) {
        ++x;
    }
    
    int main() {
        int a = 5;
        auto func = std::bind(increment, a);  // 绑定值
        func();
        std::cout << a << std::endl;  // 输出 5,而不是 6
        return 0;
    }
    
  • 结果a 的值没有被修改,因为回调函数修改的是副本。

5. 性能开销

  • 现象:复制对象可能会导致不必要的性能开销,尤其是当对象较大或包含复杂资源时。
  • 示例
    #include <vector>
    #include <functional>
    
    void process(const std::vector<int> &v) {
        // 处理向量
    }
    
    int main() {
        std::vector<int> vec = {1, 2, 3, 4, 5};
        auto func = std::bind(process, vec);  // 绑定值
        // 如果有大量数据,复制操作会带来性能开销
        func();
        return 0;
    }
    
  • 结果:如果 vec 包含大量数据,复制操作会带来额外的内存和时间开销。

6. 解决方法

  • 使用引用绑定:通过 std::ref 或 std::cref 来绑定引用,而不是复制值。
    #include <iostream>
    #include <functional>
    
    void increment(int &x) {
        ++x;
    }
    
    int main() {
        int a = 5;
        auto func = std::bind(increment, std::ref(a));  // 绑定引用
        func();
        std::cout << a << std::endl;  // 输出 6
        return 0;
    }
    

总结

  • 值绑定会复制参数的值,导致回调函数中使用的是副本,可能无法访问原始对象的最新状态。
  • 引用绑定通过 std::ref 或 std::cref 包装参数,可以避免对象的复制,确保回调函数中使用的是原始对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白也有开发梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值