Search code examples
c++lambdastdasyncstd-future

Does std::future keep std::async lambda alive after execution?


For this example:

#include <vector>
#include <future>
#include <memory>
struct S {
  std::string mName = "";
};

std::vector<std::future<void>> futures;
int main(){
  {
    auto s = std::make_shared<S>();
    futures.emplace_back(std::async([s2=s]{}));
  }
  // will s2 be alive here assuming the async execution is done?
}

will the stored future keep the lambda alive or will that lambda and its by-value captures get destroyed after the async execution is finished? ChatGPT says 'yes' it will keep lambda alive but there is no reference.


Solution

  • The declaration of std::async that you are using is according to [futures.async]:

    template<class F, class... Args>
      [[nodiscard]] future<invoke_result_t<decay_t<F>, decay_t<Args>...>>
        async(F&& f, Args&&... args);
    

    First of all, the original lambda object passed for f is a temporary object that will be destroyed at the end of the full-expression

    futures.emplace_back(std::async([s2=s]{}))
    

    and its capture s2 with it. However, std::async will have moved from the lambda into the actual object that will be invoked. This copy is constructed as if by auto(std::forward<F>(f)) (see below). Your question is probably when this object will be destroyed, taking the last reference to the S object with it.

    When you don't give any launch policy argument to std::async it defaults to std::launch::async | std::launch::deferred, which means that the implementation can choose either of the two policies. See [futures.async]/3.

    If the implementation chooses std::launch::async, then it shall behave as if starting a thread that executes ([futures.async]/3.1):

    invoke(auto(std​::​forward<F>(f)), auto(std​::​forward<Args>(args))...)
    

    In this case the copy of the callable is a temporary object in this expression. It will be destroyed in the thread invoking the callable once it returns.

    If the implementation chooses std::launch::deferred, then it shall store a copy g of f initialized by auto(std​::​forward<F>(f)) in the shared state of the std::future, and similarly copies xyz... of args... initialized by auto(std::forward<Args>(args)).... When the deferred function is invoked, it shall execute

    invoke(std​::​move(g), std​::​move(xyz)...)
    

    See [futures.async]/3.2.

    Here the destruction of the copy of f is not part of the expression or the deferred invocation of the function.

    [futures.async]/3.2 doesn't specify further when these objects are going to be destroyed.

    I think, given that [futures.async]/3.2 is clearly stating when and how the copy of f is stored in the shared state and does neither say that it is replaced or released, the implementation shouldn't destroy it just because it is not needed any more after the deferred invocation finishes.

    std::future::wait also doesn't specify that it releases shared state of the future, so it itself also shouldn't destroy g.

    On the other hand std::future::get is specified to release all shared state and so should destroy g.

    Moving the std::future into futures will also move the state of g, making another copy of the lambda, but moving the ownership of the S object by s2 again to the new copy.

    So, for your specific example, I think it is unspecified whether or not the S object will still be alive at the commented line. It depends on which policy the implementation chooses: If it chooses launch::async, then it will be destroyed, or rather its destruction will happen in the other thread unsynchronized with reaching the comment line. If it chooses launch::deferred, then it will not be destroyed (because you didn't call any member function on std::future that will release its state).