Мой основной посыл был в том что end() не нужен ни для чего кроме итерации с конца (для тех коллекций, у которых вообще говоря есть внятное понятие конца), это источник багов. То, для чего он используется, реализуется гораздо безопаснее через манипуляции с начальным итератором.
Нет, не только для "итерации с конца". Рассмотрим следующий пример. В нем мы запомнили позиции интересующих нас граничных элементов в ordered set ровно один раз в const хранилище и больше не паримся, а при добавлении элементов в этот set дополнительные элементы, оказавшиеся между этими граничными, автоматически попадают в диапазон и обрабатываются (в данном примере - тупо печатаются). Работает все это легко и просто, так сказать, естественным образом. Теперь давайте подумаем, как реализовать это через манипуляции только лишь с начальным итератором? Хранить "длину" диапазона тут не вариант, она динамическая. Сделать в недоитераторе очередной "специальный" ad hoc метод, который бы сравнивал очередной элемент с образцом и считал элемент, совпадающий с образцом, за end? Ну так это тут для иллюстрации set состоит из char'ов, а если будет что-то "сильно побольше"? Кроме того, это путь в никуда, в растовский std::iter::Iterator вон надобавляли 100500 ad hoc методов с синтаксическим сахаром на полсотни экранов (и постоянно добавляют новые в nightly), а все равно элементарных вещей не умеет. Но их понять можно, у них просто других вариантов нет, но в C++-то зачем эту убогость тащить?
Да, именно так, нужен "специальный" итератор, нужны изменения в вызывающем коде. Плюс я не пробовал, но что-то мне подсказывает, что если ты внезапно используешь некий "сторонний" контейнер из какого-то библиотечного крейта, то ты не сможешь "просто так взять и" сделать для него 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 (про который многие считают, что и его-то быть не должно) уже мало, нужно еще какое-то зарезервированное значение. Поддержки со стороны одного лишь компилятора оказалось маловато. Я просто уверен, что с дальнейшим продвижением в лес количество дров будет возрастать и дальше. А со структурами с минимальным размером в 1 байт ничего из этого просто не нужно, все работает "естественным образом".
Эхехе. Ну вот взять например тот же вызов malloc для void (ведь раз его можно разместить "на стеке", значит, и на куче можно, так?). Что должен вернуть malloc? Реально выделить память нулевого размера он не может. NULL означает ошибку. Сделать "специальный случай" и не вызывать malloc вовсе (примерно так делает и растовский Box)? Сработает, если ты контролируешь реализацию, как растовский std контролирует реализацию своего Box (и то с проблемами, когда дело доходит до получения raw указателя), не сработает для пользовательского кода (например, шаблонного). Потребуются приседания, в том числе и со стороны пейсателя кода, по сути аналогичные приседаниям автора статьи - вставить constexpr if, придумать отдельную ветку для такого случая, как-то откуда-то сделать какой-то указатель и т.п. А в C вообще пришлось бы приседать в рантайме, а не в compile time.
Нет, не только для "итерации с конца". Рассмотрим следующий пример. В нем мы запомнили позиции интересующих нас граничных элементов в ordered set ровно один раз в
const
хранилище и больше не паримся, а при добавлении элементов в этот set дополнительные элементы, оказавшиеся между этими граничными, автоматически попадают в диапазон и обрабатываются (в данном примере - тупо печатаются). Работает все это легко и просто, так сказать, естественным образом. Теперь давайте подумаем, как реализовать это через манипуляции только лишь с начальным итератором? Хранить "длину" диапазона тут не вариант, она динамическая. Сделать в недоитераторе очередной "специальный" ad hoc метод, который бы сравнивал очередной элемент с образцом и считал элемент, совпадающий с образцом, за end? Ну так это тут для иллюстрации set состоит из char'ов, а если будет что-то "сильно побольше"? Кроме того, это путь в никуда, в растовскийstd::iter::Iterator
вон надобавляли 100500 ad hoc методов с синтаксическим сахаром на полсотни экранов (и постоянно добавляют новые в nightly), а все равно элементарных вещей не умеет. Но их понять можно, у них просто других вариантов нет, но в C++-то зачем эту убогость тащить?Да, именно так, нужен "специальный" итератор, нужны изменения в вызывающем коде. Плюс я не пробовал, но что-то мне подсказывает, что если ты внезапно используешь некий "сторонний" контейнер из какого-то библиотечного крейта, то ты не сможешь "просто так взять и" сделать для него
impl trait
того же rayon'овскогоIntoParallelRefMutIterator
в своем собственном коде, если тебе захочется распараллелить работу с ним.Здорово. Вот берем такой дженерик "в том же расте" (ведь это же "нормальный язык-то"?), который принимает итератор на
HashMap
илиBTreeMap
и удваивает значение каждого элемента своего диапазона:Пример работы. Пожалуйста, распараллельте (точнее, "партиционируйте") его. Ну так, чтобы внутри этой функции этот
iter
разбивался на несколько поддиапазонов, представленных отдельными итераторами, которые затем, скажем, передавались бы в функцию, символизирующую распараллеливание работы (старт потока). В C++ это - элементарная задача для функции, принимающей классическую пару итераторовbegin
/end
, относящихся к контейнеру любого типа. Продемонстрируйте, пожалуйста, как это сделать в расте, только не словоблудием, а, как выше написал ваш коллега, "и вы тут такой вжух - и пример рабочего кода" (можно даже и без создания настоящих потоков, ожидания и т.д., просто покажите сам процесс партиционирования).Ну так эти специализации уже есть, в обобщенном коде ты просто пишешь
std::advance
илиstd::distance
и все, все искаропки работает максимально эффективным образом.Ну, ясно.
Да, без проблем, это называется RandomAccessIterator. Скажем
std::advance
имеет специализации для разных типов итераторов, чтобы можно было универсально с ними работать. Меня всегда поражали люди, которые начинают свои "а вот в нормальных языках-то", не удосужившись изучить матчасть.Ну как вы разобьете нечто, что имеет лишь begin и next, возвращающий очередной элемент, на поддиапазоны? А ваши слова про "у итератора в C++ нет операции сдвинуться на N" - это просто бред человека, который не в теме.
Даже если итератор не умеет в random access, то его инкремент как правило весьма дешевая операция. А вот обработка самих элементов, на которые ссылаются эти итераторы, может быть операцией весьма дорогой, так что распараллеливание принесет свои плюсы, даже если придется пройтись O(n) по самим итераторам.
Вы бредите.
Замечательно, только вот... что если вам нужно, скажем, разбить существующий диапазон на поддиапазоны? В C++ это можно сделать без проблем, имея пару итераторов "начало-конец", а только лишь с предлагаемыми вами убогими недоитераторами это не сделаешь, нужно работать тогда непосредственно с тем, из чего вы такой итератор получили, да и то не всегда это поможет. Например, в C++ можно разбить диапазон, состоящий из пары итераторов, полученных хоть из массива, хоть из вектора, хоть из хешмапы, на поддиапазоны и закинуть обработку этих поддиапазонов по разным потокам, универсальным образом (опять же пара итераторов, никаких проблем). А что делать с вашим недоитератором? Ладно, окей, забьем на универсальность кода, будем работать с частным случаем - с исходным контейнером, скажем, с
HashMap
из раста, покажите, пожалуйста, как разбить этотHashMap
скажем на 2 или 4 поддиапазона для параллельной обработки его элементов без создания копий.То, что в C++ обозначается универсальным образом (парой итераторов), "в нормальных-то языках" требует дополнительных сущностей, если ты, скажем, хочешь работать только с частью диапазона без создания копий - например, в расте это слайсы,
Range
, и т.д., причем для каких-то контейнеров работает только что-то одно, а для каких-то только что-то другое, писать, так сказать, контейнеронезависимый универсальный код загребешься.Ну, этими аргументами вы не проймете :) В расте, например, такой код будет работать по-разному будучи собранным в дебаге и релизе, но это никого из апологетов раста не смущает, все привыкли, дескать, "а чего вы хотите от объектов с нулевым размером", как в анекдоте про доктора, который отвечает пациенту "а вы так не делайте". И, в принципе, можно сказать, что они в своем праве, для них это нечто вроде вкусовщины.
Пока не попробуешь - не узнаешь :)
P.S. Ну вообще в релизе по крайней мере в данном конкретном (я бы сказал, простейшем) случае оптимизатор это дело оптимизирует и размещает обе структуры по одному адресу. Но это именно что оптимизация, которая по сути своей опциональна и в отладочной сборке не выполняется. Т.е. никаких гарантий по факту раст на эту тему не предоставляет.
И в чем тут "возражение"? Это взаимодополняющие утверждения, я бы сказал.
Ну это не то чтобы я упираюсь, а сами libc по большей части. Ведь
malloc()
сам по себе не в курсе целевого типа, а только лишь необходимого размера, что ему мешало раньше применять такую "оптимизацию" при выделении блоков нулевого размера? Но как-то я в распространенных реализациях libc такой "оптимизации" не встречал. Видимо, на то есть причины.Ну фактически тоже нет. Возьмем тот же раст, в котором "со всем этим справились" (нет). Уникальные адреса на стеке? Да. Значит, фактически размер уже не может быть "0 байт".
Ну это не то чтобы аргумент против, просто я к тому, что в реальности размер все равно будет не 0, так зачем притворяться, что он 0. Вот реализация пустых структур в C++ и решила не притворяться.
Ну так-то справедливости ради нужно сказать что адресную арифметику с
void*
в стандартных C и C++ использовать нельзя, можно только в расширениях, в частности от GNU. В Cvoid
- это, как там, "incomplete type that cannot be complete" или как-то так, поэтому его размер неизвестен и известен быть не может, какая уж тут адресная арифметика.Это касается только и непосредственно самого
void
. Если на местеvoid
будет пустая структура, то из-за ее ненулевого размера никаких специальных приседаний с ней не требуется совершенно.Да, и я о чем. К типам нулевого размера нужно "приучать" не только компилятор, но и libc, а это, как вы справедливо заметили, отдельная от конпелятора вещь.
Он и сейчас может это делать - создавать non-dereferenceable область "в ноль байт" (на самом деле нет) с уникальным адресом, которую потом нужно "освободить" через вызов
free()
, а может вернутьNULL
. Это допустимое с точки зрения стандарта поведение.Ну если брать например раст, то там, как я уже выше говорил, используется подход с 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.