且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

完美转发到异步lambda

更新时间:2023-11-29 16:22:40

按照我的理解,您不能通过async使用期望非常量左值引用作为参数的函数,因为async总是会复制在内部(或将它们移入内部)以确保它们存在并在所创建线程的整个运行时间内有效.

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.

具体来说,该标准对async(launch policy, F&& f, Args&&... args)进行了说明:

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

(§30.6.8)

(§30.6.8)

(2)要求:F,并且Args中的每个Ti必须满足MoveConstructible要求. INVOKE(DECAY_COPY (std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...)(20.8.2,30.3.1.2)是有效的表达式.

(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)效果:[...]如果政策& launch :: async非零-调用INVOKE(DECAY_COPY (std::forward<F>(f)),DECAY_COPY (std::forward<Args>(args))...)(20.8.2,30.3.1.2),就好像在由线程对象表示的新执行线程中一样,调用async的线程中对DECAY_COPY()的调用进行了评估.任何返回值都将作为结果存储在共享状态中.从执行INVOKE(DECAY_COPY(std :: forward(f)),DECAY_COPY(std :: forward(args))...)传播的任何异常都作为异常结果存储在共享状态中.
线程对象以共享状态存储,并影响任何异步行为 返回引用该状态的对象.

(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.

不幸的是,这意味着您甚至不能将引用替换为std::reference_wrapper,因为后者不可移动构造.我想使用std::unique_ptr代替引用是可行的(但是,这意味着您的函数参数将始终存在于堆中).

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).

(编辑/更正)
当我意识到std::reference_wrapper实际上可以解决问题时,我正在研究一个相关的问题,尽管我在上面提出了相反的看法.

(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.

如果定义一个将左值引用包装在std::reference_wrapper中的函数,但保留右值引用不变,则可以在将该函数传递给std::async之前将T&&参数传递给该函数.我在下面调用了这个特殊的包装函数wrap_lval:

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;
}

进行此更改后,对accessValueAsync的两个调用均会编译并运行.第一个使用左值引用,将其自动包装在std::reference_wrapper中.当std::async调用lambda函数时,后者会自动转换回左值引用.

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.