【问题标题】:Why C++ std::function pass the functor by value instead of universal reference?为什么 C++ std::function 按值而不是通用引用传递函子?
【发布时间】:2020-11-23 04:18:04
【问题描述】:

std::function 的构造函数看起来像这样(至少在 libc++ 中):

namespace std {

template<class _Rp, class ..._ArgTypes>
function {
  // ...
  base_func<_Rp(_ArgTypes...)> __func;

public:
  template<typename _Fp>
  function(_Fp __f) : __func(std::move(__f)) {}

  template<typename _Fp>
  function& operator=(_Fp&& __f) {
    function(std::forward<_Fp>(__f)).swap(*this);
    return *this;
  }
};

}

它提供了来自任意函子的构造函数和来自任意函子的赋值运算符。构造函数使用按值传递,但赋值运算符使用按通用引用传递。

我的问题是为什么 std::function 的构造函数不像赋值运算符那样通过通用(转发)引用传递?例如,它可以这样做:

namespace std {

template<class _Rp, class ..._ArgTypes>
function {
  // ...
  base_func<_Rp(_ArgTypes...)> __func;
public:
  template<typename _Fp>
  function(_Fp&& __f) : __func(std::forward<_Fp>(__f)) {}
  
  template<typename _Fp>
  function& operator=(_Fp&& __f) {
    function(std::forward<_Fp>(__f)).swap(*this);
    return *this;
  }
};

}

我很好奇这里区别对待赋值和构造函数的基本原理是什么。谢谢!

【问题讨论】:

  • 因为that's what the language specification requires。请注意,您的示例存在缺陷,因为您没有移动构造函数,因此唯一的选择是复制。如果你添加Func(Func&amp;&amp; f) { std::cout &lt;&lt; "move ctor is called" &lt;&lt; std::endl; },那么你会看到它被移动了。
  • 如果您查看任何接受可调用参数的标准库函数,您会发现它们都按值接受这些参数。
  • 您的最后一次编辑使问题无效。而且您也缺少默认构造函数。 :godbolt.org/z/3K7MoE不能再复制了!
  • @RaymondChen 感谢您的回答。经过多次编辑后,我意识到我的问题没有意义。我还意识到,如果正确完成,按值传递也可以避免复制。我已经修改了问题。

标签: c++ c++11 templates generics


【解决方案1】:

这就是所谓的“接收器参数”。 接收器参数是方法的参数,需要从调用者“获取”并存储在对象中(作为数据成员)。调用者通常不需要/使用调用后的对象。

接收器参数的最佳实践是按值传递它并从它移动到对象中。让我们看看为什么:

选项 1:通过引用传递

class X; // expensive to copy type with cheap move

struct A
{
     X stored_x_;

     A(const X& x) : x_{x} {}
//                   ^~~~~
//                   this is always a copy
};

在这种情况下,总会有至少 1 个无法删除的副本。

选项2:传值,然后移动

class X; // expensive to copy type with cheap move

struct A
{
     X stored_x_;

     A(X x) : x_{std::move(x)} {}
//            ^~~~~~~~~~~~~~~~
//            this is now a move
};

我们在A::x_的初始化中去掉了这个动作,但是在传递参数时我们仍然有一个副本,或者是吗?

如果调用者做了正确的事情,我们就不会。我们这里有两种情况:调用者仍然需要传递的对象的副本(这是非常不寻常且非惯用的)。在这种情况下,是的,将制作一个副本,但那是因为被调用者需要这样做,而不是因为我们的类 A 的设计存在缺陷。

调用者在传递对象后不需要它。在这种情况下,它会移动参数或更好地传递一个纯右值,并且由于 C++17 具有新的临时实现规则,因此该对象直接作为参数创建:

传递一个 xvalue

auto test()
{
    X x{};

    A a{std::move(x)}; // 2 moves (from arg to parameter and from parameter to `A::x_`)
};

传递prvalue

auto test()
{
    A a{X{}}; // just the move in the initialization of `A::x_`
}

选项 3:左值和右值引用重载

是的,这将达到相同水平的性能,但是当您只能编写和维护 1 个方法时,为什么还要有 2 个重载。

class X; // expensive to copy type with cheap move

struct A
{
     X stored_x_;

     A(const X& x) : x_{x} {}
     A(X&& x) : x_{std::move(x)} {}
};

当您在 1 个方法中有多个 sink 参数 时,不必要的复杂性就会爆发。

选项4:通过转发引用传递:

再次,可能。但它可能有一些微妙但相当严重的问题:

  • 如果您没有模板参数,则需要将其设为模板,这会增加复杂性并增加其他问题,例如现在该方法接受 any 类型。

  • 这对于构造函数来说更糟糕,因为现在这个构造函数是复制构造函数的一个可行选项,它确实会搞砸事情,因为这将更适合从非 const 对象复制。

另一个问题是它不能一直使用:

  • 如果你想接受不只是T 的任何类型,例如如果 X 是模板化的:template &lt;class T&gt; A(X&lt;T&gt;&amp;&amp; x) 这不是转发引用,而是右值引用,您需要左值引用重载。

【讨论】:

    猜你喜欢
    • 2015-03-24
    • 2020-08-03
    • 2011-02-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-11
    相关资源
    最近更新 更多