【问题标题】:How should operator plus implemented in C++11运算符加号应该如何在 C++11 中实现
【发布时间】:2014-11-21 10:08:57
【问题描述】:

我有一个基础课,比如说MyVector<T>。我希望以下表达式尽可能懒惰(尽可能有效):

MyVector<int> x = a + b + c;

+ 的语义是连接向量,所以[1,2,3] + [2,3,4] 必须导致[1,2,3,2,3,4]。假设我有operator+=,我可以在任何需要的地方依赖它。我认为以下实现足以满足a+b 被评估的情况:

template<typename T>
MyVector<T> operator+(MyVector<T> const &lhs, MyVector<T> const &rhs)
{
  return MyVector<T>(lhs)+=rhs;
}

然后我将有一个右值(a+b 的结果)和一个左值(c)。所以我想我必须写这样的东西:

template<typename T>
MyVector<T> ??? operator+(MyVector<T> &&lhs, MyVector<T> const &rhs)
{
   ???;
}

这里应该返回什么类型以及如何组装?

【问题讨论】:

  • 只是我的两分钱:如果我在向量的上下文中阅读+,我正在考虑线性组合,例如(对于标量值)(1, 2, 3) + (4, 5, 6) = (5, 7, 9),我希望它适用于定义 + 运算符的每种类型 T。我会使用operator&lt;&lt; (MyVector&lt;T&gt;&amp;, MyVector&lt;T&gt;&amp;) 连接,这样就可以写成x = a &lt;&lt; b
  • 如果你想偷懒,你应该看看表达式模板。这使您可以累积所有连接,然后一次执行它们,这意味着您知道结果向量的最终大小。
  • @quantdev 即使移动构造和移动分配正确,它不会导致MyVector&lt;T&gt; 的额外实例吗?
  • @SebastianRedl 我不认为这个问题会走那么远。让我们暂时不考虑它们。 :-)
  • @JonathanWakely 更好:MyVector&lt;T&gt; res(lhs); res += rhs; return res; 更易读,可以使用 NRVO,如果失败仍然会移动。

标签: c++ c++11 operator-overloading


【解决方案1】:

你绝对应该返回一个对象,而不是一个引用(否则连接右值会返回一个悬空引用)。

如果您希望串联在所有情况下都是最佳的,您需要遵循std::string 的示例,它为右值和左值的每个组合提供了四个重载:

X operator+(const X& l, const X& r) { X x(l); x += r; return x;}
X operator+(const X& l, X&& r) { r.insert(begin(), l.begin(), l.end()); return r; }
X operator+(X&& l, const X& r) { l += r; return l; }
X operator+(X&& l, X&& r) { l += r; return l; }

当(至少)其中一个参数是右值时,这将始终重用其中一个参数中的现有内存。

您应该小心return lhs += rhs;,因为operator+= 的结果可能是一个左值引用,它不符合复制省略(RVO)或被移入返回值的条件。请使用lhs += rhs; return lhs;return std::move(lhs += rhs); 以确保您不会在return 语句中强制进行不必要的复制。

您可以进一步优化最后一个重载,通过查看哪个列表更长并附加到那个列表(例如,如果l 为空且r 非常大,l += r 将在每个元素可以返回时复制它r)

X operator+(X&& l, X&& r)
{
  if (l.size() > r.size())
  {
    l += r;
    return l;
  }
  r.insert(begin(), l.begin(), l.end());
  return r;
}

【讨论】:

  • 如果T 对移动很有效,我认为我们可以使用std::move(r.begin(),r.end(),std::back_inserter(l)std::move(l.begin(),l.end(),std::front_inserter(r)) 做得更好。我说的对吗?
  • 这取决于 ... back_insert_iteratorfront_insert_iterator 一次添加一个元素,这可能会导致额外的重新分配和复制元素(取决于您实现容器的方式)。我的答案中的版本 应该 允许容器在插入任何内容之前获取两个迭代器之间的距离并调整大小(同样,取决于您实现容器的方式)。如果您有 reserve() 成员,您可以在 std::move() 算法之前使用它进行预分配,那么它应该总是比我回答中的版本更好。
  • 但是你的类型不会因为所有这些功能而变得非常简单......我希望你有一个很好的理由不能只使用std::vector
【解决方案2】:

如果你的类(足够快)是可移动的,你可以给出一个单独的实现,它通过值获取第一个参数:

template<typename T>
MyVector<T> operator+(MyVector<T> lhs, MyVector<T> const &rhs)
{
  return std::move(lhs += rhs);
}

一个新的MyVector&lt;T&gt; 将通过在调用点复制或从调用者的参数移动而透明地为您创建。这种方法的缺点是复制/移动是在函数调用之外完成的,这可能会给异常和调试带来惊喜。

【讨论】:

  • 编译器需要复制返回值,不允许移动或删除。
  • 没错,这里需要明确的move,修复了答案。
【解决方案3】:

您只需要返回一个MyVector&lt;T&gt;。如果你的类有一个移动构造函数或移动赋值运算符,那么编译器会调用它,让你更有效地复制/分配对象。

至于程序集,这将取决于底层实现。 lhs += rhs 是一个合理的默认值,但您也可以选择通过引用传递参数,然后分配一个预分配长度为 lhs.size() + rhs.size() 的向量的结果。

【讨论】:

  • 好的。大会呢?我应该使用return lhs+=rhs;,还是其他?
猜你喜欢
  • 2021-02-08
  • 2020-07-09
  • 1970-01-01
  • 1970-01-01
  • 2020-08-17
  • 2016-03-29
  • 2014-12-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多