std::future 如何影响关联 std::packaged_task 的生命周期?

How does std::future affects the lifetime of an associated std::packaged_task?

我有一个 std::packaged_task 包含一个通过复制捕获变量的 lambda。当这个 std::packaged_task 被删除时,我希望 lambda 中的变量被破坏,但我注意到如果我得到这个 std::packaged_task 的关联 std::futurefuture 对象延长了 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 是否包含一些 link 让关联对象保持活动状态?

看着 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 是 shared_ptr 到内部 packaged_task 共享状态。因此,事实证明 gcc 将可调用存储为 packaged_task 共享状态 的一部分,因此将可调用生命周期绑定到 whom packaged_task,未来,shared_future 最后死去。

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

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

[[futures.task]]

packaged_task defines a type for wrapping a function or callable object so that the return value of the function or callable object is stored in a future when it is invoked.

packaged_task(F&& f)[...]Constructs a new packaged_task object with a shared state and initializes the object’s stored task with std::forward(f).

packaged_task(packaged_task&& rhs)[...]Moves the stored task from rhs to *this.

reset()[...]Effects: As if *this = packaged_task(std::move(f)), where f is the task stored in *this.

这表明可调用对象属于 packaged_task,但我们也有

[[futures.state]]

-Many of the classes introduced in this subclause use some state to communicate results. This shared state consists of some state information and some (possibly not yet evaluated) result, which can be a (possibly void) value or an exception. [ Note: Futures, promises, and tasks defined in this clause reference such shared state. —endnote]

-[ Note: The result can be any kind of object including a function to compute that result, as by async [...]]

[futures.task.members]

-packaged_task(F&& f);[...]Invoking a copy of f shall behave the same as invoking f[...] -~packaged_task(); Effects: Abandons any shared state

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

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