首页 > 解决方案 > 为什么 C++ std::function 按值而不是通用引用传递函子?

问题描述

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;
  }
};

}

我很好奇在这里以不同方式处理赋值和构造函数的基本原理是什么。谢谢!

标签: c++c++11templatesgenerics

解决方案


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

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

选项 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 种方法中有多个接收器参数时,不必要的复杂性就会爆发。

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

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

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

  • 这对于构造函数来说更糟糕,因为现在这个构造函数对于一个复制构造函数来说是一个可行的选择,它可能真的把事情搞砸了,因为这将更适合来自非常量对象的副本。

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

  • 如果你想接受任何不是 simple 的类型T,例如如果X是模板化的:template <class T> A(X<T>&& x)这不是一个转发引用,而是一个右值引用,你需要一个左值引用重载。

推荐阅读