【问题标题】:What's the difference between passing a function directly to std::async and using std::bind?将函数直接传递给 std::async 和使用 std::bind 有什么区别?
【发布时间】:2019-06-15 19:35:51
【问题描述】:

我最近开始为我正在开发的库添加异步支持,但遇到了一个小问题。我从这样的事情开始(稍后的完整上下文):

return executeRequest<int>(false, d, &callback, false);

那是在添加异步支持之前。我试图将其更改为:

return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);

但是编译失败。

MCVE:

#include <iostream>
#include <future>

int callback(const int& t) {
    std::cout << t << std::endl;   
    return t;
}
class RequestData {
private:
    int x;
public:
    int& getX() {
        return x;   
    }
};
class X {
    public:
        template <typename T>
        T executeRequest(bool method, RequestData& requestData,
                       std::function<T(const int&)> parser, bool write) {
            int ref = 42;
            std::cout << requestData.getX() << std::endl;
            return parser(ref);
        }
        int nonAsync() {
            // Compiles 
            RequestData d;
            return this->executeRequest<int>(false, d, &callback, false);    
        }
        std::future<int> getComments() {
            RequestData d;
            // Doesn't compile 
            return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);
        }
};

int main() {
    X x;
    auto fut = x.getComments();
    std::cout << "end: " << fut.get() << std::endl;
}

它失败了:

In file included from main.cpp:2:
In file included from /usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:38:
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/functional:1505:56: error: no type named 'type' in 'std::result_of<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>'
      typedef typename result_of<_Callable(_Args...)>::type result_type;
              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:1709:49: note: in instantiation of template class 'std::_Bind_simple<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>' requested here
          __state = __future_base::_S_make_async_state(std::__bind_simple(
                                                       ^
main.cpp:33:25: note: in instantiation of function template specialization 'std::async<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool), X *, bool, RequestData &, int (*)(const int &), bool>' requested here
            return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);
                        ^
In file included from main.cpp:2:
In file included from /usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:38:
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/functional:1525:50: error: no type named 'type' in 'std::result_of<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>'
        typename result_of<_Callable(_Args...)>::type
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
2 errors generated.

Live example。

两者之间唯一的实际区别(至少我可以看到)是我需要显式传递this,因为我正在引用一个成员函数

我玩了一下,发现如果我用const RequestData&amp; 替换它,它突然就被允许了。但它反而会在其他地方导致问题,因为 getter 不是 const。至少从我能找到的情况来看,我需要使它成为一个 const 函数,这对 getter 本身来说很好,但我也有一些 setter,这意味着我不能这样做。

无论如何,我想我可以试试std::bind。我将异步调用替换为:

auto func = std::bind(&X::executeRequest<int>, this, false, d, &callback, false);
return std::async(std::launch::async, func);

而且,出于某种原因,it worked。

在这里让我感到困惑的是,它两次都使用相同的参数(如果计算非异步变体,则全部三次),并且考虑到this 参数,给定我正在调用的函数是一个成员函数。

我深入挖掘,找到了一些替代解决方案(尽管参考了std::thread),它们使用了std::ref。我知道std::async 在后台运行std::thread,所以我挖出了the documentation:

线程函数的参数按值移动或复制。如果需要将引用参数传递给线程函数,则必须对其进行包装(例如,使用std::ref 或std::cref)。 (强调我的)

这是有道理的,并解释了它失败的原因。我假设std::async 也受此限制,并解释了它失败的原因。

然而,挖掘std::bind:

bind 的参数被复制或移动,并且永远不会通过引用传递,除非包装在std::ref 或std::cref 中。 (强调我的)

我不使用std::ref(或者如果我用const、std::cref替换),但至少如果我理解文档正确,这两个都应该无法编译。 example on cppreference.com 也可以在没有 std::cref 的情况下编译(在 Coliru 中使用 Clang 和 C++ 17 进行了测试)。

这是怎么回事?

如果重要的话,除了 coliru 环境,我最初在 Docker 中重现了这个问题,运行 Ubuntu 18.04 和 Clang 8.0.1(64 位)。在这两种情况下都针对 C++ 17 编译。

【问题讨论】:

  • 未来问题的旁注:虽然您的示例是可重现的,但应尽可能减少,例如,godbolt.org/z/_7gg2Q
  • @Holt 在保持可运行状态并尽可能接近我的实际代码的同时尽可能小。我决定坚持上课以防万一(尽管删除它并没有太多 IIRC)。而且我的实际代码仍然比这更大(并且更明智)。点虽然 - 谢谢

标签: c++ stdasync


【解决方案1】:

标准略有不同。对于std::bind:

要求:is_­constructible_­v&lt;FD, F&gt; 应为 true。 对于BoundArgs 中的每个Ti,is_­constructible_­v&lt;TDi, Ti&gt; 应为true。 INVOKE(fd, w1, w2, …, wN) ([func.require]) 应该是某些值 w1、w2、...、wN 的有效表达式,其中 N 的值是 sizeof...(bound_­args)。 调用包装器g 的 cv 限定符 cv 如下所述,既不是 volatile 也不是 const volatile。

返回:参数转发调用包装器g ([func.require])。 g(u1, u2, …, uM)的效果应该是

INVOKE(fd, std::forward<V1>(v1), std::forward<V2>(v2), …, std::forward<VN>(vN))

其中v1, ..., vN 具有特定类型。在您的情况下,重要的是与d 对应的存储变量的类型为std::decay_t&lt;RequestData&amp;&gt;,即RequestData。在这种情况下,您可以使用左值 RequestData 轻松调用 executeRequest&lt;int&gt;。

std::async 的要求要强得多:

要求:F 和 Args 中的每个 Ti 应满足 Cpp17MoveConstructible 要求,并且

INVOKE(decay-copy(std::forward<F>(f)),
   decay-copy(std::forward<Args>(args))...)     // see [func.require], [thread.thread.constr]

最大的区别是decay-copy。对于d,您将获得以下信息:

decay-copy(std::forward<RequestData&>(d))

这是对decay-copy函数的调用(仅限说明),其返回类型为std::decay_t&lt;RequestData&amp;&gt;,所以RequestData,这就是编译失败的原因。


请注意,如果您使用std::ref,则行为将是未定义的,因为d 的生命周期可能在调用executeRequest 之前结束。

【讨论】:

  • 我只是对区别感到有些困惑。是不是因为decay-copy 创建了一个副本,该副本的使用方式会阻止左值成为函数可接受的类型,从而阻止模板接受?
  • @Zoe 区别并不在于可行性。在后台,std::bind + std::async 的版本可能与只有std::async 的版本非常相似,但std::async 的要求更强,可能会在使用不需要const 的函数时避免意外引用作为参数。
【解决方案2】:

这里让我感到困惑的是,它两次使用相同的参数

但它不会两次转发它们。调用异步版本时,调用可调用对象as if by calling:

std::invoke(decay_copy(std::forward<Function>(f)), 
            decay_copy(std::forward<Args>(args))...);

争论变成了类似于临时的东西!出于这个原因,引用RequestData&amp; requestData 不能绑定到它的参数。一个 const 引用、一个右值引用或一个普通的值参数可以在这里工作(如在,绑定),但一个非 const 左值引用不能。

std::bind 的调用方式不同。它也存储副本,但 "the ordinary stored argument arg is passed to the invokable object as lvalue argument[sic]",具有从 bind 对象本身派生的参数的 cv 限定。由于std::bind 创建了一个非常量绑定对象,因此为可调用对象提供了一个requestData 的非常量左值。引用愉快地绑定到那个。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多