std::thread 使用带有 ref arg 的 lambda 编译失败

Posted

技术标签:

【中文标题】std::thread 使用带有 ref arg 的 lambda 编译失败【英文标题】:std::thread taking lambda with ref arg fails to compile 【发布时间】:2015-03-25 02:50:48 【问题描述】:

我正在阅读C++ concurrency in action。第 2.4 章介绍了一种 parallell_accumulate 算法。

我尝试 - 作为一个学习实验 - 用通用 lambda 替换那里使用的函子。

我将编译错误归结为:

#include <thread>

template <typename T>
struct f 
    void operator() (T& result)  result = 1;
;

int main() 
    int x = 0;
    auto g = [](auto& result)  result = 1; ;

    std::thread(f<int>(), std::ref(x));  // COMPILES
    std::thread(g, std::ref(x));         // FAILS TO COMPILE

错误信息:

 In file included from /usr/include/c++/4.9/thread:39:0,
                 from foo.cpp:1:
/usr/include/c++/4.9/functional: In instantiation of ‘struct std::_Bind_simple<main()::<lambda(auto:1&)>(std::reference_wrapper<int>)>’:
/usr/include/c++/4.9/thread:140:47:   required from ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = main()::<lambda(auto:1&)>&; _Args = std::reference_wrapper<int>]’
foo.cpp:13:31:   required from here
/usr/include/c++/4.9/functional:1665:61: error: no type named ‘type’ in ‘class std::result_of<main()::<lambda(auto:1&)>(std::reference_wrapper<int>)>’
       typedef typename result_of<_Callable(_Args...)>::type result_type;
                                                             ^
/usr/include/c++/4.9/functional:1695:9: error: no type named ‘type’ in ‘class std::result_of<main()::<lambda(auto:1&)>(std::reference_wrapper<int>)>’
         _M_invoke(_Index_tuple<_Indices...>)
         ^

我的编译器版本

$ g++ --version
g++ (Ubuntu 4.9.1-16ubuntu6) 4.9.1

为什么 lambda 编译失败,而仿函数编译失败?

编辑:如何使用通用 lambda 实现仿函数正在执行的操作(分配给 ref)?

【问题讨论】:

f 并不完全等同于通用 lambda。有一个模板operator()() 会更精确(它会产生同样的错误)。 std::bind 指定在将reference_wrapper 参数传递给目标函数之前对其进行解包,因此std::thread(std::bind(g, std::ref(x))) works as you intend。 【参考方案1】:

模板参数推导不查看转换的同一主题的另一个变体。

f&lt;int&gt;operator()

void operator() (int& result);

当您将 reference_wrapper&lt;int&gt; 传递给它时,转换函数 (operator int &amp;) 会被调用,从而产生一个可以绑定到 result 的引用。

您的通用 lambda 的 operator()

template<class T> void operator() (T& result) const;

如果它传递了一个reference_wrapper 左值,它会将T 推断为reference_wrapper,然后无法编译分配。 (分配给reference_wrapper 会重新设置“引用”而不是影响值。)

但它甚至在此之前就失败了,因为标准要求您传递给std::thread 的内容必须可以使用纯右值调用 - 并且非常量左值引用不会绑定到纯右值。这是您看到的错误 - result_of 不包含 type 因为您的仿函数不可调用参数类型。如果你尝试做g(std::ref(x));,clang produces 一个相当明显的错误:

main.cpp:16:5: error: no matching function for call to object of type '(lambda at main.cpp:11:14)'
    g(std::ref(x));
    ^
main.cpp:11:14: note: candidate function [with $auto-0-0 = std::__1::reference_wrapper<int>] not viable: expects an l-value for 1st argument
    auto g = [](auto& result)  result = 1; ;         
    ^

您可能应该考虑仅通过引用捕获相关的本地:

auto g = [&x]()  x = 1; ;

或者,如果出于某种原因,您必须使用通用 lambda,那么您可以按值(或通过 const 引用)获取 reference_wrapper,然后使用 get() 解包:

 auto g = [](auto result)  result.get() = 1; ;

或者添加一个std::bind,它将解开reference_wrappers,让模板参数推导做正确的事情(帽子提示@Casey):

 std::thread(std::bind(g, std::ref(x)));

或者也许放弃这个reference_wrapper废话并编写你的lambda来取一个非拥有指针:

auto g = [](auto* result)  *result = 1; ;
std::thread(g, &x);

【讨论】:

哇!有时clang很棒。这就是为什么您应该使用多个编译器检查可疑代码 sn-ps 的原因。 @Drop 好吧,公平地说,当你把它简化为一个普通的函数调用 g(std::ref(x)); 时,这真是太棒了;在 OP 中的原始代码上,它并不是真的很棒。 我爱你@T.C. -- 只接受了 5 个 *** 链接来找到我的答案。 (std::thread(g, &amp;x);)【参考方案2】:

通过“INVOKE(...)”函数系列std::asyncstd::bindstd::thread::thread 传递参数涉及各种问题。如果你想使用一个重载的函数名,或者传递一个左值引用,或者天堂禁止通过引用传递一个右值,你将很难。您将来到这里,我们中的一个学习过相关咒语的人会将其传递给您。希望下次出现时您会记住它。

我认为自 C++14 以来的最佳实践是通过自己处理参数并始终为 INVOKE 函数提供一个封装实际目标函数所需参数的零参数仿函数来完全避免参数传递怪异。自己动手可以让您准确地获得您想要的语义,而无需了解每个怪癖和解决方法以及 INVOKE 系列函数接口中的细微差别。 C++14 通用 lambda 捕获使得封装任何类型的函数和参数集变得非常简单。

在您的情况下,这种方法会导致:

#include <thread>

template <typename T>
struct f 
    void operator() (T& result)  result = 1;
;

int main() 
    int x = 0;
    auto g = [](auto& result)  result = 1; ;

    std::thread([&] return f<int>(x); );
    std::thread([&] return g(x); );

完全符合预期并且更具可读性。

std::reference_wrapper 在 TR1 时代很棒,当时我们需要它通过 std::bind 传递引用,但它的辉煌时代已经过去,我认为在现代 C++ 中最好避免它。

【讨论】:

“更具可读性”可能更像是一种观点陈述而不是事实。鉴于自 C++11 以来我一直在避免 std::bindstd::reference_wrapper 就像瘟疫一样,我对 lambdas 的偏好使我对“可读”的看法产生了偏见。【参考方案3】:

这是解决您的问题的功能。它接受一个函数对象,并返回一个函数对象,该函数对象将在将std::reference_wrappers 解包之前将其传递给内部函数对象。

#include <utility>
#include <functional>

template<class T>
T&& unref( T&& t )return std::forward<T>(t);
template<class T>
T& unref( std::reference_wrapper<T> r ) return r.get(); 

template<class F>
auto launder_refs( F&& f ) 
  return [f = std::forward<F>(f)](auto&&... args)
    return f( unref( std::forward<decltype(args)>(args) )... );
  ;


//

  auto g = launder_refs([](auto& result)  result = 2; );

live example -- 现在g 的行为就像你原来的g 一样,除了在传递std::reference_wrappers 时,它会将它们转换为引用,然后再将它们传递给内部的result

您的问题是传递给您的 lambda 的 std::reference_wrapper&lt;T&gt;&amp;&amp; 会导致它尝试推断出 U 使得 U&amp; = std::reference_wrapper&lt;T&gt;&amp;&amp;,但不存在。

简而言之,这是模板函数中类型推导的一个限制(它不考虑转换),因为在同一步骤中混合转换和模板类型推导会使每个人都发疯。

上面的代码从底层的 lambda 闭包(或函数对象)中隐藏了 std::reference_wrappers。这样做的开销也很小。

【讨论】:

以上是关于std::thread 使用带有 ref arg 的 lambda 编译失败的主要内容,如果未能解决你的问题,请参考以下文章

std::thread创建线程,使用std::ref()传递类对象参数

std::thread创建线程,使用std::ref()传递类对象参数

在带有标志选项 -m32 的 gcc-8.2.2 上找不到 std::thread。我正在使用 mingw

带有 std::thread 的 MVSE12 中的错误 C2248

带有 winsock 和 std::thread 的 C++ 多线程服务器

带有类参数的 std::thread 初始化导致类对象被多次复制