Pull to refresh
7
0.3

Пользователь

Send message

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

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

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

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

Здорово. Вот берем такой дженерик "в том же расте" (ведь это же "нормальный язык-то"?), который принимает итератор на 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, относящихся к контейнеру любого типа. Продемонстрируйте, пожалуйста, как это сделать в расте, только не словоблудием, а, как выше написал ваш коллега, "и вы тут такой вжух - и пример рабочего кода" (можно даже и без создания настоящих потоков, ожидания и т.д., просто покажите сам процесс партиционирования).

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

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

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

Ну, ясно.

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

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

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

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

Вы бредите.

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

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

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

Ну, этими аргументами вы не проймете :) В расте, например, такой код будет работать по-разному будучи собранным в дебаге и релизе, но это никого из апологетов раста не смущает, все привыкли, дескать, "а чего вы хотите от объектов с нулевым размером", как в анекдоте про доктора, который отвечает пациенту "а вы так не делайте". И, в принципе, можно сказать, что они в своем праве, для них это нечто вроде вкусовщины.

А что будет, если это требование не выполнить?

Пока не попробуешь - не узнаешь :)

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

Вы говорите, что malloc(0) потратит не 0? На что я могу возразить, что malloc(17) потратит не 17 байт, а больше.

И в чем тут "возражение"? Это взаимодополняющие утверждения, я бы сказал.

Причём, есть реальная возможность чтобы malloc(0) тратил 0, но вы всеми лапами упираетесь против такой оптимизации libc, приплетая сюда какой-то "второй NULL" (который будет неизвестен никому, кроме free от libc, а значит, не потребует нигде ещё особого обращения).

Ну это не то чтобы я упираюсь, а сами libc по большей части. Ведь malloc() сам по себе не в курсе целевого типа, а только лишь необходимого размера, что ему мешало раньше применять такую "оптимизацию" при выделении блоков нулевого размера? Но как-то я в распространенных реализациях libc такой "оптимизации" не встречал. Видимо, на то есть причины.

Опять же, вы зацепились за malloc. В других же местах будет 0 байт, хоть структура, хоть массив из 100 void-ов.

Ну фактически тоже нет. Возьмем тот же раст, в котором "со всем этим справились" (нет). Уникальные адреса на стеке? Да. Значит, фактически размер уже не может быть "0 байт".

То есть, это просто замечание, а не аргумент против.

Ну это не то чтобы аргумент против, просто я к тому, что в реальности размер все равно будет не 0, так зачем притворяться, что он 0. Вот реализация пустых структур в C++ и решила не притворяться.

Ну так-то справедливости ради нужно сказать что адресную арифметику с void* в стандартных C и C++ использовать нельзя, можно только в расширениях, в частности от GNU. В C void - это, как там, "incomplete type that cannot be complete" или как-то так, поэтому его размер неизвестен и известен быть не может, какая уж тут адресная арифметика.

За исключением того, что если нужно какое-то метапрограммирование, нужно отдельно описывать случай void-функции и не-void.

Это касается только и непосредственно самого void. Если на месте void будет пустая структура, то из-за ее ненулевого размера никаких специальных приседаний с ней не требуется совершенно.

Это вообще никак не влияет на стандарт и пользователей библиотек/компиляторов. Это детали реализации stdlib.

Да, и я о чем. К типам нулевого размера нужно "приучать" не только компилятор, но и libc, а это, как вы справедливо заметили, отдельная от конпелятора вещь.

Если не хотите оптимизации, пусть malloc выделяет область в 0 байт, делает перед ней обычные заголовки области, и отдаёт уникальный адрес.

Он и сейчас может это делать - создавать non-dereferenceable область "в ноль байт" (на самом деле нет) с уникальным адресом, которую потом нужно "освободить" через вызов free(), а может вернуть NULL. Это допустимое с точки зрения стандарта поведение.

Кроме malloc (по которому вы меня не убедили), других возражений нет?

Ну если брать например раст, то там, как я уже выше говорил, используется подход с non-dereferenceable указателем (со значением 0x1) для объектов нулевого размера, расположенных "в куче" (по крайней мере Box возвращает такой вот "указатель" на содержащийся в себе unit type). Это весьма error-prone - скажем, в libc'шном memcpy() такой "указатель" использовать нельзя вне зависимости от значения параметра count. Но это сейчас, а какие соображения были против zero-sized void в свое время у создателей C, чем они руководствовались - я не знаю, но, видимо, какие-то основания у них были.

Предлагаемые вами выкрутасы со "вторым NULL" - это не рост цены?

Ну т.е. одного NULL (про который многие считают, что и его-то быть не должно) уже мало, нужно еще какое-то зарезервированное значение. Поддержки со стороны одного лишь компилятора оказалось маловато. Я просто уверен, что с дальнейшим продвижением в лес количество дров будет возрастать и дальше. А со структурами с минимальным размером в 1 байт ничего из этого просто не нужно, все работает "естественным образом".

Ввели ограничения для своего удобства

Именно что для удобства, причём не только и не столько своего.

оправдывают этим принципиальную невозможность

А кто говорит о принципиальной невозможности? По-моему никто. Все говорят исключительно о цене реализации этих хотелок.

Не вижу проблемы

Эхехе. Ну вот взять например тот же вызов malloc для void (ведь раз его можно разместить "на стеке", значит, и на куче можно, так?). Что должен вернуть malloc? Реально выделить память нулевого размера он не может. NULL означает ошибку. Сделать "специальный случай" и не вызывать malloc вовсе (примерно так делает и растовский Box)? Сработает, если ты контролируешь реализацию, как растовский std контролирует реализацию своего Box (и то с проблемами, когда дело доходит до получения raw указателя), не сработает для пользовательского кода (например, шаблонного). Потребуются приседания, в том числе и со стороны пейсателя кода, по сути аналогичные приседаниям автора статьи - вставить constexpr if, придумать отдельную ветку для такого случая, как-то откуда-то сделать какой-то указатель и т.п. А в C вообще пришлось бы приседать в рантайме, а не в compile time.

1
23 ...

Information

Rating
2,394-th
Registered
Activity