18

The standard C++ containers offer only one version of operator[] for containers like vector<T> and deque<T>. It returns a T& (other than for vector<bool>, which I'm going to ignore), which is an lvalue. That means that in code like this,

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

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

copyOfObject will be copy constructed. Given that makeVector() returns an rvalue vector, it seems reasonable to expect copyOfObject to be move constructed.

If operator[] for such containers was overloaded for rvalue and lvalue objects, then operator[] for rvalue containers could return an rvalue reference, i.e., an rvalue:

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

In that case, copyOfObject would be move constructed.

Is there a reason this kind of overloading would be a bad idea in general? Is there a reason why it's not done for the standard containers in C++14?

KnowItAllWannabe
  • 12,972
  • 8
  • 50
  • 91
  • I guess it could be undesirable for two reasons, 1) leaving `container[0]` empty/moved makes repeat access of the same element tricky(unless say, `container.at(0)` is not overloaded for rvalue), and 2) `auto& ref = container[0]` will [not work everywhere](http://coliru.stacked-crooked.com/a/73d3cea0bca5d736) – melak47 Mar 27 '15 at 22:53
  • Note that we're talking about rvalue containers here. That means that repeat element access is difficult, because the container exists for only the duration of the statement. If you extend its lifetime by binding the container to a named reference (as in your example), the reference is an lvalue, and subsequent accesses through the reference will invoke the lvalue operator[] overload. – KnowItAllWannabe Mar 27 '15 at 22:59
  • 8
    Well, one obvious possible reason is that it can break existing code. Given `vector f(); void g(int &);`, `g(f()[0])` will suddenly stop working, or, if there's also a `void g(const int &);`, silently go to a different overload. – T.C. Mar 27 '15 at 23:00
  • 1
    @T.C.: Valid point, thanks. That's a reasonable answer to the question why the standard containers don't overload operator[] for lvalues and rvalues, but what about new containers where legacy code is not an issue? Is there a problem with the design in general? – KnowItAllWannabe Mar 27 '15 at 23:09
  • 1
    I don't see a problem with the design in general. After all, class member access uses very similar rules (`E1.E2` is an xvalue if `E2` names a non-static data member and `E1` is an rvalue). `std::experimental::optional` also uses a similar design, though I can't remember whether the `&&` version for that returns `T` or `T&&`. – T.C. Mar 27 '15 at 23:14
  • Returning a big vector by value and then throwing it all away except one element, is poor design – M.M Mar 28 '15 at 00:34

2 Answers2

3

Converting comment into answer:

There's nothing inherently wrong with this approach; class member access follows a similar rule (E1.E2 is an xvalue if E1 is an rvalue and E2 names a non-static data member and is not a reference, see [expr.ref]/4.2), and elements inside a container are logically similar to non-static data members.

A significant problem with doing it for std::vector or other standard containers is that it will likely break some legacy code. Consider:

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

foo(bar()[0]);

That last line will stop compiling if operator[] on an rvalue vector returned an xvalue. Alternatively - and arguably worse - if there is a foo(const int &) overload, it will silently start calling that function instead.

Also, returning a bunch of elements in a container and only using one element is already rather inefficient. It's arguable that code that does this probably doesn't care much about speed anyway, and so the small performance improvement is not worth introducing a potentially breaking change.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • 1
    Do you think your answer also applies to the omission of an rvalue iterator? – Nir Friedman Jul 05 '15 at 02:34
  • @NirFriedman rvalue iterator? Special overloads of `begin()/end()` for rvalues? I don't see how they can be useful. – T.C. Jul 05 '15 at 02:38
  • It would be a new type, the way there is an iterator and a const_iterator, you would have rvalue_iterator. They would be useful because they would eliminate the necessity of having separate "move flavored" algorithms. That is, there is std::copy to copy a range, and std::move to move a range, and copy_if, but no move_if. These move flavored algorithms shouldn't exist, it should be handled by the iterators. – Nir Friedman Jul 05 '15 at 02:41
  • 2
    @NirFriedman We have `std::move_iterator` for that, though it's arguable that an iterator indiscriminately presenting an rvalue is not necessarily the right way to do it (for instance, using `copy_if` with a predicate taking its argument by value is correct if inefficient, but if you add a move iterator to the mix everything blows up). – T.C. Jul 05 '15 at 02:45
  • I did not know about std::move_iterator, that's good stuff! The problem is that it does not lend itself to generic code in the same way. If your function takes a universal reference to a vector, and internally you want to use copy_if, how do you ensure that you get moves if the vector is an rvalue and copies otherwise? Obviously doable but ugly as sin (as far as I can see). Putting rvalue iterator on the same level as const_iterator would make this trivial. – Nir Friedman Jul 05 '15 at 02:52
  • 2
    @NirFriedman `using iter_t = std::conditional_t{}, std::move_iterator, decltype(vec.begin())>; std::copy_if(iter_t{vec.begin()}, iter_t{vec.end()}, out, pred);`? Regardless, getting an iterator out of an rvalue is dangerous enough that I don't think it's a good idea to support it beyond what is needed for backwards compatibility. – T.C. Jul 06 '15 at 20:37
  • This is what I meant by doable, but ugly as sin, but yes, thank you. I tend to not think that these are as dangerous as some make them out to be, move semantics are either: i) safe, or ii) via explicit cast. I actually think that rvalue iterators are important if you start doing stuff like Niebler's ranges; once you start composing functions involving standard containers there will be a lot of container rvalues, and if their elements are complex you'll want to efficiently move them. – Nir Friedman Jul 06 '15 at 20:52
  • I have a question about your legacy code: wouldn't `foo` be binding an integer reference to an integer that would be "deleted" as soon as `bar()` is done returning and the temporary vector it creates is deleted? Or is the deletion of the returned `std::vector` held off until `foo` is done executing? Maybe built-in types don't get deleted, but what about if instead of `int` it was `std::string`? I just find it odd that you can have lvalue references to entities contained within rvalues - since those rvalues are deleted and normally take their entities with them. – SergeantPenguin Oct 29 '16 at 11:19
  • @SergeantPenguin I think the object is alive till the end of statement (signaled by ";"). So in this case ``std::vector`` returned by ``bar()`` would be kept alive during the execution of ``foo()`` – skgbanga Feb 23 '17 at 15:04
  • @T.C. " rvalue iterator?" . Yes, in generic code this is the only way to keep information (in the iterator) that the elements can be moved individually. See here https://stackoverflow.com/questions/48915905/move-iterators-for-containers – alfC Aug 16 '18 at 08:07
  • @T.C. In perspective, this overload `void foo(int &);` should never have worked with `foo(bar()[0])` because it is consistent with the fact (in C++98) that a "temporary" object cannot bind to a non-const reference type. Moreover `bar()[0]` should never have returned a non-const reference in the first because that sets an unreasonable asymmetry between member functions and free functons on temporary objects. – alfC Aug 16 '18 at 08:12
0

I think you will leave the container in an invalid state if you move out one of the elements, I would argue the need to allow that state at all. Second, if you ever need that, can't you just call the new object's move constructor like this:

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

Update:

Most important point is, again in my opinion, that containers are containers by their nature, so they should not anyhow modify the elements inside them. They just provide a storage, iteration mechanism, etc.

Arsen Y.M.
  • 673
  • 1
  • 7
  • 18
  • 1
    Moving is required to leave the source object in a valid state, so the container would not be left in an invalid state. It's true that users can apply `std::move` themselves, but if that's the only reason for not overloading `operator[]` for lvalues and rvalues, I consider it a pretty weak motivation. – KnowItAllWannabe Mar 27 '15 at 22:44
  • @KnowItAllWannabe, OK I agree, it will be in a valid, but still unspecified state, and all you can do with those elements (which have been moved out from the container), just the actions that do not imply any preconditions. Can you give an example of a reasonable algorithm that you can run on your container after you move out some elements? – Arsen Y.M. Mar 27 '15 at 23:19
  • Sure: sort, partition, find_if, etc. For example, you can sort a container of containers by size, even if some of the contained containers have been moved from. Moved-from objects aren't evil or toxic or dangerous, they're just moved from, and for many types, you know what state they're in. For example, moved-fromt shared_ptrs are null. – KnowItAllWannabe Mar 27 '15 at 23:25
  • Yes, I understand that (for many types, you know what state they're in), please see my update in the answer, I guess that is the main reason. – Arsen Y.M. Mar 27 '15 at 23:38