7

我有一个std::packaged_task包含一个通过复制捕获变量的 lambda。当 thisstd::packaged_task被删除时,我希望 lambda 内的变量被破坏,但我注意到如果我获得std::futurethis的关联std::packaged_task,该future对象会延长 lambda 内变量的生命周期。

例如:

#include <iostream>
#include <future>

class Dummy
{
public:
    Dummy() {std::cout << this << ": default constructed;" << std::endl;}
    Dummy(const Dummy&) {std::cout << this << ": copy constructed;" << std::endl;}
    Dummy(Dummy&&) {std::cout << this << ": move constructed;" << std::endl;}
    ~Dummy() {std::cout << this << ": destructed;" << std::endl;}
};

int main()
{
    std::packaged_task<void()>* p_task;
    {
        Dummy ScopedDummy;
        p_task = new std::packaged_task<void()>([ScopedDummy](){std::cout << "lambda call with: " << &ScopedDummy << std::endl;});
        std::cout << "p_task completed" << std::endl;
    }
    {
        std::future<void> future_result;
        {
            future_result = p_task->get_future();
            (*p_task)();
            delete p_task;
        }
        std::cout << "after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task" << std::endl;
    }
    std::cout << "p_task cleans up when future_result dies" << std::endl;
}

一个可能的输出是:

0x7fff9cf873fe: default constructed;
0x7fff9cf873ff: copy constructed;
0x1904b38: move constructed;
0x7fff9cf873ff: destructed;
0x7fff9cf873fe: destructed;
lambda call with: 0x1904b38
after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task
0x1904b38: destructed;
p_task cleans up when future_result dies

因此 lambda 中的对象的生命周期延长了future_result.

如果我们注释掉这一行future_result = p_task->get_future();,可能的输出是:

0x7fff57087896: default constructed;
0x7fff57087897: copy constructed;
0x197cb38: move constructed;
0x7fff57087897: destructed;
0x7fff57087896: destructed;
lambda call with: 0x197cb38
0x197cb38: destructed;
after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task
p_task cleans up when future_result dies

我一直想知道这里有什么机制起作用,是否std::future包含一些使关联对象保持活动状态的链接?

4

1 回答 1

7

查看gcc7.2.0 packaged_task sources,我们读到:

packaged_task(allocator_arg_t, const _Alloc &__a, _Fn &&__fn)
    : _M_state(__create_task_state<_Res(_ArgTypes...)>(std::forward<_Fn>(__fn), __a)){}

~packaged_task()
{
  if (static_cast<bool>(_M_state) && !_M_state.unique())
    _M_state->_M_break_promise(std::move(_M_state->_M_result));
}

其中_M_state是内部 packaged_task 共享状态的 shared_ptr。因此,事实证明gcc将可调用对象存储为 packaged_task共享状态的一部分,因此将可调用生命周期绑定到 packaged_task,future,shared_future 中最后死去的对象。

相比之下,clang不会,在打包的任务被销毁时销毁可调用对象(事实上,我的 clang 副本会将可调用对象存储为适当的成员)。

谁是对的?标准对存储任务的生命周期不是很清楚;从一方面来说,我们有

[[期货.任务]]

packaged_task 定义了一种用于包装函数或可调用对象的类型,以便函数或可调用对象的返回值在调用时存储在未来。

packaged_task(F&& f)[...]构造一个新的具有共享状态的 packaged_task 对象,并用 std::forward(f)初始化该对象的存储任务。

packaged_task(packaged_task&& rhs)[...]将存储的任务从 rhs 移动到 *this。

reset()[...]效果:好像 *this = packaged_task(std::move(f)),其中 f 是存储在 *this中的任务。

这表明 callable 归 packaged_task 所有,但我们也有

[[期货.状态]]

- 本小节中介绍的许多类都使用某种状态来传达结果。此共享状态由一些状态信息和一些(可能尚未评估)结果组成,结果可以是(可能是无效的)值或异常。[注意:本节中定义的期货、承诺和任务引用了这种共享状态。——尾注]

-[注意:结果可以是任何类型的对象,包括计算结果的函数,如异步 [...]]

[期货.任务.成员]

-packaged_task(F&& f);[...]调用 f 的副本应与调用 f[...] -~packaged_task(); 的行为相同 效果:放弃任何共享状态

建议可以将可调用对象存储在共享状态中,并且不应依赖于任何可调用的每个实例行为(这可能被解释为包括可调用生命周期结束的副作用;顺便说一句,这也意味着您的可调用对象不是严格有效的,因为它的行为与其副本不同);此外,没有提及 dtor 中存储的任务。

总而言之,我认为 clang 更一致地遵循了措辞,尽管似乎没有什么明确禁止 gcc 行为。也就是说,我同意这应该得到更好的记录,否则可能会导致令人惊讶的错误......

于 2017-11-10T09:36:43.323 回答