c++ - 为什么 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;
}
};
}
我很好奇在这里以不同方式处理赋值和构造函数的基本原理是什么。谢谢!
解决方案
这就是所谓的“接收器参数”。接收器参数是需要从调用者“获取”并存储在对象中(作为数据成员)的方法的参数。调用者通常不需要/使用调用后的对象。
接收器参数的最佳实践是按值传递它并从它移动到对象中。让我们看看为什么:
选项 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)
这不是一个转发引用,而是一个右值引用,你需要一个左值引用重载。
推荐阅读
- python - 为什么我的代码不会为输入到 tkinter 文本框中的数据返回一个值
- algorithm - 我将如何编写一行代码来查看路径的顶点是否已被访问?
- blazor - 我如何知道何时需要在 Blazor 中调用 StateHasChanged()?
- reactjs - 为什么在使用空数组作为第二个参数的 useEffect 中声明时,我的 websocket 侦听器只触发一次?
- c - 简单 C 程序中的结构成员运算符错误
- angular - Ionic 4 - 屏幕方向(纵向)不起作用,NullInjectorError
- java - 模拟函数在指定返回后返回 null
- elixir - Elixir - 进行 DNS 查询(使用 dig 命令)
- vue.js - 在 Electron 模式构建中为 Quasar 设置 API URL
- c - 为什么这个浮点数组没有正确打印?