向std::async传入引用包装的可调用对象时成员变量未修改的原因
为什么向std::async传入std::ref包装的可调用对象时,原对象未被修改
你的代码返回值为4说明Y::operator()确实被正常执行了,计算逻辑没有问题;原对象m_val仍为0,是因为operator()实际运行在y的一份临时拷贝上,而非你定义的原对象,这是C++11/14标准下的典型反直觉实现行为,具体原理如下:
std::ref返回的是std::reference_wrapper<T>类型的轻量包装器,设计初衷是在模板按值传参的场景下模拟引用传递,避免对象拷贝。- C11和C14标准中,
std::reference_wrapper本身没有定义operator()成员函数。它能被当作可调用对象使用,完全依赖标准库INVOKE机制的特殊处理规则:INVOKE流程遇到reference_wrapper时,应当自动解包到其内部存储的引用,再执行调用。 - 但这个特殊解包要求在C++11/14中,仅被强制约束在「可调用对象的参数」场景:也就是你给
std::async/std::thread传函数和实参时,用std::ref包装实参一定会被正确解包为引用,不会发生拷贝。而当reference_wrapper本身作为「待执行的可调用对象」传入时,标准没有强制要求实现做特殊解包,当时的多数标准库实现会直接尝试调用这个包装器对象。 - 编译器找不到reference_wrapper的operator(),就会触发它的隐式转换函数
operator T&() const,尝试把包装器转换成可调用的Y类型。而std::async内部对可调用对象是按值存储、转发的,这个转换流程最终会调用Y的拷贝构造函数生成一份临时对象,在临时对象上执行operator()。临时对象执行完就被销毁,所有修改都不会落到原y对象上,就出现了返回值正确但原对象未更新的现象。
解决方案
你可以任选以下一种写法规避问题,都能得到原对象被正确修改的预期结果:
- 直接传入对象指针:对象指针作为可调用对象时,INVOKE规则明确要求解引用后调用,不会触发意外拷贝:
auto f = std::async(&y, 2.0);
- 用lambda捕获原对象引用,将lambda作为可调用对象传入,这是兼容性、可读性最好的写法:
auto f = std::async([&y](double val) { return y(val); }, 2.0);
- 升级到C17及以上编译标准:C17正式为
std::reference_wrapper增加了支持完美转发的operator(),会直接转发调用到内部持有的引用,不会再触发隐式转换和临时拷贝,你的原始代码在C++17兼容环境下会直接输出预期结果。
验证方式:给Y添加拷贝构造函数、析构函数的打印日志,就能看到执行流程中确实多了一次临时对象的构造和析构,对应那份被执行了operator()的拷贝。
内容的提问来源于stack exchange,提问作者theh2o
相关产品推荐
相关产品推荐

