Переместить итераторы для контейнеров?

Контейнеры C++98 определяют два типа итераторов: ::iterators и ::const_iterators. В общем, вот так:

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

В С++ 11 эта часть дизайна кажется неизменной. Вопрос в том, для согласованности и практических целей имеет ли смысл добавить также ::move_iterators? или это излишество.

Я могу представить, что элементы контейнера rvalue могут быть перемещены, если это возможно.

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

Если я правильно понимаю, в простых случаях это можно реализовать так:

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

Конечно, обычный итератор можно преобразовать в итератор перемещения (с помощью std::make_move_iterator), однако для этого используется общий код.

Например, с итератором перемещения это было бы очень элегантно реализовано без условий, зависящих от того, является ли аргумент lvalue или 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());
}

Обратите внимание, что этот код не будет иметь ненужных копий, если это возможно. Как это можно реализовать без move_iterators, сгенерированного begin.


Я также понимаю, что этот вопрос относится практически к любому средству доступа к контейнеру, например, operator[], front() и 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&{...}
};

Возможно, контейнеры должны были быть переработаны с нуля в C++11. Их дизайн показывает свой возраст.


Есть предложение автоматически вывести (decl) тип (*this), в основном имея всю соответствующую перегрузку begin (и других функций-членов) бесплатно.

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


person alfC    schedule 21.02.2018    source источник


Ответы (2)


Контейнеры STL предназначены для использования в сочетании с алгоритмами STL. Общие алгоритмы, имеющиеся в настоящее время в STL, имеют дело с итераторами, но ваша функция шаблона 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());
}

состоит из кода, основанного на контейнерах, то есть он работает с контейнерами, а не с итераторами. Возможно, мы сможем найти такие алгоритмы на основе контейнеров как часть STL, начиная с C++20, благодаря концепциям.

«STL-подобный подход» к общему алгоритму transport_first на самом деле будет таким:

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

Следуя этому подходу (т. е. итераторы вместо контейнеров), когда дело доходит до универсального кода, можно просто определить, использовать ли move_iterator или нет в «самом верхнем» универсальном алгоритме, поскольку переданные итераторы копируются дальше (т. е. передаются значение в «базовые» алгоритмы).

Кроме того, в отличие от описанного выше алгоритма transport_first на основе итератора, в STL полно алгоритмов на основе пары итераторов (например, std::copy) для реализации диапазонов. Этот интерфейс делает перегрузку этих функций-членов контейнера для rvalues малопривлекательной, потому что, как уже указано в этот другой ответ эти перегруженные функции-члены будут вызываться, помимо прочего, для неименованных (не const) объектов-контейнеров, а безымянные объекты-контейнеры могут использоваться только в одном выражении, что затрудняет создание пары итераторов, вызвав функции-члены begin() и end() для одного и того же объекта-контейнера.


Что действительно имеет смысл для контейнеров, так это иметь новые новые функции-члены для возврата соответствующих объектов move_iterator:

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

Это был бы просто ярлык для вызова std::make_move_iterator со значением, возвращаемым функциями-членами, возвращающими iterator. Функция-член mbegin() будет использоваться в transport_first на основе итератора как:

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

Соответствующие шаблоны функций std::mbegin() и std::mend() также имеют смысл:

transport_first(std::mbegin(coll), std::back_inserter(vec));
person 眠りネロク    schedule 03.03.2018
comment
Я пошел по этому пути, сначала добавил mbegin и mend (аналогично cbegin и cend), а потом понял, что было бы неплохо реализовать begin()&& и end()&& для автоматического вывода вызова mbegin и mend (аналогично тому, что begin() const и end() const делает), и тогда я задал вопрос. - person alfC; 04.03.2018
comment
См. здесь: youtu.be/yB4E-SzQPdI?t=4131 , предложение имея все перегрузки begin, добавив языковую функцию. - person alfC; 04.06.2018

Вы можете тривиально преобразовать любой неконстантный итератор в итератор перемещения. Обычно это не имеет значения для контейнера.

Вы не можете тривиально преобразовать неконстантные итераторы в константные итераторы. Например, строка с копированием при записи (в прошлом для некоторых компиляторов std::string пользовательские строки все еще могли быть такими) должна пессимистически отсоединяться от общих данных, когда используется неконстантный итератор (с begin()), чтобы выполнить типичные гарантии аннулирования, что крайне неэффективно, когда вам нужен только константный итератор.

Кроме того, правила перегрузки C++ не позволяют вводить перегрузку rvalue begin() без изменения неуказанной версии на lvalue, а это было бы критическим изменением.

Наконец, перегрузка begin() для rvalue в любом случае бесполезна - предполагается, что функции rvalue вызываются для rvalue, и, за исключением тех, которые создаются std::move, эти rvalue 1) скоро исчезнут (что сделает недействительным полученный итератор) и 2) имеют без имени, что означает, что они могут использоваться только в одном выражении, что означает, что вы не можете вызывать оба итератора begin() и end() для получения пары итераторов, и один итератор бесполезен, так как вы никогда не можете знать, безопасно ли его разыменовывать.

person Sebastian Redl    schedule 21.02.2018
comment
Я не понимаю, почему вы сказали, не меняя неуказанную версию на lvalue, и это было бы критическим изменением. Даже если это нарушит код, что, если контейнеры можно будет переделать, не будет ли это хорошей идеей? Как насчет недавно разработанных контейнеров? Конечно, обычный итератор можно преобразовать в итератор перемещения (с помощью std::make_move_iterator), однако для этого используется общий код. По сути, иногда слишком поздно знать, можно ли применить make_move_iterator или нет. Посмотрите пример кода, который я сейчас вставил в свой вопрос. - person alfC; 22.02.2018
comment
Послушайте, myfront = *std::forward(cont).begin(); myback = *std::forward(cont).rbegin() — хороший пример того, почему эта перегрузка была бы полезна. См. также здесь stackoverflow.com/questions/50149972/ , вызывая переадресацию дважды (это не означает, что объект немедленно становится недействительным, особенно потому, что вызов begin не должен ничего делать недействительным в первую очередь). - person alfC; 03.05.2018
comment
не должен... да, как будто я этому доверяю. - person Sebastian Redl; 03.05.2018
comment
Это саркастично? Я имею в виду, что begin() семантически означает указатель на первый элемент, конечно, можно написать очень среднее begin(), но здесь вопрос в хорошем/полезном обобщении текущего begin() для контейнеров. - person alfC; 03.05.2018
comment
Я говорю, что не доверяю никакому методу, вызываемому для ссылки rvalue, чтобы он не модифицировал объект, и я думаю, что это глупо. (Кроме того, ваш код сломается, если контейнер содержит ровно один элемент.) - person Sebastian Redl; 03.05.2018