我正在用C编写一个小型数值分析库 . 我一直在尝试使用最新的C 11功能,包括移动语义 . 我理解以下帖子中的讨论和最佳答案:C++11 rvalues and move semantics confusion (return statement),但有一种情况我仍然试图解决这个问题 .
我有一个类,称之为 T
,它配备了重载运算符 . 我也有复制和移动构造函数 .
T (const T &) { /*initialization via copy*/; }
T (T &&) { /*initialization via move*/; }
我的客户端代码大量使用运算符,所以我试图确保复杂的算术表达式从移动语义中获得最大的好处 . 考虑以下:
T a, b, c, d, e;
T f = a + b * c - d / e;
没有移动语义,我的操作符每次都使用复制构造函数创建一个新的局部变量,所以总共有4个副本 . 我希望通过移动语义,我可以将其减少到2个副本加上一些动作 . 在括号中:
T f = a + (b * c) - (d / e);
(b * c)
和 (d / e)
中的每一个都必须以通常的方式创建临时副本,但是如果我可以利用其中一个临时值仅用移动来累积剩余的结果,那将是很好的 .
使用g编译器,我已经能够做到这一点,但我怀疑我的技术可能不安全,我想完全理解为什么 .
以下是加法运算符的示例实现:
T operator+ (T const& x) const
{
T result(*this);
// logic to perform addition here using result as the target
return std::move(result);
}
T operator+ (T&& x) const
{
// logic to perform addition here using x as the target
return std::move(x);
}
如果没有调用 std::move
,则只调用每个运算符的 const &
版本 . 但是当如上所述使用 std::move
时,使用每个运算符的 &&
版本执行后续算法(在最内层表达式之后) .
我知道RVO可以被抑制,但在计算成本非常高的现实问题上,似乎收益略微超过了RVO的缺乏 . 也就是说,当我包含 std::move
时,在数百万次计算中,我确实获得了非常小的加速 . 虽然说实话,但没有足够快 . 我真的只想完全理解这里的语义 .
是否有一位C大师愿意花时间以简单的方式解释我是否以及为何使用std :: move在这里是一件坏事?提前谢谢了 .
3 回答
您应该更喜欢将运算符重载为自由函数以获得完全类型对称(可以在左侧和右侧应用相同的转换) . 这使得你在问题中遗漏的内容更加明显 . 将您的操作员重新设置为您提供的免费功能:
但是你没有提供一个处理左侧是临时的版本:
并且当两个参数都是rvalues时,为了避免代码中出现歧义,您需要提供另一个重载:
常见的建议是将
+=
实现为修改当前对象的成员方法,然后将operator+
编写为修改接口中相应对象的转发器 .我真的没有想过这么多,但可能有一个替代方法使用
T
(没有r /左值参考),但我担心它不会减少你需要提供的重载次数,以使operator+
在所有情况下都有效 .以其他人所说的为基础:
T::operator+( T const & )
中对std::move
的调用是不必要的,可能会阻止RVO .最好提供委托给
T::operator+=( T const & )
的非成员operator+
.我还想补充一点,完美的转发可以用来减少所需的非成员
operator+
重载次数:对于一些运算符来说,这个“通用”版本就足够了,但由于加法通常是可交换的,我们可能想检测右手操作数何时是右值并修改它而不是移动/复制左手操作数 . 这需要一个版本作为左值的右侧操作数:
另一个是右手操作数,它们是右值:
最后,您可能也对Boris Kolpackov和Sumant Tambe以及Scott Meyers的response提出的技术感兴趣 .
我同意DavidRodríguez认为使用非成员
operator+
函数更好的设计,但我会把它放在一边,专注于你的问题 .写作时,你会发现性能下降,我感到很惊讶
代替
因为在前一种情况下,编译器应该能够使用RVO在内存中为函数的返回值构造
result
. 在后一种情况下,编译器需要将result
移动到函数的返回值中,因此会产生额外的移动成本 .一般来说,假设你有一个函数返回一个对象(即不是引用),这种事情的规则是:
如果你
std::move
应用std::move
. 这允许编译器执行RVO,这比副本或移动便宜 .如果要返回rvalue类型参数,请对其应用
std::move
. 这会将参数转换为右值,从而允许编译器从中移动 . 如果只返回参数,编译器必须执行返回值的副本 .如果您是通用引用(即推导类型的“
&&
”参数,可以是右值引用或左值引用),请对其应用std::forward
. 没有它,编译器必须执行返回值的副本 . 有了它,如果引用绑定到右值,编译器可以执行移动 .