Move iterators for containers?

2019-02-12 17:56发布

C++98 containers defined two kinds of iterator, ::iterators and ::const_iterators. Generally, like this:

struct vec{
   iterator begin();
   const_iterator begin() const;
};

In C++11 this part of the design seems to be unchanged. The question is, for consistency and for practical purposes would it make sense to add ::move_iterators as well? or it an overkill.

I can imagine that an rvalue container maybe have their elements moved if possible.

class vec{
   iterator begin() &;
   const_iterator begin() const&;
   move_iterator begin() &&;
};

If I understand correctly, it could be implemented like this in simple cases:

auto vec::begin() &&{return std::make_move_iterator(this->begin());}

Of course a normal iterator can be converted to a move iterator (with std::make_move_iterator), however the motivations is generic code.

For example, with a move iterator this would be very elegantly implemented without conditions depending on whether the argument is an lvalue or an rvalue.

template<class Container, class T = Container::value_type>
void transport_first(Container&& c, std::vector<T>& v){
    v.emplace_back(*std::forward<Container>(c).begin());
}

Note that this code would incur in no unnecessary copies if possible. How can this be implemented without move_iterators generated by begin.


I also realize that this question applies to almost any accessor to the container, for example, operator[], front() and back().

template<class Value>
class vec{
   using value_type = Value;
   using reference = Value&;
   using const_reference = Value const&;
   using rvalue_reference = Value&&; // NEW!
   reference front() &{...}
   rvalue_reference front() &&{...} // NEW!
   const_reference front() const&{...}
};

Perhaps containers should have been redesigned from scratch in C++11. Their design is showing its age.


There is a proposal, to automatically deduce the (decl)type of (*this) basically having all the corresponding overload of begin (and other member functions) for free.

https://youtu.be/yB4E-SzQPdI?t=4131

2条回答
疯言疯语
2楼-- · 2019-02-12 17:59

STL containers are designed to be used in conjunction with STL algorithms. The generic algorithms currently found in the STL deal with iterators, but your template function transport_first:

template<class Container, class T = Container::value_type>
void transport_first(Container&& c, std::vector<T>& v){
    v.emplace_back(*std::forward<Container>(c).begin());
}

consists of container-based code, i.e.: it operates on containers rather than on iterators. We may be able to find such container-based algorithms as part of the STL as of C++20 thanks to concepts.

The "STL-like approach" to a generic transport_first algorithm would actually be:

template<typename InIt, typename OutIt>
void transport_first(InIt src, OutIt dst) {
    *dst = *src;
}

Following this approach (i.e.: iterators instead of containers), when it comes to generic code, whether to use move_iterator or not can be simply determined at the "topmost" generic algorithm, since the passed iterators are copied further (i.e.: being passed by value into the "underlying" algorithms).

In addition, unlike the iterator-based transport_first above, the STL is full of iterator pair-based algorithms (e.g.: std::copy) to implement ranges. This interface makes overloading those container member functions on rvalues little attractive, because as already stated in this other answer, those overloaded member functions will be called, among others, on unnamed (non-const) container objects, and unnamed container objects are limited to be used in a single expression, hindering the creation of an iterator pair by calling both begin() and end() member functions on the same container object.


What would really make sense for containers, is to have fresh new member functions for returning the corresponding move_iterator objects:

move_iterator Container<T>::mbegin();
move_iterator Container<T>::mend();

This would be just a shortcut for calling std::make_move_iterator with the value returned by the member functions returning a iterator. The member function mbegin() would be used in the iterator-based transport_first as:

transport_first(coll.mbegin(), std::back_inserter(vec));

The corresponding function templates std::mbegin() and std::mend() would also make sense:

transport_first(std::mbegin(coll), std::back_inserter(vec));
查看更多
叛逆
3楼-- · 2019-02-12 18:08

You can trivially convert any non-const iterator into a move iterator. It makes generally no difference to the container.

You cannot trivially convert non-const iterators to const iterators. For example, a copy-on-write string (std::string in the past for some compilers, custom strings could still be this) has to pessimistically detach from shared data when a non-const iterator is taken (with begin()) in order to fulfill typical invalidation guarantees, which is hugely inefficient when you just want a const iterator.

In addition, C++ overloading rules don't allow you to introduce an rvalue overload of begin() without changing the unspecified version to lvalue, and that would be a breaking change.

Finally, overloading begin() on rvalues is not useful anyway - the expectation is that rvalue functions are called on rvalues, and except for those produced by std::move, these rvalues are 1) going away soon (which would invalidate the obtained iterator) and 2) have no name, which means that they can only be used in one expression, which means you cannot call both begin() and end() to obtain an iterator pair, and a single iterator is useless, since you can never know whether it's safe to dereference it.

查看更多
登录 后发表回答