【问题标题】:Why isn't operator[] overloaded for lvalues and rvalues?为什么不为左值和右值重载 operator[]?
【发布时间】:2015-06-01 09:39:34
【问题描述】:

标准 C++ 容器只为 vector<T>deque<T> 等容器提供一个版本的 operator[]。它返回一个T&(除了vector<bool>,我将忽略它),这是一个左值。这意味着在这样的代码中,

vector<BigObject> makeVector();       // factory function

auto copyOfObject = makeVector()[0];  // copy BigObject

copyOfObject 将被复制构造。鉴于makeVector() 返回一个右值vector,期望copyOfObject 被移动构造似乎是合理的。

如果此类容器的operator[] 被右值和左值对象重载,则右值容器的operator[] 可以返回右值引用,即右值:

template<typename T>
container {
public:
    T& operator[](int index) &;       // for lvalue objects
    T&& operator[](int index) &&;     // for rvalue objects
...
};

在这种情况下,copyOfObject 将被移动构造。

这种重载一般来说是个坏主意,有什么原因吗? C++14 中的标准容器没有这样做是有原因的吗?

【问题讨论】:

  • 我想这可能是不可取的,原因有两个,1) 将 container[0] 留空/移动会使重复访问同一元素变得棘手(除非说,container.at(0) 没有为右值重载),并且2)auto&amp; ref = container[0]not work everywhere
  • 请注意,我们在这里讨论的是右值容器。这意味着重复元素访问很困难,因为容器仅在语句的持续时间内存在。如果您通过将容器绑定到命名引用来延长其生命周期(如您的示例中所示),则该引用是一个左值,并且通过该引用进行的后续访问将调用左值运算符 [] 重载。
  • 嗯,一个明显的可能原因是它会破坏现有代码。给定vector&lt;int&gt; f(); void g(int &amp;);g(f()[0]) 将突然停止工作,或者,如果还有void g(const int &amp;);,则默默地转到另一个过载。
  • @T.C.:有效点,谢谢。对于为什么标准容器不会为左值和右值重载 operator[] 的问题,这是一个合理的答案,但是对于遗留代码不是问题的新容器呢?总体设计有问题吗?
  • 总体上我认为设计没有问题。毕竟,类成员访问使用非常相似的规则(E1.E2 是一个 xvalue,如果 E2 命名一个非静态数据成员,E1 是一个右值)。 std::experimental::optional 也使用了类似的设计,但我不记得 &amp;&amp; 版本是返回 T 还是 T&amp;&amp;

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


【解决方案1】:

我认为如果您移出其中一个元素,您将使容器处于无效状态,我认为完全需要允许该状态。其次,如果你需要它,你不能像这样调用新对象的移动构造函数:

T copyObj = std::move(makeVector()[0]);

更新:

在我看来,最重要的一点是,容器本质上就是容器,所以无论如何它们都不应该修改其中的元素。它们只是提供存储、迭代机制等。

【讨论】:

  • 需要移动才能使源对象处于有效状态,因此容器不会处于无效状态。确实,用户可以自己申请std::move,但如果这是不为左值和右值重载operator[] 的唯一原因,我认为这是一个非常弱的动机。
  • @KnowItAllWannabe,好的,我同意,它将处于有效但仍未指定的状态,并且您可以对这些元素(已从容器中移出)执行所有操作,只需执行以下操作不暗示任何先决条件。您能否举一个合理的算法示例,在移出一些元素后可以在容器上运行?
  • 当然:排序、分区、find_if 等。例如,您可以按容器大小对容器进行排序,即使其中一些容器已被移出。移动的对象不是邪恶的、有毒的或危险的,它们只是被移动了,对于许多类型,你知道它们处于什么状态。例如,moved-fromt shared_ptrs 为空。
  • 是的,我了解(对于很多类型,您知道它们处于什么状态),请在答案中查看我的更新,我想这是主要原因。
【解决方案2】:

将评论转化为答案:

这种方法本质上没有任何问题;类成员访问遵循类似的规则(E1.E2 是一个 xvalue,如果 E1 是一个右值并且 E2 命名一个非静态数据成员并且不是一个引用,请参阅 [expr.ref]/4.2)和元素容器内部在逻辑上类似于非静态数据成员。

std::vector 或其他标准容器执行此操作的一个重大问题是它可能会破坏一些遗留代码。考虑:

void foo(int &);
std::vector<int> bar();

foo(bar()[0]);

如果右值向量上的operator[] 返回一个 xvalue,则最后一行将停止编译。或者 - 可以说更糟 - 如果有 foo(const int &amp;) 重载,它将默默地开始调用该函数。

另外,在一个容器中返回一堆元素并且只使用一个元素已经相当低效了。有争议的是,执行此操作的代码可能不太关心速度,因此小的性能改进不值得引入潜在的破坏性更改。

【讨论】:

  • 你认为你的答案也适用于右值迭代器的省略吗?
  • @NirFriedman 右值迭代器?右值的begin()/end() 的特殊重载?我看不出它们有什么用处。
  • 这将是一种新类型,就像有一个迭代器和一个 const_iterator 一样,您将拥有 rvalue_iterator。它们将很有用,因为它们将消除使用单独的“移动风格”算法的必要性。也就是说,有std::copy来复制一个范围,有std::move来移动一个范围,还有copy_if,但没有move_if。这些移动风格的算法不应该存在,它应该由迭代器处理。
  • @NirFriedman 我们有std::move_iterator,尽管有争议的是迭代器不分青红皂白地呈现右值不一定是正确的方法(例如,使用copy_if 和带有它的谓词如果效率低下,按值进行参数是正确的,但是如果您将移动迭代器添加到混合中,一切都会爆炸)。
  • @NirFriedman using iter_t = std::conditional_t&lt;std::is_rvalue_reference&lt;decltype(vec)&gt;{}, std::move_iterator&lt;decltype(vec.begin()&gt;, decltype(vec.begin())&gt;; std::copy_if(iter_t{vec.begin()}, iter_t{vec.end()}, out, pred);?无论如何,从右值中获取迭代器已经足够危险了,我认为在向后兼容所需的范围之外支持它并不是一个好主意。
猜你喜欢
  • 1970-01-01
  • 2012-02-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多