【问题标题】:What is the rationale behind std::bind and std::thread always copying arguments?std::bind 和 std::thread 总是复制参数的原因是什么?
【发布时间】:2012-05-17 05:03:44
【问题描述】:

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

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

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

【问题讨论】:

  • 如果你绑定一个本地的引用,你会遇到麻烦。
  • 如果被调用函数不存在超出本地范围,我不会。 C++ 中有很多东西可以让你大吃一惊。我的问题更多的是这种行为似乎不太直观。
  • 在某些时候,我的答案包含“并且本地超出范围”,但显然它在编辑后被删除:S

标签: c++ stl c++11


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

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

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

    哎呀。

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

    【讨论】:

    • 有道理。投票赞成!如果允许我接受两个答案,我会接受。谢谢。
    【解决方案3】:

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

    【讨论】:

    • 但是C++的原则不就是程序员知道自己最想要什么吗?在悬空引用的情况下,程序员应该小心,如果它愿意,它的函数的参数将是按值的,并且当我们的函数的参数接受引用时,我希望事物能够完美地作为参考转发,以应对任何风险。只是一个想法。
    • 没有。原则是程序员不会为他不使用的功能支付性能费用。 “知道你在做什么”这件事只是为了达到这个目的,而不是目的本身。
    • C++ 有很多原则:-​​)。最基本的之一是它默认使用值语义。您必须明确请求引用或指针才能执行其他操作。 (当然,标准库并不总是遵循这个规则——迭代器和谓词总是按值,但其他的东西往往会有所不同。)
    【解决方案4】:

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

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

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

    【讨论】:

    • std::thread 的情况是否相同?在线程启动函数的情况下,它不太可能被写成 lambda。并且强制使用 lambda 的闭包通过引用传递似乎很奇怪? Lambdas 似乎并没有解决这里引起的所有问题。我认为?
    【解决方案5】:

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

    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();
    

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

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

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

    【讨论】:

    • 这是一个非常有趣的想法,您能指出我们的实现,以便我们进行探索吗?此外,生成的仿函数在逻辑上只能调用一次吗?如果我们复制生成的函子会发生什么?
    • @authchir Code here (像emplace 这样的东西来自here)。你是正确的,只调用一次仿函数是正确的(进一步调用一个空的pointer_type),此外调用包装器是只移动的。这也适用于等效的 std::bind 用法。
    • 另外unit test 显示了典型用法。
    猜你喜欢
    • 2016-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-13
    • 1970-01-01
    • 2015-06-18
    • 2020-01-07
    相关资源
    最近更新 更多