Pull to refresh
13
0.2

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

Send message

Ну, одни дегенераты уже доигрались с "зелёным переходом", сделав свое производство неконкурентоспособным, удачи и этим с такими начинаниями, бгг.

P.S. А когда подобные баги на "безопасных языках" встанут на поток, бюрократы напишут очередные бумажки составят очередные дорожные карты по переходу на очередные "чудо-языки", которые, конечно же, уж в этот-то раз точно решат все проблемы на свете.

Это как это вывод типа auto без trailing return type работает до генерации кода return?

Генерация исполняемого кода для этого не нужна. Выполнение каких-либо оптимизаций тем более не нужно. А уж в случае если возвращаемый тип известен из объявления (даже если он шаблонный, как в примере из этой статьи), то внутри функции может быть вообще практически что угодно.

Паника есть паника, она не предусматривает обработки, только посмертный дамп.

Глупости. Вот пример того, как в коде на расте используют панику как прямой аналог исключений (в данном случае для отмены запросов).

А про Python, C#, Java, JS это вообще не аргумент, это все legacy из 90-х с заложенными еще тогда очень модными заблуждениями, а по сути - ошибками проектирования - мало кто понимал в те годы, как на самом деле нужно в надежность и прочие моменты.

Ну да, дураки были, не знали, что обработчики со switch (errcode) из C образца семидесятых годов - это верх совершенства. Хорошо, что пришли вы, и развенчали их заблуждения.

Гипотетически компилятор натыкается на нестыковку, оптимизирует do_something() до return nullptr; и вуаля, decltype() возвращает вообще не то. Хотя никакого кода не генерируется и более того, в стандарте прямым текстом написано, что передаваемое выражение НЕ выполняется.

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

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

Да, в рантайме. И она их не производит (практически как в моем примере с if (false) {...}). Слово "поведение" в словосочетании "неопределенное поведение" относится к поведению виртуальной машины C++, которую описывает стандарт, при выполнении сгенерированного компилятором кода. Нет выполняемого кода - нет и виртуальной машины, которая может "неопределенно себя повести". Не нужно распространять понятие UB на компилятор, там у него есть свои заморочки типа IFNDR, но это уже совсем другая история.

UB - это runtime concept, а не compile-time. Пока код с UB не исполняется в реальности, UB не существует. Например, здесь нет UB:

if (false) {
  int *i = nullptr;
  *i += 1;
}

Здесь UB может быть, а может и не быть:

int incr(int x)
{
  return ++x;
}

Когда пишется код типа такого:

T x;
decltype(do_something(x)) y;

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

Мой основной посыл был в том что 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 будет пустая структура, то из-за ее ненулевого размера никаких специальных приседаний с ней не требуется совершенно.

Information

Rating
3,088-th
Registered
Activity