c++c++11stdasync

Is it still valid when the variable passed to `std::async` is out of scope?


Is the code snippet below legal? What worries me is when factorial is invoked the fut_num may be already out of scope.

#include <future>
#include <vector>
#include <iostream>

//int factorial(std::future<int> fut) //works, because there is a move constructor
int factorial(std::future<int>&& fut)
{
    int res = 1;
    int num = fut.get();

    for(int i=num; i>1; i--)
    {
        res *= i;
    }

    return res;
}

int main()
{
    std::promise<int> prs;
    std::vector<std::future<int>> vec;

    {
        std::future<int> fut_num{prs.get_future()};
        vec.push_back(std::async(std::launch::async, factorial, std::move(fut_num)));
    }  //`fut_num` is out of range now.
    
    prs.set_value(5);

    for(auto& fut: vec)
    {
        std::cout << fut.get() << std::endl;
    }
}

And the same question about similar code snippet:

#include <future>
#include <vector>
#include <iostream>

//int factorial(std::future<int> fut) //works, because there is a move constructor
int factorial(std::future<int>& fut)
{
    int res = 1;
    int num = fut.get();

    for(int i=num; i>1; i--)
    {
        res *= i;
    }

    return res;
}

int main()
{
    std::promise<int> prs;
    std::vector<std::future<int>> vec;

    {
        std::future<int> fut_num{prs.get_future()};
        vec.push_back(std::async(std::launch::async, factorial, std::ref(fut_num)));
    }  //`fut_num` is out of range now.
    
    prs.set_value(5);

    for(auto& fut: vec)
    {
        std::cout << fut.get() << std::endl;
    }
}

My two cents about these code snippets:

1.The former code snippet is legal, since std::async copies std::move(fut_num)(i.e. std::move(fut_num) is passed by value to std::async). So there is a local fut_num when fcatorical is called.

2.The latter one is illegal, since fut_num is passed as a reference to std::async. When fut_num is out of scope, it's illegal to call functions which uses fut_num.


Solution

  • The first one is fine, the second one is not.


    std::async with std::launch::async uses the same procedure of invoking the thread function as the constructor of std::thread does. Both effectively execute

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

    in the new thread, but with the auto(...) constructions executing in the context of the calling thread.

    Here F/Args... are the (other) template parameters with corresponding forwarding-reference function parameters F&& f/Args&&... args and auto has the new C++23 meaning which creates a prvalue/temporary of the decayed argument's type from the argument.

    See [futures.async]/4.1.

    This means there will be unnamed copies constructed for all arguments which live in the new thread until after the thread function invocation returns.

    So with std::move(fut_num), there will actually be another std::future<int> object which will be move-constructed from fut_num and live until the thread ends execution. factorial will be passed a reference to this unnamed object.


    With std::ref(fut_num) you are explicitly by-passing this mechanism protecting you from passing references to the objects in the constructing thread.

    The constructor will still make a decayed copy of type std::reference_wrapper<std::future<int>> from the argument, but that object will just contain a reference referring to fut_num in the main thread.

    std::invoke will then unwrap the std::reference_wrapper before passing to factorial and the fut argument will refer to fut_num in the main thread, which is then destroyed without any synchronization, causing undefined behavior as it is also accessed in the factorial function.


    It either case it doesn't matter whether factorial takes the argument by-reference or by-value. Nothing about the above reasoning changes.