12

众所周知,std::bind 和 std::thread 的默认行为是复制(或移动)传递给它的参数,并且要使用引用语义,我们将不得不使用引用包装器。

  1. 有谁知道为什么这是一个很好的默认行为?特别是。在具有右值引用和完美转发的 C++11 中,在我看来,仅完美转发参数更有意义。

  2. std::make_shared 虽然并不总是复制/移动,但只是完美地转发提供的参数。为什么这里有两种看似不同的转发参数行为?(总是复制/移动的 std::thread 和 std::bind 与不复制/移动的 std::make_shared )

4

5 回答 5

15

make_shared转发到现在正在调用的构造函数。如果构造函数使用引用调用语义,它将获取引用;如果它确实按值调用,它将制作一个副本。无论哪种方式,这里都没有问题。

bind创建对函数的延迟调用,该函数在未来某些未知点被调用,此时本地上下文可能已经消失。如果bind使用完美转发,您将不​​得不复制通常通过引用发送且在实际调用时不知道处于活动状态的参数,将它们存储在某处,并管理该存储。使用当前的语义bind可以为您完成。

于 2012-05-08T17:44:16.567 回答
7

对于std::bindstd::thread,对给定参数的函数调用从调用站点延迟。在这两种情况下,函数何时被调用都是未知的。

在这种情况下直接转发参数需要存储引用。这可能意味着存储对堆栈对象的引用。实际执行调用时可能不存在。

哎呀。

Lambda 可以做到这一点,因为您可以根据每次捕获来决定是要通过引用还是值来捕获。使用std::ref,您可以通过引用绑定参数。

于 2012-05-08T18:07:10.637 回答
2

最可能的原因很简单,C++ 在默认情况下几乎在所有地方都使用值语义。并且使用引用很容易产生与被引用对象的生命周期有关的问题。

于 2012-05-08T17:20:28.850 回答
1

实际上,我确实编写了一个小实用程序来创建延迟调用函子(有点类似std::bind,但没有嵌套的绑定表达式/占位符功能)。我的主要动机是这个我发现违反直觉的案例:

using pointer_type = std::unique_ptr<int>;
pointer_type source();
void sink(pointer_type p);

pointer_type p = source();

// Either not valid now or later when calling bound()
// auto bound = std::bind(sink, std::move(p));
auto bound = std::bind(
    [](pointer_type& p) { sink(std::move(p)); }
    , std::move(p) );
bound();

该适配器(将其左值 ref 参数移动到sink)的原因是调用包装器 return bystd::bind总是将绑定的参数作为左值转发。在 C++03 中这不是问题,boost::bind因为左值要么绑定到底层 Callable 对象的引用参数,要么通过副本绑定到值参数。在这里不起作用,因为pointer_type只能移动。

我得到的见解是,确实有两件事需要考虑:绑定的参数应该如何存储,以及它们应该如何恢复(即传递给 Callable 对象)。授予您的控制std::bind如下:参数以浅层(通过使用std::ref)或常规方式(使用std::decay完美前向)存储;它们总是被恢复为左值(从拥有的调用包装器继承的 cv 限定符)。除了你可以像我刚刚做的那样用一个小的现场适配器 lambda 表达式绕过后者。

它可以说是大量的控制和大量的表达,而学习相对较少。相比之下,我的实用程序具有如下语义bind(f, p)(衰减和存储副本,恢复为左值),bind(f, ref(p))(浅层存储,恢复为左值),bind(f, std::move(p))(从移动中衰减和存储,恢复为右值),bind(f, emplace(p))(从移动中衰减和存储,恢复为左值) . 这感觉就像学习 EDSL。

于 2012-05-10T01:17:53.563 回答
1

std::bind 创建一个可调用的,它与 的调用站点分离std::bind,因此默认情况下按值捕获所有参数是很有意义的。

一般用例与将函数指针传递给函数而不知道它可能在哪里结束是相同的。

Lambda 为程序员提供了更大的灵活性来决定 lambda 是否会超出捕获参数的范围。

于 2012-05-08T17:21:17.173 回答