3

Is it legal to use an rvalue reference of std::future as a parameter? Is there any potential problem that I should be aware of, since the std::future::get() is not marked as const.

If I miss something, please let me know.

Here is the full 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::future<int> fut_num{prs.get_future()};
    std::vector<std::future<int>> vec;

    vec.push_back(std::async(std::launch::async, factorial, std::move(fut_num)));
    

    prs.set_value(5);

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

I know if I pass a lvalue reference, things would be much easier. But I still conscious about when the function use a rvalue reference of std::future as a parameter.

UPDATED:

In general, rvalue reference is bound to temporary object. And non-const method could be invoked by the object which is pointed by right references? I am afraid it's illegal because non-const method may modify the temporary object.

John
  • 2,963
  • 11
  • 33

1 Answers1

1

Yes, it is legal, but I think accepting by value in factorial would be much easier.

This is safe because, as with std::thread, you are not really passing a reference back to main::fut_num. So main could exit and there would be no dangling references.

Instead, std::async(std::thread) move-constructs its own std::future tmp variable from std::move(fut_num) ( stored somewhere safe, effectively in the scope of the new thread). After that, it calls factorial(std::move(tmp).

As I said, I do not see much value from this, it saves one extra move and you are the proof the code is not more readable. Any moves will pale in comparison to the overhead of launching a thread (or even getting a job from thread pool).

Note that passing lvalue is either dangerous when you use std::ref or it copy constructs tmp which might be wasteful.

Quimby
  • 17,735
  • 4
  • 35
  • 55
  • "std::async(std::thread) move-constructs its own std::future tmp variable from std::move(fut_num) ( stored somewhere safe, effectively in the scope of the new thread). " Should not it be achieved by **perfect forwarding**? – John Jun 08 '22 at 09:08
  • " you are not really passing a reference back to main::fut_num. So main could exit and there would be no dangling references." Sorry, I can't catch up. Could you please explain that in simple words? – John Jun 08 '22 at 09:18
  • @John Yes, of course, there is perfect forwarding :) I was talking about your case in particular.... Any arguments passed to `std::async` (or `std::thread` for that matter) are perfect-forwarded into some variables local to the new thread. Thus for `factorial(int&& ref)`, the parameter points to this local storage and not to the `main::fut_num` variable even if you call it like `int x=5; std::async(factorial, x)`. So if `x` goes out of scope, the async call is unaffected. See e.g. https://stackoverflow.com/a/34078246/7691729 – Quimby Jun 08 '22 at 10:15
  • If I understand you correctly, ***1***. why "the `async` call is unaffected even if `x` goes out of scope" is because `x` is passed by value. If it's passed as `std::ref(x)`, "the` async` call would go wrong if `x` goes out of the scope. Am I right? ***2***, this [code snippet](https://godbolt.org/z/bhv5Wcsnv) is legal(the comment indicates the modification). Am I right? – John Jun 08 '22 at 12:02
  • 1. Yes, `x` is perfect-forwarded to `x_temp` and factorial is called with `x_temp`, not `x`. So if `factorial` accept a reference, its a reference to safe `x_temp` that will always outlive the thread, `x` can go out of scope. Yes, exactly, `std::ref` is dangerous since factorial will receive a reference to `x`. 2. Yes, had you used `std::ref` you would have UB, likely segfault. – Quimby Jun 08 '22 at 12:39
  • "Note that passing lvalue is either dangerous when you use std::ref or ***it copy constructs tmp which might be wasteful***." What do you mean by it copy constructs tmp which might be wasteful? Could you please explain that in more detail for me? – John Jun 08 '22 at 12:45
  • If you have `std::string long;` pass it to `std::thread(foo,long);` with `void foo(const std::string& str)`, a copy will be made because thread will create its own `long_tmp` by perferfect-forwarding `long` expression. That will call a copy constructor. I recommend that you read [`std::thread`](https://en.cppreference.com/w/cpp/thread/thread/thread). The 3) param section very precisely describes how the passed values end up in `foo`. – Quimby Jun 08 '22 at 12:58