Как стать автором
Обновить

Комментарии 28

Реклама, опять реклама :(.

Хотя бы ссылку на "подробности" сделайте нажимаемой, а то все усилия пропадут втуне.

--

Сарказм: не хочу показаться невежливым, но вы свой ник пробовали в переводчик вставить. Ну, на всякий случай.

Иногда хочется, чтобы у Хабра была платная подписка с бесплатным вариантом для школьников/студентов вместо сотрудничества с крупными компаниями в качестве рекламной платформы.

--

По поводу сарказма: предполагаю, что никнейм был взят из Urban Dictionary. Непропеченное печенье.

Тем временем в нормальных языках программирования у итератора есть два метода: next(), возвращающий элемент и hasNext(), сигнализирующий что всё, элементов больше нету. Это даже можно было бы скукожить до одного метода, возвращающего std::optional. Но нет, давайте изобретать костыли с указателями-хрен-знает-куда (и порожать баги вида "случайно сравнили с end от другой коллекции"), дополнительных объектов в виде sentinel и т.д...

То, что в C++ обозначается универсальным образом (парой итераторов), "в нормальных-то языках" требует дополнительных сущностей, если ты, скажем, хочешь работать только с частью диапазона без создания копий - например, в расте это слайсы, Range, и т.д., причем для каких-то контейнеров работает только что-то одно, а для каких-то только что-то другое, писать, так сказать, контейнеронезависимый универсальный код загребешься.

Если я хочу работать с частью диапазона, я создам итератор-обёртку, который скажет что он кончился раньше. Если я захочу работать только с чётными элементами, я создам итератор, который в next() будет делать два шага вперёд. Если я захочу создать бесконечный итератор, в цикле перебирающий коллекцию, я тоже прекрасно могу это сделать. Не нужны никакие дополнительные сущности.

Замечательно, только вот... что если вам нужно, скажем, разбить существующий диапазон на поддиапазоны? В C++ это можно сделать без проблем, имея пару итераторов "начало-конец", а только лишь с предлагаемыми вами убогими недоитераторами это не сделаешь, нужно работать тогда непосредственно с тем, из чего вы такой итератор получили, да и то не всегда это поможет. Например, в C++ можно разбить диапазон, состоящий из пары итераторов, полученных хоть из массива, хоть из вектора, хоть из хешмапы, на поддиапазоны и закинуть обработку этих поддиапазонов по разным потокам, универсальным образом (опять же пара итераторов, никаких проблем). А что делать с вашим недоитератором? Ладно, окей, забьем на универсальность кода, будем работать с частным случаем - с исходным контейнером, скажем, с HashMap из раста, покажите, пожалуйста, как разбить этот HashMap скажем на 2 или 4 поддиапазона для параллельной обработки его элементов без создания копий.

Я правильно понимаю что вы хотите взять end() - begin(), поделить это на N кусков и пойти с ними что-то делать? Есть подозрение что вычисление концов промежуточных диапазонов на большом количестве коллекций (примерно всех, которые основаны не на массиве внутри) выльется в O(n) и перф будет грустить.

UPD: или даже хуже, это O(n) вообще для каких угодно коллекций, потому что у C++-итератора нет операции "сдвинуться на N". А раз речь про O(n) - ну берем begin, проматываем его полностью чтобы узнать размер, дальше мотаем опять от начала до нужных границ :)

Даже если итератор не умеет в random access, то его инкремент как правило весьма дешевая операция. А вот обработка самих элементов, на которые ссылаются эти итераторы, может быть операцией весьма дорогой, так что распараллеливание принесет свои плюсы, даже если придется пройтись O(n) по самим итераторам.

потому что у C++-итератора нет операции "сдвинуться на N"

Вы бредите.

Ну вот я и говорю, оно прекрасно делается с той же самой ассимптотикой имея на руках только лишь begin.

Ну как вы разобьете нечто, что имеет лишь begin и next, возвращающий очередной элемент, на поддиапазоны? А ваши слова про "у итератора в C++ нет операции сдвинуться на N" - это просто бред человека, который не в теме.

А ваши слова про "у итератора в C++ нет операции сдвинуться на N" - это просто бред человека, который не в теме.

И тут вы такой вжух, и ссылку на описание соответствующего метода.

Да, без проблем, это называется RandomAccessIterator. Скажем std::advance имеет специализации для разных типов итераторов, чтобы можно было универсально с ними работать. Меня всегда поражали люди, которые начинают свои "а вот в нормальных языках-то", не удосужившись изучить матчасть.

имеет специализации

Ну начинается. Процитирую вас же: "писать, так сказать, контейнеронезависимый универсальный код загребешься". То есть в C++ специализации - это норм, а в других языках уже не норм.

Впрочем, учитывая вашу манеру общения с переходом на личности, продолжать беседу не имею ни малейшего желания. Хорошего вечера.

То есть в C++ специализации - это норм, а в других языках уже не норм.

Ну так эти специализации уже есть, в обобщенном коде ты просто пишешь std::advance или std::distance и все, все искаропки работает максимально эффективным образом.

Впрочем, учитывая вашу манеру общения с переходом на личности, продолжать беседу не имею ни малейшего желания. Хорошего вечера.

Ну, ясно.

В том же расте у итераторов есть дополнительные методы, такой универсальный диапазон можно организовать с помощью .skip(a).take(b).

Здорово. Вот берем такой дженерик "в том же расте" (ведь это же "нормальный язык-то"?), который принимает итератор на HashMap или BTreeMap и удваивает значение каждого элемента своего диапазона:

fn mul<
    I: std::iter::Iterator<Item = (K, V)>,
    K,
    V: std::ops::DerefMut<Target = T>,
    T: std::ops::MulAssign<i32>,
>(
    iter: I,
) {
    for (_, mut val) in iter {
        *val *= 2;
    }
}

Пример работы. Пожалуйста, распараллельте (точнее, "партиционируйте") его. Ну так, чтобы внутри этой функции этот iter разбивался на несколько поддиапазонов, представленных отдельными итераторами, которые затем, скажем, передавались бы в функцию, символизирующую распараллеливание работы (старт потока). В C++ это - элементарная задача для функции, принимающей классическую пару итераторов begin/end, относящихся к контейнеру любого типа. Продемонстрируйте, пожалуйста, как это сделать в расте, только не словоблудием, а, как выше написал ваш коллега, "и вы тут такой вжух - и пример рабочего кода" (можно даже и без создания настоящих потоков, ожидания и т.д., просто покажите сам процесс партиционирования).

Тут вы правы, обычный итератор так разбить не получится, как минимум потому что никто не запрещает ему вернуть ссылку на один и тот же элемент дважды подряд, что сразу нарушит правило "только одна &mut ссылка на каждый объект".

С помощью rayon можно сделать так: playground

Это, конечно, не делает явное партиционирование, но вполне честно распарралеливает обработку.

С помощью rayon можно сделать так

Да, именно так, нужен "специальный" итератор, нужны изменения в вызывающем коде. Плюс я не пробовал, но что-то мне подсказывает, что если ты внезапно используешь некий "сторонний" контейнер из какого-то библиотечного крейта, то ты не сможешь "просто так взять и" сделать для него impl trait того же rayon'овского IntoParallelRefMutIterator в своем собственном коде, если тебе захочется распараллелить работу с ним.

Только функция next() нарушает принцип единственной ответственности, делает сразу две вещи, и итератор передвигает, и элемент возвращает.

SRP абсолютно про другое.

То есть, функцию next() делает две вещи, но принцип SRP не нарушает? Вот тут нужны прям подробные пояснения.

Я специально дал ссылку. Цитирую дядюшку Мартина:

A class should have only one reason to change

Также

Another wording for the Single Responsibility Principle is: Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Можно хоть миллион вещей запихивать в одну функцию, если они change for the same reasons, и это не будет нарушением SRP.

Что дядюшка Мартин говорит про именование функций? Как бы он порекомендовал назвать функцию .которая возвращает текущий элемент и передвигает итератор?

Без понятия, это вы пытаетесь апеллировать к Мартину, притаскивая сюда SRP, а не я.

Предположу, что в случает с next дело больше в гарантиях исключений.

Есть очень хорошая гарантия, когда при выбрасывании исключения ничего другое в значениях не поменялось (т.е. осталось исходное состояние).

В случае с next, если при копировании возвращаемого значения вылетит исключение (а в C++ это очень возможно), то вы получите итератор на новой позиции. И это не очень хорошо.

Поэтому в стандартной библиотеке C++ любят делить собственно движение по контейнеру и возврат/удаление/итд элемента - тогда можно разделить обработку ошибок/исключений.

Если рассматривать другие языки (где копируются не объекты, а указатели, и при этом ошибок быть не может), то там функционал наподобие next применяется и приветствуется. Просто это другие языки :).

Мой основной посыл был в том что end() не нужен ни для чего кроме итерации с конца (для тех коллекций, у которых вообще говоря есть внятное понятие конца), это источник багов. То, для чего он используется, реализуется гораздо безопаснее через манипуляции с начальным итератором. А штуки вида "проитерируемся только по чётным элементам" кроме как через начальный итератор вообще никак не делаются.

Мой основной посыл был в том что end() не нужен ни для чего кроме итерации с конца (для тех коллекций, у которых вообще говоря есть внятное понятие конца), это источник багов. То, для чего он используется, реализуется гораздо безопаснее через манипуляции с начальным итератором.

Нет, не только для "итерации с конца". Рассмотрим следующий пример. В нем мы запомнили позиции интересующих нас граничных элементов в ordered set ровно один раз в const хранилище и больше не паримся, а при добавлении элементов в этот set дополнительные элементы, оказавшиеся между этими граничными, автоматически попадают в диапазон и обрабатываются (в данном примере - тупо печатаются). Работает все это легко и просто, так сказать, естественным образом. Теперь давайте подумаем, как реализовать это через манипуляции только лишь с начальным итератором? Хранить "длину" диапазона тут не вариант, она динамическая. Сделать в недоитераторе очередной "специальный" ad hoc метод, который бы сравнивал очередной элемент с образцом и считал элемент, совпадающий с образцом, за end? Ну так это тут для иллюстрации set состоит из char'ов, а если будет что-то "сильно побольше"? Кроме того, это путь в никуда, в растовский std::iter::Iterator вон надобавляли 100500 ad hoc методов с синтаксическим сахаром на полсотни экранов (и постоянно добавляют новые в nightly), а все равно элементарных вещей не умеет. Но их понять можно, у них просто других вариантов нет, но в C++-то зачем эту убогость тащить?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий