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

std::thread taking lambda with ref arg fails to compile

我正在阅读 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<int>operator()

void operator() (int& result);

当您将 reference_wrapper<int> 传递给它时,将调用转换函数 (operator int &),生成可以绑定到 result.

的引用

您的通用 lambda 的 operator()

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

如果它被传递了一个 reference_wrapper 左值,它会将 T 推断为 reference_wrapper,然后无法在赋值时编译。 (分配给 reference_wrapper 会重置 "reference" 而不是影响值。)

但它甚至在此之前就失败了,因为标准要求您传递给 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);

通过 "INVOKE(...)" 函数族 std::async, std::bind, std::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++ 中最好避免使用它。

这里有一个函数可以解决你的问题。它需要一个函数对象,returns 一个函数对象,它将在将它传递给内部函数对象之前解压 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里面。

你的问题是 std::reference_wrapper<T>&& 传递给你的 lambda 导致它试图推导出 U 这样 U& = std::reference_wrapper<T>&& 和 none 存在。

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

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