Perfect Forwarding to async lambda

Alexander Kondratskiy picture Alexander Kondratskiy · Dec 11, 2012 · Viewed 7.8k times · Source

I have a function template, where I want to do perfect forwarding into a lambda that I run on another thread. Here is a minimal test case which you can directly compile:

#include <thread>
#include <future>
#include <utility>
#include <iostream>
#include <vector>

/**
 * Function template that does perfect forwarding to a lambda inside an
 * async call (or at least tries to). I want both instantiations of the
 * function to work (one for lvalue references T&, and rvalue reference T&&).
 * However, I cannot get the code to compile when calling it with an lvalue.
 * See main() below.
 */
template <typename T>
std::string accessValueAsync(T&& obj)
{

    std::future<std::string> fut =
        std::async(std::launch::async,
            [](T&& vec) mutable
            {
                return vec[0];
            },
            std::forward<T>(obj));

    return fut.get();
}

int main(int argc, char const *argv[])
{
    std::vector<std::string> lvalue{"Testing"};

    // calling with what I assume is an lvalue reference does NOT compile
    std::cout << accessValueAsync(lvalue) << std::endl;

    // calling with rvalue reference compiles
    std::cout << accessValueAsync(std::move(lvalue)) << std::endl;

    // I want both to compile.

    return 0;
}

For the non-compiling case, here is the last line of the error message which is intelligible:

main.cpp|13 col 29| note:   no known conversion for argument 1 from ‘std::vector<std::basic_string<char> >’ to ‘std::vector<std::basic_string<char> >&’

I have a feeling it may have something to do with how T&& is deduced, but I can't pinpoint the exact point of failure and fix it. Any suggestions?

Thank you!

EDIT: I am using gcc 4.7.0 just in case this could be a compiler issue (probably not)

Answer

jogojapan picture jogojapan · Dec 11, 2012

The way I understand it you cannot use a function through async that expects non-const lvalue references as arguments, because async will always make a copy of them internally (or move them inside) to ensure they exist and are valid throughout the running time of the thread created.

Specifically, the Standard says about async(launch policy, F&& f, Args&&... args):

(§30.6.8)

(2) Requires: F and each Ti in Args shall satisfy the MoveConstructible requirements. INVOKE(DECAY_COPY (std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) (20.8.2, 30.3.1.2) shall be a valid expression.

(3) Effects: [...] if policy & launch::async is non-zero — calls INVOKE(DECAY_COPY (std::forward<F>(f)),DECAY_COPY (std::forward<Args>(args))...) (20.8.2, 30.3.1.2) as if in a new thread of execution represented by a thread object with the calls to DECAY_COPY() being evaluated in the thread that called async. Any return value is stored as the result in the shared state. Any exception propagated from the execution of INVOKE (DECAY_COPY (std::forward(f)), DECAY_COPY (std::forward(args))...) is stored as the exceptional result in the shared state.
The thread object is stored in the shared state and affects the behavior of any asynchronous return objects that reference that state.

Unfortunately, this means you cannot even replace the reference with a std::reference_wrapper, because the latter isn't move-constructible. I suppose using a std::unique_ptr instead of the reference would work (implying, however, that your function arguments will always live on the heap).

(EDIT/CORRECTION)
I was working on a related problem when I realized that std::reference_wrapper actually enables a workaround, although I claimed the opposite above.

If you define a function that wraps lvalue references in a std::reference_wrapper, but leaves rvalue references unchanged, you can pass the T&& argument through this function before handing it over to std::async. I have called this special wrapper function wrap_lval below:

#include <thread>
#include <future>
#include <utility>
#include <iostream>
#include <vector>
#include <type_traits>

/* First the two definitions of wrap_lval (one for rvalue references,
   the other for lvalue references). */

template <typename T>
constexpr T&&
wrap_lval(typename std::remove_reference<T>::type &&obj) noexcept
{ return static_cast<T&&>(obj); }

template <typename T>
constexpr std::reference_wrapper<typename std::remove_reference<T>::type>
wrap_lval(typename std::remove_reference<T>::type &obj) noexcept
{ return std::ref(obj); }


/* The following is your code, except for one change. */
template <typename T>
std::string accessValueAsync(T&& obj)
{

  std::future<std::string> fut =
    std::async(std::launch::async,
           [](T&& vec) mutable
           {
             return vec[0];
           },
           wrap_lval<T>(std::forward<T>(obj)));   // <== Passing obj through wrap_lval

  return fut.get();
}

int main(int argc, char const *argv[])
{
  std::vector<std::string> lvalue{"Testing"};

  std::cout << accessValueAsync(lvalue) << std::endl;

  std::cout << accessValueAsync(std::move(lvalue)) << std::endl;

  return 0;
}

With this change, both calls to accessValueAsync compile and work. The first one, which uses an lvalue reference, automatically wraps it in a std::reference_wrapper. The latter is automatically converted back to an lvalue reference when std::async calls the lambda function.