在 std::accumulate 中使用 std::move

Use of std::move in std::accumulate

在我的 Fedora 34 环境 (g++) 中,std::accumulate 定义为:

template<typename ITER, typename T>
constexpr inline T accumulate(ITER first, ITER last, T init)
{
  for (; first != last; ++first)
      init = std::move(init) + *first; // why move ?

  return init;
}

如果表达式 init + *first 已经是一个右值,那么 std::move 的目的是什么?

init + *first 的值类别无关紧要。

init in init + *first 是一个左值。

因此,如果 init + *first 调用一个 operator+ 重载参数,它将导致该参数的复制构造

但是在 init + *first 之后不再需要 init 的值,因此将其移至参数中是有意义的。

类似地,通过右值引用获取其第一个参数的 operator+ 重载可用于允许通过操作修改参数。

这就是 std::move 在这里取得的成就。

自 C++20 起,标准指定了此行为。

std::move(init) + *first 有时可以生成比 init + *first 更高效的代码,因为它允许 init 被覆盖。但是,由于(如您所观察到的)+ 的结果通常是右值,因此无需在第二个 std::move.

中包装整个表达式

例如,如果您正在累积 std::string,那么 std::move(init) + *first 可能能够将 *first 附加到保留但尚未使用的 space在 init 的缓冲区中,而不是必须分配一个新的缓冲区,其长度是 init*first.

的长度之和

我刚刚被提醒,当我昨天写了一个大数加法的例子作为教训时,为什么这样效率更高。

二进制 + 运算符创建并 returns 一个临时对象。对于适合机器寄存器的操作数,这可能是零成本的(最多将之前目标寄存器中的内容溢出到堆栈中),但有时需要创建大型数据结构。这似乎是可能必须实现该用例的模板代码。

但是,如果可以破坏左操作数,+ 可以实现为 +=,覆盖操作数并优化新副本的创建。

这种编码风格让我觉得有点奇怪——正如我提到的,+= 具有程序员在这里似乎想要的语义,所以我不确定他们为什么不使用它。

它与 operator + 的 return 值无关,但与它的参数有关。

据我所知,std::accumulate 的有效实现可能是

template<typename ITER, typename T, typename OP = plus>
constexpr inline T accumulate(ITER first, ITER last, T init, OP op = {})
{
  for (; first != last; ++first)
      init = op(std::move(init), *first); // move the argument that is being overwritten

  return init;
}

根据 operator += 指定 accumulate 将是一个更大的突破性更改。为了对称,您想要更改 BinaryOperation 重载,这将破坏所有现有用途,就像定义 + 但未定义 += 的任何类型一样。