Comments 238
Отличная статья!

Спасибо. Читал в оригинале, поэтому пролистал — вроде все ок ))))
Забавно, что автор перед публикацией тоже попросил прочесть, поправил пару ошибок и сказал «вроде все ок» :) Оказывается, он хоть и не пишет по-русски, но тексты понимает!
Вот это вот все сверху написатое «на C++20» — это на самом деле отличная лакмусовая бумажка. Если вдруг оказывается, что от вас на собеседовании хотят чего-то в этом духе — это отличный повод задуматься «а надо ли оно мне?» и молча под шумок слинять.

Ну, у меня в резюме написано, что я шарю в template/constexpr metaprogramming и в «C++17 and upcoming 20 standard». Как ещё это проверить?


Да и работать с задающими такие вопросы людьми при прочих равных интереснее, чем с теми, кто этого не знает. Я бы скорее пошёл туда, где у меня просят написать такое, чем, не знаю, auto_ptr.

В интернетах ходит шутка о стадиях развития программиста на C/C++. Я помню, как был на средней стадии где-то лет 16 назад.


У нас контора сейчас находится где-то примерно между 4 и 5 этапом.
Большинство программистов — даже миновав стадию 3.
А часть вообще смотрит в сторону js.

Реально с++ развивается куда-то не туда.
Появляются полезные штуки, но ещё больше полезных — не появляется. Даже в перспективе.
Зато вместо этого ширится и глубится «шаблонная магия».
Или как вариант, вы становетесь старпёрами :-) Как отследить этот момент и различить — когда технология действительно свернула куда-то не туда, и когда просто ты постарел и стал скучным?
Становимся конечно.

Только это не отменяет того, что в языке не появляются нужные для работы вещи.
Т.е. речь идёт не о субъективной «скучности» языка, а об объективной «нужности».
C++ становится интереснее, выразительнее наверное.
Но… не становится более нужным.

Ну вот ренжи решают вполне конкретную проблему, на этом примере — переиспользуемость, избегание дублирования, разделение сущности и алгоритма обработки. У вас нет такой проблемы? Есть ещё некая проблема записи в "функциональном стиле".


А какие фичи бы вам хотелось иметь, что вам нужно согласно критерию "объективной нужности"?

Хочется двоичной совместимости — как в C.
Хочется нормальной интроспекции.
Т.е. хочется больше фич в runtime, а у c++ все новые навороты — в compile-time.
Хочется двоичной совместимости — как в C.

Стандарт С что-то говорит про двоичную совместимость?


Хочется нормальной интроспекции.

Так интроспекция будет выражаться через компилтайм-интроспекцию, для котоой шаблонной магии пока недостаточно (constexpr for, например). И это как раз вполне укладывается в рамки неплаты в рантайме за то, что не используешь.

Не знаю что стандарт С говорит о двоичной совместимости, но на ней уже лет дцать построена и работает тьма проектов.
А с c++ ты даже за собственный проект не можешь ручаться: будет ли подключаться какая-нибудь СВОЯ библиотека при смене версии компилятора.


Про compile-time интроспекцию не совсем понятно. Интроспекция же это получение информации о внутренней структуре объекта в runtime. Этого не хватает.

А с c++ ты даже за собственный проект не можешь ручаться: будет ли подключаться какая-нибудь СВОЯ библиотека при смене версии компилятора.

Последний раз, когда ломалась двоичная совместимость — при переходе в gcc 4 на gcc 5, когда по новому стандарту потребовалось изменить устройство некоторых вещей в STL. Да и то, по факту это можно было обойти, определив соответствующий дефайн.


Про compile-time интроспекцию не совсем понятно. Интроспекция же это получение информации о внутренней структуре объекта в runtime. Этого не хватает.

Рантайм вынесен на библиотечный уровень. Условно, предполагается, что будут какие-нибудь библиотеки, где будут регистрироваться нужные для рантайм-интроспекции классы. А компилтайм-интроспекция позволит соответствующие рантайм-описания построить.

Ладно, давайте так:
Могу я взять либу, скомпилированную неизвестно кем неизвестно чем (но из c++ исходника), создать объект класса, который она экспортирует и пошариться по его внутренностям, повызывать методы, пообращаться к переменным?

Не можете и не сможете. Это же, получается, надо создавать рантайм-представление объектов для вообще любых классов. Оверхед, который редко когда нужен. Комитет по стандартизации на это не пойдёт никогда.

Замечу, оверхед — по памяти.
В плане производительности, если тебе это не надо, то ты "за это не платишь".
Более того, это можно было бы вообще отключать опциями компиляции.
Тогда выдавалась бы ошибка, что интроспекция недоступна.


А так, c++ остаётся эдаким средством строительства высокоэффективных "кирпичей" (монолитов). Вся гибкость языка — в compile-time, а если нужно универсальное взаимодействие на двоичном уровне, то… C-style интерфейс. И это частично, неуклюже так, прикроет только один вопрос.

Замечу, оверхед — по памяти.

А значит, и по производительности, если раньше у меня был мелкий объект на 16 байт, которых четыре в кешлайн влезало, то теперь там куча какой-то ерунды.


Более того, это можно было бы вообще отключать опциями компиляции.

Для одной структуры данных? Для всего TU?


Тогда выдавалась бы ошибка, что интроспекция недоступна.

Где, в рантайме? А в чём разница по сравнению с предлагаемым решением, только opt-in вместо opt-out?

Информация о внутреннем устройстве это всё-таки принадлежность класса, а не объекта. Так что объект как был маленьким, так и останется.
И — да: отличие в том, что интроспекция по умолчанию включена, а не отключена, а самое главное — стандартна, а не то что один программист использует одну библиотеку, другой — другую.

Так что объект как был маленьким, так и останется.

И даже указателя на описание класса нет?


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

Да, какая-то отсылка на класс в объекте должна быть, Вы правы.
Но это всё-таки указатель, а не массивная структура данных.

Вспомните, что таблица виртуальных функций тоже по умолчанию не генерируется.

В случае невиртуального класса можно добавить ещё один невиртуальный метод, который будет давать доступ к "классу объекта" с информацией о полях и методах. Конечно, если указатель на невиртуальный класс скастовать к void*, то не получится восстановить информацию, но это следствие гибкости С++ и с этим ничего не поделать. Информацию о полях и методах класса можно будет положить куда-нибудь в секцию констант рядом с описаниями других классов — и если к ним никто не будет обращаться, то, возможно, они даже в память не будут загружены. Единственный минус — бинарники будут больше.

Очевидно, такой метод будет давать описание только статического типа объекта, чего, вероятно, для практических применений недостаточно.
Можно до посинения цитировать стандарты, но C предлагает вменяемый ABI, а C++ нет. Как-то так…
Коллеги тут недавно рассказывали. Есть родная система А. Понадобилось интегрировать её с чужой системой Б. Для интеграции система Б отдает исходники SDK который собирается на стороне системы A и через него общается. Трахались какое-то время но в конце-концов подружили А и Б и все заработало ОК. Да, обе естественно на C++ (мы же не хипстеры какие это кровавый прод).

Через некоторое время систему А решили перевести на модерн С++. Ну в самом деле, GCC 4.x, C++98 и все такое уже почти 10ть лет. Доколе? ОК, взяли последний GCC (что мелочиться? чтобы сразу C++17) немного потрахались но перевели. Заработало. И стало вдруг всем тепло уютно и сухо.

Но вот незадача. Система Б без которой уже никуда под модерн С++ жить отказалась наторез. Даже собираться. Отказаться от ней нельзя. Поменять её нельзя. Как-либо повлиять на неё нельзя. В общем что дали с тем и живите.

ОК. Решили — а давайте поженим нашу А уже под модерт С++ с ихней Б собранной тем, древним, от мамонта. Собрали. И даже успешно слинковали что всех удивило. Но вот незадача: начало все валиться к чертям собачьим. Патамучта что-то там не так с разницей реализации в std::string и прочими мелкими ништяками STL. А они задействованы в API между А и Б.

В общем, пока не обрезали API почти до уровня plain C — ничего не заработало. А ведь казалось бы ну что нам стоит? Есть два проекта оба на С++ оба вроде почти вменяемые даже оба в исходниках — взять и поженить! А. Шас…
Перефразируя сказку о рыбаке и рыбке: "… много ли в _GLIBCXX_USE_CXX11_ABI корысти?!...", если для его соблюдения всеми нужными зависимостями и самой системой надо наизнанку вывернуться через накрест лежащее подпространство? По моему опыту, конечно.
А какие фичи бы вам хотелось иметь, что вам нужно согласно критерию "объективной нужности"?

Хочу адекватные модули, но они, боюсь, принципиально нереализуемы с имеющимся набором констрейнтов.

Вброшу свою ложечку. Судя по https://old.reddit.com/r/cpp/comments/akihlv/c_modules_might_be_deadonarrival/, эти самые констрейнты заключаются в "шоб мы могли свой код времён Кеннеди перенести в новый стандарт в 2 строчки".
Т.е. module lookup хотят сделать зависимым от содержимого файлов (а там имя модуля может задаваться в зависимости от макроссов, упс...) просто потому, что, грубо говоря, есть ОС, которая не умеет больше одного слоя директорий в своей ФС. Или ОС, которая не знает, что такое расширение. Или в байте 9 3/4 бит...

Да, и поэтому тоже.

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

P.S. Блин, не долистал до конца ветки, уже сказали.
На самом деле все просто. «Не туда» это когда для того чтобы впсисать концепцию в язык приходиться измудряться со скобками. Даже в этой статье пример c# и linq. Хорошо читаемый и понятный код на шарпах… Представляю как будет выглядеть linq на с++.
«Золотые, бессмертные строки Виталия Светославовича Луговского как нельзя лучше подходят к моменту»
… Кембриджским батоном лямбдоты.
Я функтором аппликативным
Eval, eval, eval, eval
UFO landed and left these words here
Проблема в том, что этими фичами могут пользоваться другие разработчики, код которых приходится читать.
Этими фичами также могут пользоваться разработчики библиотек, которыми приходится пользоваться. И никакое ревью тут не поможет.

Это меня в Scala в свое время очень выбешивало. Я пытался использовать ее просто как java-с-плюшками, но постоянно натыкался на то, что популярные в Scala-мире библиотеки делают даже простейшие вещи какими-то очень сложными и неочевидными фичами языка, и мне волей-неволей приходится это все разбирать и изучать, чтобы их использовать.

С С++ я распрощался уже лет пять назад, и рад этому. Уже тогда это был перегруженный фичами язык с переусложненным синтаксисом, во что он там сейчас превратился мне страшно подумать.
Хотя вам просто дают дополнительные возможности, которые следует применять там, где это оправдано.
Осталось только узнать, какие фичи нужно использовать, и когда они оправданы. Тому кто варится в плюсах нонстопом последние лет 10 это может и не сложно. А я вот на них около 6 лет почти не писал, и когда недавно решил чуть освежиться и открыл cppreference.com, у меня глаза на лоб полезли от объема информации. Просто чтобы перечислить все новые термины, фичи, инструкции и функции нужно с десяток A4 газетным шрифтом. Каким образом новичку вообще определить, какую из них стоит изучить, а какую просто пропустить, не входя в рекурсию? Нанимать сеньора и проходить с ним по всему списку, чтобы он отметил все, что реально используют на практике и могут спросить на собеседовании?
Ну что ж, ещё один гвоздь в крышку гроба C++. Хорошо, что теперь появилась достойная альтернатива — Rust. В этой статье открыли (не в первый раз) проблемы C++, которые не решаются годами. И чем больше таких статей, тем быстрее люди поймут, что наиболее эффективным и безболезненным улучшением C++ для них будет переход на Rust.
Ну вообщет эта проблема решается прямо сейчас. Вы даже можете всех участников легко и просто найти в интернетах, в Твиттере. Важно только не проходить мимо. Если вы сами не поучаствуете в обсуждении, то в конце концов всё решат за вас другие люди — и возможно тем способом, который вам не нужен.
К сожалению решают «другие люди». Мы можем только «рублём/долларом проголосовать» за компилятор и своим открытым кодом без этого нового уродства (личное мнение).
Очень рад увидеть статью, в которой вижу единомышленников. Спасибо.

А чо сразу рублём-то. Может, вначале пообщаться с авторами Стандарта? Или свою фичу подать на рассмотрение? У нас в России, кстати, есть три человека из Комитета. И один из них будет выступать на ближайшей C++ Russia.

Ну как сказать. Мне вот не понравилась вполне конкретная мелочь в языке, то что в chrono теперь есть типы и month и months и они значат совершенно разное)
я написал, предложил именование которое меня устроит, antoshka перевел и написал автору либы. Автор либы ответил что он лучше знает, и если у нас в голове что-то путается, это наши проблемы =) Дальше желание «улучшать стандарт С++» у меня пока отпало… если только сильно не припрет)
А можно более подробно? Потому что я уже боюсь, что я сам заранее запутался ))))
Почитал, мозг взорвался. Могу сказать одно — на таком языке с такими пропозалами (я не про Ваше уточнение — оно как раз по делу) я не хочу писать…
month — это момент времени, конкретный месяц; months — это интервал времени, разница между двумя моментами. В естественном языке то же самое. В чём тут путаница, и каким было ваше предложение?
А, т.е. вопрос не к разнице типов, а к неоднозначности определения интервалов «месяц» и «год». Я согласен, что проблема есть, но я лично бы вообще не вводил в chrono неоднозначные интервалы вроде months/years и остановился бы максимум на weeks. Пусть пользователь (или дополнительная библиотека) сам решает, какой именно год (календарный, тропический, сидерический) или месяц (календарный, средний, лунный) ему нужен в каждом конкретном случае.

То есть автор пропозала потратил время, ответил вам развёрнуто что, где и как, а вы представляете это "мне автор ответил, что он лучше знает". Я верно вас понял?

какой же это гвоздь? Это просто реакция на слишком заумные предложения к стандарту. Мне лично некоторые предложения ranges понравились (сами интервалы — ranges), хотя эти представления (view) и может-быть-представления (maybe_view) я тоже считаю непонятыми и переусложненными.

Думаю, что Rust недостаточно плох, чтобы стать действительно популярным и составить конкуренцию C++.

Когда -то я думал «вот дураки, пишут на чистом Си до сих пор — сами себе отрубают возможности». А на самом деле на С++ простую задачу часто стараются решать именно как написано самом верхнем примере, когда в этом нет необходимости, когда в случае чего можно поменять две строчки простого и понятного когда. Так что получается пишущие на чистом Си как раз умные, отрубая возможности которые редко применяются, мешают распространению быдлокода.
Да, не всё идеально в Си. Тот же Паскаль сильно бьёт по рукам за использования просто Pointer где не надо, а указатель на указатель вообще редкость(но бывает). Ещё можно отметить использование хака include вместо нормальной модульности. Ну что же, нет в мире совершенства.
Вы из одной крайности в другую бросаетесь, на самом деле синтаксиса «си с классами» ревизии эдак C++99 было достаточно для всего, и он был весьма удобен и прост, в отличие от голого си, где даже нет RAII. Выбирать голый си вместо этого, увеличивая объем кода раза в 4 и увеличивая вероятности внесения ошибок это действительно как минимум странно.
Вообще лучше иметь какую-то возможность, чем не иметь, вас же никто не заставляет всем этим пользоваться для любой задачи, в этом и прелесть, можете писать в стиле С, если так по вашему проще, а можете какие-нибудь лямбды с ренджами завернуть при случае, если это сократит код в 10 раз.
Дак собственно и проблема что в «Си-с-классами», который сильно условно «объектно ориентированый» и не очень «метапрограмированный» пытаются натянуть еще больше объектно ориентированности и метапрограммируемости. Все косяки плюсов на которые кивают опоненты из разряда «агааа вот как криво реализована эта фича, множество ограничений и UB». Ну дак новые стандарты кривости не исправляют, максимум подпирают костылями, а плюсом втыкают еще грабли… 99 стандарт «жил» 12 лет. а 11 стандарт только 3 года, и отнюдь не из за того что за 3 года «придумали новые хорошие плюшки». Зачем это делать лично мне непонятно.

Хороший пример, это собеседования… Когда вместо написания кода просят пояснить чем make_shared отличается от конструктора shared_ptr. Зачем forward, если есть move и прочее. Т.е. просят понимания ньюансов языка. При этом вопросы про ньюансы работы всяких «вызов виртуального метода в конструкторе» — никуда не ушли.

Идеология C++ заключается в том, что "ты не платишь за то, что не используешь". Никто не заставляет писать все в стиле C++ 20, выбирайте по месту те механизмы из наличия, которые в данном случае с вашей точки зрения будут удобнее и нагляднее. А безумный лапшеобразный код одинаково легко пишется на любом более-менее развитом языке, это просто вопрос квалификации пишущего. Если человек неграмотный, то никакой Rust тут не спасёт.

Так часто приходиться не самому писать, а чужой код читать. Когда сам писал, то сам не понимал чистых сишников. Потому, что без классов всё же хуже, чем с оными.
Иногда все же платишь, просто включение флага C++17 может заметно замедлить компиляцию, как показывает этот пост.
ты не используешь, а библиотека которую ты подключил, его требует. Например fmt какой-нибудь (я знаю, там сейчас C++11 вроде минимум, но представьте что это какая-то другая либа). И хоба, даже если вы либу в одном файле не включили, все равно его компиляция замедляется (извращенства вроде выставления набора флагов для каждого файла не предлагать)
Ну это уже фактически «используешь», если библиотека собирается из исходников.
Хотя, кмк, собирать библиотеку отдельно, а потом уже линковать было бы неплохим решением.

Идеология C++ заключается в том, что «ты не платишь за то, что не используешь».
Но в итоге все равно платишь, например когда код стал компилироваться дольше, а IDE стала монструозно тяжелой и медленной, но ты не можешь взять старую, потому что в проекте кто-то где-то использовал новую фичу или либу с таковой.
Если в проекте «кто-то использовал» что-то, то, соответственно, уже нельзя сказать, что это что-то «не используется», а раз это что-то используется, то за это надо платить, в том числе временем компиляции тех модулей, где это используется. Другое дело, что нужно думать, что стоит тащить в проект, а что нет, должно быть какое-то элементарное review кода любителей тащить в проект «новые либы с новыми фичами».
Плата начинается с момента выбора современной IDE, а есть в коде что-то новое или нет это уже не важно. Смысл был в том что использовать старые IDE уже затруднительно, особенно если работаешь над коллективным проектом, и не всегда есть возможность выбора.
Давайте отделять мух от котлет. Писать сколько угодно сложный код можно хоть в Notepad'е. Другое дело, что для повышения удобства этого процесса используют навороченные IDE, они действительно парсят ваш код для работы всяких IntelliSense, но они не инстанцируют шаблоны, не инлайнят код, не занимаются вычислениями всяких constexpr и не оптимизируют потом все это. Я хочу сказать, что с C99 кодом такая IDE будет работать вряд ли сильно быстрее, чем с C++20 кодом (имеется в виду сам процесс писанины, а не сборки).
Я хочу сказать, что с C99 кодом такая IDE будет работать вряд ли сильно быстрее, чем с C++20 кодом
В этом то и дело. Даже если писать C99 код, необходимость поддержки IDE новых стандартов плюсов с кучей их фич ведет к ее монстеризации и замедлению. И с каждым новым стандартом это сказывается все сильнее и сильнее. При этом не всегда можно просто взять старую IDE, даже если пишешь на C99. Возвращаясь к изначальному утверждению, мы платим в том числе за то, что не используем, но за потенциальную возможность это использовать.

Всякие возможности рефакторинга и навигации существуют параллельно со всем этим.

Я как раз таки сильно сомневаюсь, что "монстеризация и замедление" и "поддержка новых стандартов C++" так уж связаны. Вон новые версии Скайпа не поддерживают ни одного стандарта C++, а все равно еле ворочаются на средненьком ноутбуке пятилетней давности, потому что Электрон, вот это вот все :) Сейчас вообще тенденции в разработке ЛЮБОГО ПО направлены в сторону "монстеризации и замедления" (под лозунгом "ускорим разработку ценой скорости работы готового продукта", что, кстати, хорошо видно по некоторым комментариям к этой статье), а IDE просто следуют в общем тренде.

Конечно, не одними стандартами IDE жирнеют, но свою лепту они вносят однозначно. Особенно это касается поддержки всех этих новых фич IntelliSense'ом.
но они не инстанцируют шаблоны
не занимаются вычислениями всяких constexpr

Вообще-то это плохие IDE, если они этого не делают. Инстанциировать шаблоны и вычислять constexpr нужно для адекватного Intellisense (и на самом деле даже для адекватной раскраски кода).

Ну не знаю — не знаю. IDE именно с IntelliSense (типа Visual Studio) у меня сейчас под рукой нет, есть Qt Creator, он, конечно, выводит типы в случаях типа такого:

constexpr auto fn(int k)
{
    return k + 1;
}

constexpr auto var = fn(3);


то есть при наведении мышки на var показывает, что тип у него int, но нет никаких признаков, что он пытается именно вычислять значение var.

Как насчёт чего-то вроде


std::conditional_t<fn(3) > 0, int, char> foo = 10;

?


Ну и подсветить это красненьким, если выбранная ветка будет несовместима с инициализатором? Например, если вместо int написать std::string.

Ну вообще да, действительно вычисляет. Только скобочки нужны:

std::conditional_t<(fn(3) > 0), int, char> foo = 10;


Так показывает int, если ">" заменить на "<", показывает signed char. Но опять же, IDE скорее всего делает это гораздо реже, чем компилятор — например, в случае с constexpr auto IDE вряд ли будет утруждать себя лишними вычислениями. То есть скорее всего принцип «ты не платишь за то, что не используешь» все же работает и здесь.
Случайно наткнулся на некропост и захотел ответить. Например, Visual Studio страшно замедлилась после того как .NET команда протолкнула переписывание IDE на .NET, а потом пришлось долго оптимизировать её производительность.
Согласен, сознательное ограничение в языковых средствах имеет смысл.
это свежевыдуманный синтаксис, потому что в стандарте C++ корутин нет

Это не свежевыдуманный синтаксис, а синтаксис из актуального Coroutines TS

Это мой косяк, ща поправлю. В оригинале было написано «tentative syntax», каламбур прошёл мимо меня, не оглядываясь.
Прочитав блогпост, я думаю, что все это притянуто за уши. Этот пример с пифагоровыми тройками привели, чтобы показать, что есть в ranges и как это можно использовать. Но кто-то подумал, что так нужно писать код с приходом нового стандарта?
А в реальном проекте как это всё будет? Отнюдь я относительно мало взаимодействую с плюсами по работе, но кмк, реальный проект с этим будет собираться не сильно медленнее, а при умеренном использовании даже будет более читабильно.
А пример из разряда, что будет если взять все известные паттерны и соединить их вместе.
Понимаю, что оффтоп, но то же самое на Питончике делается совсем красиво, просто и наглядно:
>>> def triples():
    z = 0
    while True:
        z += 1
        for x in range(1, z):
            for y in range(x, z):
                if x*x + y*y == z*z:
                    yield x, y, z

>>> # Потестируем
>>> import time
>>> def test_triples(n):
    t_beg = time.perf_counter()
    first_n = [triple for _, triple in zip(range(n), triples())]
    duration = time.perf_counter() - t_beg
    print(*first_n, sep='\n')
    print(f'{n} triples in {duration:0.4f} seconds')

>>> test_triples(100)
(3, 4, 5)
(6, 8, 10)
(5, 12, 13)
(9, 12, 15)
(8, 15, 17)
(12, 16, 20)
...
(65, 156, 169)
(119, 120, 169)
(26, 168, 170)
100 triples in 0.1836 seconds

Ну да, только программа на С++ работает в 180 раз быстрее. А если программа на С++ отрабатывает за час, скажем, то эквивалентный питончик будет работать 180 часов, если не дольше. Серебрянной пули нет и все такое.

Хоба!

# triples.pyx
def triples():    
    cdef int x = 0
    cdef int y = 0
    cdef int z = 0
    (...)

import time
import pyximport; pyximport.install()
from triples import triples

def test_triples(n):
    (...)

Было: 100 triples in 0.1393 seconds
Стало: 100 triples in 0.0009 seconds

Cython конечно не везде прокатывает, но зачастую очень выручает.

Ну да, но только Cython компилируется, притом С компилятором. Возможно, компиляция будет быстрее за счет того, что код транслируется в С, не в С++. Это если о времени компиляции говорить. А код, наверное, да, писать проще. Но я с Cython не работал, не знаю.


Я подозреваю, если писать что-то длиннее, чем такой скрипт, как в статье, вылезет куча проблем или подводных камней. Например, в С++ я могу сказать для каждой переменной: вот ты будь на стэке, а ты в куче. Могу вернуть из функции unique_ptr и не копировать результат (и памятью вручную не управлять). А в Cython, судя по документации, надо вызывать Сишные free и malloc для ручного управления памятью… Может быть, для большинства задач, где нужна производительность, в итоге проще сразу взять С++ (?).

Теоретически да, но есть нюансы. Скорость работы программы определяется не только скоростью отработки элементарных операций. Если разрабу не приходится воевать с инструментом, если среда к нему дружелюбна, то у него остаются силы поразмыслить над алгоритмом, над красотой реализации, над гибкостью решения и прочими такими как-бы не очень важными вещами. Глядишь, и придумается у него, как O(N^3) превратить в какое-нибудь O(N^2), и тогда его питонский код сделает C++ного как щенка.

Например, взглянув на приведённую выше функцию, можно заметить, что каждый раз делать умножения z*z и x*x не нужно, и поэтому:
>>> def triples():
    z = 0
    while True:
        z += 1
        z2 = z*z
        for x in range(1, z):
            x2 = x*x
            for y in range(x, z):
                if x2 + y*y == z2:
                    yield x, y, z

В результате вместо «100 triples in 0.1836 seconds» получаем «100 triples in 0.0974 seconds». То есть уже медленнее не в 180 раз, а всего в 100. На такой простой фигне. Если присмотреться, можно заметить, что третий вложенный цикл лишний. Игрек можно вычислять сразу, вот так:
>>> def triples():
    z = 0
    while True:
        z += 1
        z2 = z*z
        for x in range(1, z):
            y2 = z2 - x*x
            y = int(round(y2**0.5))
            if y >= x and y*y == y2:
                yield x, y, z

В результате мало того, что ещё в два раза разогнались («100 triples in 0.0498 seconds»), но и своё О() улучшили. На 10000 этот код у меня показывает такое: «10000 triples in 32.6228 seconds». Если сравнить с оригинальным алгоритмом на C++, наверняка Питон будет быстрее. Здесь можно возразить, что никто нам не мешает в C++ провернуть ту же каверзу. Но не всё так просто. В реальной жизни в реальных системах сишный код являет собой мегабайты жуткой лапши из классов, библиотек, шаблонов, выделений/освобождений памяти и прочего ужаса (сам этим занимался несколько лет кряду). Здесь не до всяких сопливых О(), здесь лишь бы не падало, и на том спасибо.

Да, но с тем же успехом можно и С++-ный код переписать таким же способом и он снова будет в 180 раз быстрее. Но проблема в том, что часто и после переписывания код все равно медленный, ну просто потому, что быстрее он быть не может. И тогда С++ снова на коне. Если б все проблемы с производительностью можно было решить, подумав над алгоритмом, С++ бы давно и умер, наверное. Да и numpy бы не писали как обертку над С-шным кодом, и Cython вот никто бы не разрабатывал.


А, насчет последнего параграфа. Если руками памятью не управлять (то есть никаких new/delete/free/malloc в коде), то частота падений устремляется к нулю, мне кажется. Ну и ворнинги чистить, чтоб undefined behavior не было. Насчет легкости подключения библиотек—тут вы правы. Но, очевидно, все еще есть много задач, где скорость работы программ перевешивает этот недостаток. Ну и все ждут-не-дождутся модулей :-)

Но, очевидно, все еще есть много задач, где скорость работы программ перевешивает этот недостаток.
Совершенно верно. Поэтому C++ тоже любим. Просто для каждого типа задач свой инструмент. Делать быстрый расчёт на плюсах — в самый раз, инструментальщина — тоже, а юзать их для бизнес-логики, особенно в какой-нибудь динамичной предметной области, где семь пятниц на неделе — чистое безумие. Описанное в статье мне показалось попыткой затянуть в стандарт С++ то, что поможет решать те задачи, для которых С++ вообще напрочь не предназначен.
Да, но с тем же успехом можно и С++-ный код переписать таким же способом и он снова будет в 180 раз быстрее. Но проблема в том, что часто и после переписывания код все равно медленный, ну просто потому, что быстрее он быть не может. И тогда С++ снова на коне. Если б все проблемы с производительностью можно было решить, подумав над алгоритмом, С++ бы давно и умер, наверное. Да и numpy бы не писали как обертку над С-шным кодом, и Cython вот никто бы не разрабатывал.

Да, только С++-ный код не получится так переписать, потому что время отведенное на задачу закончится.

Да, только С++-ный код не получится так переписать, потому что время отведенное на задачу закончится.

Да ладно, на питоне многие вещи писать быстрее, конечно, но не то чтобы прям сильно.

Ну если считать время на разбор "что тут пришло в виде void** a[]" то очень даже сильно, имхо.


Хотя есть компромиссы и получше, для меня это C#/Rust. Но и у питона ментальная модель для понимания кода в разы проще. https://habr.com/ru/company/jugru/blog/438260/#comment_19690430 например, например.

Когда в каждой компании свои умные указатели — это не сильно лучше. В хроме свои, у мозиллы свои, у `opencv` тоже свои… Это только из того, что я сам видел. «Да они все похожи, переучиться 5 наносек» — а вот не сказал бы.
Свои умные указатели на свои динамические массивы своих строк. Сразу в нескольких библиотеках в проекте…
В реальной жизни в реальных системах сишный код являет собой мегабайты жуткой лапши из классов, библиотек, шаблонов, выделений/освобождений памяти и прочего ужаса (сам этим занимался несколько лет кряду). Здесь не до всяких сопливых О(), здесь лишь бы не падало, и на том спасибо.


Мне кажется вы несколько перегибаете. Переписывание именно мегабайтов кода на статически типизируемом языке (С++, например), как правило, не в пример легче переписывания кода на динамически типизируемом языке (Python). Особенно если на это накладывается условный NumPy с его вольными правилами бродкастинга.
Проблемы с утечками, по большей части, решаются использованием инструментов типа valgrind, и соблюдением правил выделения/освобождения ресурсов.
Можно еще ускориться на вычислении 10000 троек раз эдак в 15 )))

from math import sqrt

def triples():
    c = 4.
    while True:
        a, b = sqrt(c * 2. + 1.), c
        c += 1.
        сс = c * c
        while a < b:
            if a.is_integer():
                yield int(a), int(b), int(c)
            b -= 1.
            a = sqrt(сс - b * b)
Ну да, только программа на С++ работает в 180 раз быстрее.
Да, но не потому что ее код в примере выглядит уродским и трудночитаемым. То же самое и в обратную сторону.

Можно же добавить в быстрый компилятор возможность писать красивый код.
На рэнджах и итераторах раста.

fn main() {
    (0..)
        .map(|z|
            (1..=z)
                .map(move |x| (x..=z)
                .map(move |y| (x,y,z))
                .filter(move |(x,y,z)| x*x + y*y == z*z))
                .flatten()
        )
        .flatten()
        .take(100)
        .for_each(|x| println!("{:?}", x));
}
Читабельность кода конечно просто зашкаливает :)

То, что написато в оригинале на C поймет практически любой школьник. То, что вы привели на расте — они ни чем не лучше, чем на «C++20». И там и там одинаково хреново.

Вы не мешайте в одну кучу задачи "вывести в цикле" или сделать итератор/генератор. Результат конечно похож, но это всё же не одно и тоже. Эти абстракции как раз для того, чтобы решать проблемы, описанные для C кода в статье.


Уверен, что на большинстве языков (включая раст) — просто через for будет как на C. Но итератор или генератор зачастую полезнее.


— добавлено --


simple-reusable.cpp уже является аналогом, но не совсем: внутри он другой. На расте это комбинация итераторов рэнджей, в cpp — ручное создание итератора с нуля, даже с ручным сохранением состояния вроде.

> Вы не мешайте в одну кучу задачи «вывести в цикле» или сделать итератор/генератор. Результат конечно похож, но это всё же не одно и тоже. Эти абстракции как раз для того, чтобы решать проблемы, описанные для C кода в статье.

Конечно с этим я согласен! Задачи разные. Я лишь заметил, что когда в очередной раз приводят вроде как спасительный код на серебряной пуле он почему-то на деле оказывается совсем другого цвета. Ни чуть не лучше вот этого всего что плавает рядом.
Признаться я не очень понял. Вы сами изначально сравнили некорректно, но в итоге сейчас написали, что всё равно вас не устраивает. Почему не устраивает или что не так — не объяснили. В целом это довольно обычная реакция на «непривычный синтаксис».
Вот то, что написано в оригинале на C, но на Rust, поймёт практически любой школьник:
fn print_n_triples(n: u32) {
    let mut i = 0;
    for z in 1.. {
        for x in 1..=z {
            for y in x..=z {
                if x*x + y*y == z*z {
                    println!("{}, {}, {}", x, y, z);
                    i += 1;
                    if i == n { return; }
                }
            }
        }
    }
}

Начал писать свой вариант, прежде чем увидел, что уже написано… Ну да ладно, раз уж написал


fn triples(n: u32) -> impl Iterator<Item = (u32, u32, u32)> {
    (0..n).flat_map(move |z| 
        (1..z).flat_map(move |x| 
            (x..z).map(move |y| 
                (x, y, z)
            )
        )
    )
    .filter(|(x, y, z)| x*x + y*y == z*z)
}

fn main() {
    for triple in triples(10000).take(100) {
        println!("{:?}", triple);
    }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9ea7684067d9ef90e78308116afbc333


Компилируется за 0.5 секунды, поиграть можно на годболте

Не знаю, меня наоборот этот синтаксис очень раздражает.


do x <- Just 1
   y <- Just 2
   return (x+y)

Для меня лично выглядит намного приятнее, чем


[ x + y | x <- Just 1, y <- Just 2 ]

Про планы сделать что-то такое не слышал. Мне вообще кажется, что специальный синтаксис для этого не нужон.

Ну, я, если честно, включал эти monad comprehensions, наверное, пару раз за всю жизнь. Последний раз — когда я работал со списками, но мне нужна была другая семантика для guard (бектрекал там что-то), так что да, это не сказать чтобы особо часто нужная фича.

Простите за оффтоп, но меня очень забавляет, что вы меряетесь скоростью работы программ, запущенных на разных машинах и в разных окружениях… и никого это не смущает :)

Я предлагаю мерить не скорость программ. Можно глянуть на ассемблер и прикинуть, что будет быстрее. Типа "ага, тут векторизовал, а тут нет, ну всё ясно".

Прошу прощения, но зачем вам ограничение до N? O_o
Тот же код, но с бесконечной последовательностью
fn triples() -> impl Iterator<Item = (u32, u32, u32)> {
    (0..).flat_map(move |z| 
        (1..z).flat_map(move |x| 
            (x..z).map(move |y| 
                (x, y, z)
            )
        )
    )
    .filter(|(x, y, z)| x*x + y*y == z*z)
}

fn main() {
    for triple in triples().take(100) {
        println!("{:?}", triple);
    }
}

Не так уродливо в примере из статьи, но совершенно не self explanatory, если человек не знаком с rust и map/filter подходом. Не говорю, что это плохо, возможно это лучший компромисс между универсально, быстро, надежно и читаемо. Может быть, стоит инвестировать в изучение таких патернов ради того, что бы избавиться от кучи более серьезных проблем.
Школьник и правда не поймёт, но должен ли?
Раз пошла такая пьянка, то Питон меня опять не убедил, даже скорее разочаровал своим синтакисисом. То же на C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Triples
{
    class Program
    {
        static IEnumerable<(int, int, int)> Triples()
        {
            var z = 0;
            while (true)
            {
                ++z;
                for (int x = 1; x < z; x++)
                for (int y = x; y < z; y++)
                    if (x * x + y * y == z * z)
                        yield return (x, y, z);            
            }
        }
        
        private static void TestTriples(int n)
        {
            var sw = new Stopwatch();
            sw.Start();
            var firstN = Triples().Take(n).ToList();
            sw.Stop();
            firstN.ForEach(i => Console.WriteLine(i));
            Console.WriteLine($"{n} triples in {sw.ElapsedMilliseconds} milliseconds");
        }
        
        static void Main(string[] args)
        {
            TestTriples(100);
        }
    }
}

...
(96, 128, 160)
(36, 160, 164)
(99, 132, 165)
(65, 156, 169)
(119, 120, 169)
(26, 168, 170)
100 triples in 6 milliseconds


А вот эта конструкция что делает? Спрашиваю потому что сходу не разобрал, а гуглить по скобкам бесполезно )
first_n = [triple for _, triple in zip(range(n), triples())]
Уж не ленива ли она? Тогда что мы меряли?
А вот эта конструкция что делает? Спрашиваю потому что сходу не разобрал, а гуглить по скобкам бесполезно )…
Уж не ленива ли она? Тогда что мы меряли?
Нет, не ленивая. Честно достаёт n элементов из генератора и складывает в массив. Полный аналог «Triples().Take(n).ToList();».
Механика достаточно простая. Функция zip создаёт итерируемый объект, сцепляющий две последовательности. Здесь мы сцепляем range(n), тупо последовательность n чисел с нескончаемой последовательностью triples(). Когда одна из сцепливаемых последовательностей заканчивается, zip финиширует. То, что выдаёт range(n), нам не интересно, и мы складываем это в мусорную переменную "_".
То же самое сделал бы такой код:
first_n = []
gen = triples()
for _ in range(n):
    first_n.append(next(gen))

Или такой:
import itertools
first_n = [triple for triple in itertools.islice(triples(), 0, 10)]

Ну, раз на до диезе музыка пошла, то вот и на фа диезе

open System
open System.Diagnostics

let triple = 
    seq {
    let mutable z  =  0
    while true do
        z <- z + 1
        for x in 1..z-1 do
            for y in x..z-1 do
                if x * x + y * y = z * z then
                    yield (x, y, z)            
    }

let gen_n_triples N = 
    let sw = Stopwatch()
    sw.Start()
    let pifa = triple |> Seq.take N |> Seq.toList
    sw.Stop()

    let pri_tri t = printfn "%A" t
    List.iter pri_tri pifa.[..5]
    printfn ". . . . ."
    List.iter pri_tri pifa.[N-5..]

    printfn "\n\t%d triples in %O milliseconds (%O ticks)\n" N sw.ElapsedMilliseconds sw.ElapsedTicks

[<EntryPoint>]
let main argv = 
    gen_n_triples 100
    0 

(3, 4, 5)
(6, 8, 10)
(5, 12, 13)
(9, 12, 15)
(8, 15, 17)
(12, 16, 20)
. . . . .
(36, 160, 164)
(99, 132, 165)
(65, 156, 169)
(119, 120, 169)
(26, 168, 170)

100 triples in 7 milliseconds (25844 ticks)


Интересно, что попытки оптимизации, вроде массива квадратов, чтоб не возводить всё время, приводили лишь к значительному увеличению времени выполнения.

А, мы тут языками соревнуемся?


module Main where

pytha :: [(Int, Int, Int)]
pytha = [(x, y, z)
        | z <- [0..]
        , x <- [1..z]
        , y <- [x..z]
        , x * x + y * y == z * z
        ]

main :: IO ()
main = print $ take 100 pytha

Тайминги:


  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    0.000s  (  0.002s elapsed)
  GC      time    0.000s  (  0.000s elapsed)
  EXIT    time    0.000s  (  0.008s elapsed)
  Total   time    0.000s  (  0.010s elapsed)

Т. е. программа за собой подчищает дольше, чем выполняется. И работает, кстати, в константной памяти, жрёт 72 килобайта. Мне даже думать не нужно было о том, чтобы так писать. yieldы какие-то...


А, код на питоне выполняется 0.13 с.

Если судить по вашим увлечениям, то это, должно быть Хаскель?

Однако, этот код заставил меня задуматься над тем, что может стоит глянуть в сторону Хаскеля и изучить его. Спасибо за приведённый пример. ;)

Да, вы верно поняли.


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

На windows компиляция и линковка haskell выглядят удручающе долго. Насколько я понимаю, все статически линкуется в exe файл, с размером в несколько мегабайт. Это чисто windows проблемы? Данные проблемы можно купировать флагами компиляции/линковки?


P.s. если я не ошибаюсь, то haskell полностью рассахаривает take 100 pytha в список троек, так как они вычислимы на этапе компиляции. Получается в примере посчитана производительность операции print.

На windows компиляция и линковка haskell выглядят удручающе долго. Насколько я понимаю, все статически линкуется в exe файл, с размером в несколько мегабайт.

Ну что вы начинаете, нормально же общались!


А если серьёзно, то да, линковка в хаскеле (в ghc, конкретнее) — больная тема, там худшее и от мира статической, и от мира динамической линковки.


Собирается у меня на линуксе быстро, но файл да, большой, 17 метров.


P.s. если я не ошибаюсь, то haskell полностью рассахаривает take 100 pytha в список троек, так как они вычислимы на этапе компиляции. Получается в примере посчитана производительность операции print.

Хорошее замечание!


Сделал, чтобы оно читало из файла вместо хардкода — те же 0.002 секунды.


Дополнительно взял и побенчмаркал конкретно функцию вычисления criterion'ом, чтобы не измерять скорость print и readFile — получил


benchmarking pytha/100
time                 968.8 ns   (966.9 ns .. 971.5 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 968.9 ns   (967.2 ns .. 972.2 ns)
std dev              7.781 ns   (4.271 ns .. 13.40 ns)
возможно на windows готовый бинарник проверяет антивирус, при компиляции go такое замечал
А если 100 забить не константой, а подавать на вход программы? А то, как тут уже сказали, есть подозрение, что оно вычисляет на этапе компиляции.
Впечатляет, конечно. Даже стало жаль, что Хаскель для моих задач совсем ни в жилу.

А что у вас за задачи? Просто Хаскель неплохо подходит для гораздо большего количества областей, чем принято считать.

Про Хаскель не скажу, а на F# делают это всё. Плюс, реактивная модель неплохо ложится на иммутабельность и прочую функциональщину.

А что нужно подчищать перед выходом такого, что операционка не подчистит?

Ну, например, прогнать GC, вызвать финализаторы.


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

Про время компиляции вот прям в точечку, вместо того чтобы сосредоточиться на действительно важном, они делают «интересное». С другой стороны использовать все эти головоломки, как и любые другие инструменты языка, нужно именно тогда, когда это действительно необходимо, а не потому что можем. Возможно пример с тройками как раз оказался из таких, и надо было привести задачу, которая без ренжей занимает 100 строк, а с ренжами — 50 и выглядит менее запутанно.
Здравый смысл никто не отменял. Пример с тройками был выбран, чтобы было во второй половине поста рассказать о всех изменениях. Конечно так не должен выглядеть код в продакшене. Наглядней было бы для каждой возможности привести какой-то кусок кода и показать как повысилась его читабельность благодаря новым функциям. А тут какой-то совершенно противоположный подход.
Друзья, простите за нескромный вопрос — почему изначальную проблему повторного использования нельзя решать обычным callback'ом? Сгенерировал тройку, дернул вызов — что там будет делаться — код генерации троек не интересует.

Как написали выше, пифагоровы тройки — не очень удачный пример. Вообще же вы подняли старую проблему push vs pull iteration. push-iteration, которая на коллбэках, делается проще — но гораздо хуже ведёт себя в композиции.

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

Хорошо что сейчас в языке уже есть лямбды с замыканиями, поэтому не надо создавать отдельную функцию void list_callback(void *arg); но все равно, логика приложения размазывается по коллбекам. И в вырожденном случае начинает напоминать классическое асинхронное приложение, где коллбеки ездят на коллбеках и коллбеками погоняют. Кстати, async/await и придумали для того, что бы держать логику в одном месте, а не в пяти коллбеках.

Поэтому да, вариант с корутиной из поста был бы лучше.
Как бы да, но с другой стороны представим, что мне нужен список троек. Значит мне надо создать пустой список, написать колбек, который добавляет тройку в этот список, дернуть генератор, передав ему коллбек (и список, возможно).

Так и на здоровье — обернем создание списка, вызов итератора в одну функцию/процедуру, пусть будет callback с context'ом или лямбда — кому как нравится.
Код получится простой, читаемый.

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

Не совсем. Если это одна функция с лямбдой, в которую все завернуто, то никакого размазывания нет. Четко видно, к чему лямбда относится.
Так и на здоровье — обернем создание списка, вызов итератора в одну функцию/процедуру, пусть будет callback с context'ом или лямбда — кому как нравится.
Код получится простой, читаемый.


С лямбдой — да, можно запихнуть в одно место. С классическом коллбэком-функцией — у вас часть логики будет в коллбэке, часть — в основной функции. Уже начинаются проблемы с читаемостью.

Но это лайтовый вариант. Теперь представьте что из коллбэка вам нужно вызвать другой итератор, который будет дергать другой коллбэк, а из него — третий. Как в предложенной задаче с тремя циклами.

Тут с коллбэками-функциями будет вообще мрак, ибо логика теперь будет размазана по куче функций. Хотя, программисты на JS как-то справляются же.

С коллбэками-лямбдами будет уже проще, но все равно, лямбда в лямбде уже попахивает извращением. Но опять же, в коде под Андроид, я видел еще и не такое.

Я не то что говорю, что коллбэки — это абсолютное зло. Я сам ими пользуюсь. Просто однажды я пытался поддерживать драйвер в линуксе, который весь был сделан на коллбеках — и это был ад. Плюс, веселья еще добавляла активная многопоточность.
С классическом коллбэком-функцией — у вас часть логики будет в коллбэке, часть — в основной функции. Уже начинаются проблемы с читаемостью.

Такое может быть, но надо смотреть конкретный вариант. Если функции относительно небольшие и коллбэк описан рядом с основной функцией, то это не такая проблема.

Но это лайтовый вариант. Теперь представьте что из коллбэка вам нужно вызвать другой итератор, который будет дергать другой коллбэк, а из него — третий. Как в предложенной задаче с тремя циклами.

Не вижу проблемы, потому что первый CB будет работать с (условно) ComposeAnotherList() функцией, а чем она там внутри пользуется, есть у нее свой (второй) CB или нет — уже не наше дело. Для нас это просто готовый кирпич, который мы используем. Нам важен результат, который мы запросили из первого CB.

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

Проблема в трансформациях. Типичная история: взяли что-то, пофильтровали, смаппили, смаппили, вызвали пару нестандартных комбинаторов, собрали вектор.


Делать это на коллбеках значит иметь ~10 уровней вложенности на ровном месте.


Разница примерно такая же, как асинхронность на async/await и a.then(x => ...)

Делать это на коллбеках значит иметь ~10 уровней вложенности на ровном месте.

Да, но каждый уровень относится к следующему как к готовому кирпичу, черному ящику. Какая разница, сколько там дальше будет уровней вложенности? (В разумных пределах, конечно, стек не бесконечный и так далее).

Потому что все это влияет на читаемость.


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


let a = [1_u64, 2, 3, 4, 5];
let result = a
    .into_iter() // хотим поитерироваться
    .map(|item| item.pow(2)) // возвести все числа в квадрат
    .flat_map(|item| once(0).chain(once(item))) // перед каждым числом дописать ноль
    .skip(1); // пропустить первый ноль (чтобы нули были между числами, а не перед каждым)

Ссылка на плейграунд


Предлагаете делать 4 вложенных коллбеков? Или развернуть цепочку вычислений в императивный цикл, где мы в одном месте играем, в другом нет, а третьим рыбу заворачиваем?

Потому что все это влияет на читаемость.

Совершенно необязательно. Никто же не мешает выносить «скучный» технический код, разбавляющий общий смысл деталями, в отдельную функцию.

Это, пожалуй, дело вкуса. Не далее чем год назад имел спор именно по поводу читаемости stream-стиля, не хочется повторять его по тем же аргументам.

Предлагаете делать 4 вложенных коллбеков?

Почему вы раз за разом упираете на вложенность? На каждом уровне мы работаем только со следующим, какая там дальше вложенность — никого не волнует.

В данном случае у вас будет один CB, имеющий доступ к входному и выходному спискам — для каждого элемента — возведение в квадрат, добавление к выходному списку и добавление ноля (или добавление в начале).
Всё, по окончании итерации — удаление лишнего ноля.
Совершенно необязательно. Никто же не мешает выносить «скучный» технический код, разбавляющий общий смысл деталями, в отдельную функцию.

Да нет. я хочу все перед глазами видеть. Вносить только какие-то законченые куски кода, особенно если они довольно большие. Тут ничего большого нет.


Почему вы раз за разом упираете на вложенность? На каждом уровне мы работаем только со следующим, какая там дальше вложенность — никого не волнует.

Потому что вводить миллион переменных и раздувать код в 3 раза тут совершенно не нужно.


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

А давайте пример, сравним, так сказать, подходы.




К слову про итераторы, ниже верно написали: в моем случае map реализовано в одной библиотеке, filter_map в другой, а skip в третьей. И все они собраны вместе благодаря общему интерфейсу. Как это будет работать в случае коллбеков, где авторы все по-разному сделают не представляю.


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

Да нет. я хочу все перед глазами видеть. Вносить только какие-то законченые куски кода, особенно если они довольно большие. Тут ничего большого нет.

Зачем, если можно сделать из отдельных блоков, каждый из которых — закончен и может гипотетически использоваться отдельно? Вам удобнее писать в stream стиле — пожалуйста. По мне, так читается не очень хорошо.

Вообще, интересный маятник с течением времени получается. От разбиения и сокрытия всего чего только можно и обратно к «давайте весь код будет здесь, одним куском, так нагляднее, но со stream'ами, чтобы не размазывать».

Потому что вводить миллион переменных и раздувать код в 3 раза тут совершенно не нужно.

Так не раздувайте. Вот у вас готовый push итератор, вот ваш custom коллбэк.

А давайте пример, сравним, так сказать, подходы.

В итоге получится, что ваш пример тоже недостаточно сложный, чтобы нехорошесть коллбэков (лямбд) была видна и далее будут дополнения «а если нам надо так, а если еще вот так и вот так». Но хорошо, давайте в псевдо-коде:
func buildSparseList(in inList) someListType
{
  someListType outList;
  new(outList);
  iterateList(inList, (listItem)=>[outList.Add(listItem.pow(2)); outList.Add(0)]);
  outList.trunc(outList.Capacity - 1);
  return outList;  
}

iterateList внутри — просто foreach с вызовом
listItem=>[] — лямбда, которая захватывает внешний контекст, включая outList. Если на чистых коллбэках — будет указатель на коллбэк и переменная контекста с cast'ом, как в старом С.

Я не говорю, заметьте, что ваш способ чем-то ужасен. Я говорю, что старый не так плох. Я бы мог писать и в потоковом стиле, и в императивном, но императивный мне нравится больше чисто субъективно.

Старый способ плох. Во-первых вы используете тот факт, что тип переменной один и тот же. Если у нас [1,2,3].map(|i| i as f64).map(|double| double.to_string()) то сделать так не выйдет — раз
У вас аллокации на ровном месте — два.
Куча изменяемого состояния, практически каждая строка мутирует переменные — три.


Зачем, если можно сделать из отдельных блоков, каждый из которых — закончен и может гипотетически использоваться отдельно? Вам удобнее писать в stream стиле — пожалуйста. По мне, так читается не очень хорошо.

Не знаю, кто скажет, что второй вариант читается лучше. Если есть такие, объясните, пожалуйста, как так получилось. Мне было бы крайне любопытно.

Как интересно. Случайно набрел на этот комментарий, оставшийся без ответа год назад.

Во-первых вы используете тот факт, что тип переменной один и тот же.

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


Но с разными типами данных тоже не должно быть проблем: мы захватили снаружи i и в лямбде можем как угодно его привести, преобразовать в строку и работать с итоговым списком строк. Это будет перегруженная функция или шаблон.

У вас аллокации на ровном месте — два.

Это зависит от конкретной платформы и типов данных, но да — согласен, где-то это может быть проблемой. Но если данные — POD или список не делает копирования (а аллокация для создания элемента итогового списка нужна все равно), то это не проблема.

практически каждая строка мутирует переменные — три.

Да, но это не проблема сама по себе, поскольку это функция data in-data out, побочки здесь нет, в рамках определенного контракта, разумеется.

Но в целом — согласен, если взять более сложную цепочку неоднородных преобразований, stream вариант может быть лучше. Я опирался в рассуждениях на простое преобразование, сродни описанному в статье.

Потому что изначальный вопрос шире. Давайте переиспользуем комментарий Александра Есилевича из моего фейсбука:


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


В реальности ranges — это замена итераторам, которая позволяет определять свои собственные итераторы в несколько строк кода, вместо нескольких десятков строк кода на голом C++ или десятка строк с использованием Boost.Iterator. Для того, чтобы понять полезность ranges, надо сравнить код, определяющий свои кастомные итераторы, с аналогичным кодом на ranges. Кто вспомнит, когда в последний раз писал свои итераторы на голом C++? Я пожалуй в последний раз так делал никогда, только с использованием Boost.Iterator.


Вот очень легко формулируемая задача, которая решается в пару строк на ranges, или в десяток строк на Boost.Iterator, а на голом C++ ее вообще обычно не решают никак, потому что сильно сложно.


Задача формулируется так. Есть класс, содержащий внутри коллекцию std::unique_ptr. Надо выпихнуть наружу возможность пользователям класса перебирать объекты, содержащиеся в коллекции, но без возможности изменить их. Т. е. вернуть пару итераторов, у которых value_type — это "const SomeObject*" (можно и просто "const SomeObject", в зависимости от конкретной задачи).

В реальности никто эту задачу обычно по-правильному на голом C++ не решает. Я обычно использую Boost.Iterator, если есть возможность, но это очень напрягает. С ranges все элементарно и просто."

> Кто вспомнит, когда в последний раз писал свои итераторы на голом C++? Я пожалуй в последний раз так делал никогда, только с использованием Boost.Iterator.

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

> Вот очень легко формулируемая задача, которая решается в пару строк на ranges, или в десяток строк на Boost.Iterator, а на голом C++ ее вообще обычно не решают никак, потому что сильно сложно.

Приведенный ниже код случайно не устроит отца русской демократии :-? Вот только что поняв задачу буквально накидал за 10ть минут:
#include <iostream>
#include <memory>
#include <list>

struct Item
{
    Item(const Item&) = delete;
    Item &operator = (Item&) = delete;

    explicit Item(int value) : value(value) {}

    int value;
};

inline std::ostream &operator << (std::ostream &os, const Item &item)
{
    return os << std::dec << item.value;
}

template <class T>
class Storage {
private :
    using PtrList = std::list<std::unique_ptr<T>>;

public :
    void addItem(std::unique_ptr<T> item)
    {
        items_.push_back(std::move(item));
    }

    class const_iterator {
    public :
        explicit const_iterator(typename PtrList::const_iterator it) : it_(it) {}

        const T& operator * () const noexcept
        {
            return *it_->get();
        }

        bool operator != (const const_iterator &src) const noexcept
        {
            return it_ != src.it_;
        }

        const_iterator operator ++ () noexcept
        {
            it_++;
            return *this;
        }

    private :
        typename PtrList::const_iterator it_;
    };

    const_iterator begin() const noexcept
    {
        return const_iterator(items_.begin());
    }

    const_iterator end() const noexcept
    {
        return const_iterator(items_.end());
    }

private :
    PtrList items_;
};

int main()
{
    Storage<Item> storage;

    for (int i = 0; i < 10; i++) {
        auto item = std::make_unique<Item>(i);
        storage.addItem(std::move(item));
    }

    for (const auto &item : storage) {
        std::cout << "Item " << item << std::endl;
    }

    return 0;
}

А вот такое накидал Александр (в том же треде на ФБ) мгновенно. Не нужно знать, как писать кастомные итераторы. И в отличие от публичного наследования итератора такой код нельзя хакнуть и поменять элементы — а это между прочим условие задачи.


#include <vector>
#include <memory>
#include <range/v3/view/transform.hpp>

class MyObject {
public:
    int x = 20;
};

class MyCollection {
public:
    auto objects() const {
        auto fn = [](const std::unique_ptr<MyObject> & ptr) { return const_cast<const MyObject*>(ptr.get()); };
        return objects_ | ranges::view::transform(fn);
    }

private:
    std::vector<std::unique_ptr<MyObject>> objects_;
};

int main() {
    const MyCollection col;
    for (auto && obj : col.objects()) {
        int a = obj->x;
    }
}
> И в отличие от публичного наследования итератора такой код нельзя хакнуть и поменять элементы — а это между прочим условие задачи.

Не совсем если честно понял это замечание. Каким образом в приведенном мною случае вы сможете поменять возвращенный объект в коллекции :-?

PS: Естественно не прибегая к const_cast-у т.к. это грязный хак по-определению. Решается административными средствами в процессе code review «с последующим лишением премии».
return objects_ | ranges::view::transform(fn);

Ох уж эта любовь перегрузить операторы на каждый случай… Если по имени метода можно догадаться, что он конкретно делает, то что делает objects_ | transform можно понять только зарывшись в контекст.

Да тут собственно хоть используй оператор, хоть не используй, все равно ничего не понятно будет, если читающий не понимает что такое ranges и как они используются. От того, что будет «return ranges::view::transform(objects_, fn);» ничего принципиально не изменится. С итераторами на самом деле все точно так же: не понимая концепции итераторов код понять практически невозможно.

Да не, аналогом было бы скорее что-то вроде


return  ranges::view::create_view(objects_, ranges::view::transform(fn))

и так действительно понятнее.

Версия ianzag выглядит намного более простой и прямолинейной по моему личному мнению. В ней мало что происходит за сценой, а объем не настолько уж велик. Есть подозрение, что и по времени компиляции намного выигрывает.
В C# реализация перечислений по сути сводится к одному интерфейсу, как по мне это идеальный баланс между "явностью" и затратами разработчика. Конечно в C++ сложнее такое реализовать, учитывая констатность, смарт-поинтеры и прочие особенности, но вышло откровенно не очень. Озвученный пример некоего Александра я понимаю с натяжкой, поскольку знаю задачу, за обозримое время не зная синтаксиса точно не смог бы восстановить задачу по реализации (хотя опыт позволяет читать код даже на незнакомых языках), а ведь на С++ (в основном в паре с Qt) пишу уже 8 лет.
P.S. Ни один человек не напечатает этот код за время, хоть сколько-нибудь близкое к "мгновенно".

Ваш итератор не будет работать со стандартными алгоритмами. Вот такое не будет компилироваться:
auto res = std::find_if(storage.begin(), storage.end(), [](auto && ptr) { return ptr->x == 20; });

А еще ваш итератор удовлетворяет критериям только ForwardIterator, но не BidirectionalIterator или RandomAccessIterator, т. е. будет работать только с частью алгоритмов (когда вы сделаете его пригодным для использования с алгоритмами), а с некоторыми алгоритмами будет работать медленнее, чем можно было бы.

Я правильно понимаю, что ranges — это что-то типа слайса, итератор с возможностью индексированного доступа?

range — это по сути пара итераторов, begin и end. Итератор в C++17 может сам по себе иметь индексированный доступ. Плюс есть еще sized range — это range, для которого еще определена операция size, для получения размера с константной сложностью.
> Ваш итератор не будет работать со стандартными алгоритмами.
> А еще ваш итератор удовлетворяет критериям только ForwardIterator, но не BidirectionalIterator или RandomAccessIterator, т. е. будет работать только с частью алгоритмов (когда вы сделаете его пригодным для использования с алгоритмами), а с некоторыми алгоритмами будет работать медленнее, чем можно было бы.

А он и не должен. Поскольку:

> Задача формулируется так. Есть класс, содержащий внутри коллекцию std::unique_ptr. Надо выпихнуть наружу возможность пользователям класса перебирать объекты, содержащиеся в коллекции, но без возможности изменить их.

Чему с моей точки зрения приведенный мною пример вполне удовлетворяет, разве нет?
Там вообще-то есть специальное уточнение: «Т. е. вернуть пару итераторов,...». Ваш класс не является итератором в терминологии STL. С таким же успехом можно было бы добавить метод «const SomeObject* object_at_index(size_t i) const» и считать, что задача выполнена.
> 0 Там вообще-то есть специальное уточнение: «Т. е. вернуть пару итераторов,...»

Не-не-не, господа, позвольте! Задача четко сформулирована в одном единственном предложении. Всякие «то-есть» или «ну может быть» и «было бы неплохо» — это уже за рамками ТЗ. Их можно учитывать а можно не учитывать. На усмотрение исполнителя.

Я сейчас, конечно же, не про итераторы и не про ренжи. Я сейчас сугубо про культуру постановки задачи :) Хотите, чтобы моторная лодка имела функцию вертикального взлета? Пожалуйста! Преодолевала звуковой барьер? Не вопрос! Укажите это явным образом в ТЗ. В противном случае она будет плавать но не более того.
Ну т. е. мы пришли к пониманию, что задача определения собственных итераторов даже для таких простых вещей, как добавление const, довольно трудна и занимает десятки строк кода?
Ну на самом деле само по себе количество строк кода — это не самый важный критерий. При прочих равных я бы предпочел двадцать строк кода но тупого и понятного основной массе одной, к которой без поллитры даже не подходить.
Так в том то и дело, что код итераторов — он только кажется тупым, а на самом деле он довольно сложен. Начать с того, что надо определить, каким требованиям будет соответствовать возвращаемый итератор. А после этого аккуратно реализовать все требуемые операции. В реальности это обычно встречается только в библиотеках, а в прикладном коде почти никогда, потому что сильно сложно и муторно. Ranges решают эту проблему. Использовать их для таких вещей в прикладном коде очень просто.
Возможно, возможно. Если ranges позволяет просто и наглядно решить эту проблему — это хорошо.

Мы видимо рассматриваем проблему языка с разных колоколен. Я смотрю с сугубо практической стороны. Мне редко приходится на практике применять сложные языковые конструкции C++. Зато я видел тонны, просто тонны кода — как открытого так и в основном коммерческого — где разработчики имели очень, очень отдаленное представление как о культуре ООП так и о культуре С++ в частности. Когда std::auto_ptr<> является недостижимой вершиной а сокрытие данных в классе — назойливой мухой от которой все отмахиваются. Результат подхода легко предсказуем и всегда сбывается на практике. И потом вот это вот все «управляет атомной станцией». Некоторые вещи, кстати, почти без преувеличения.

А вы про какие-то ренжи… Я понимаю желание сделать язык лучше. Но в ситуации, когда львиная доля реальных конечных пользователей языка не в состоянии принять элементарные идиомы которым уже буквально четверть века — какие там нафик ренжи…
Ну т. е., помимо формулировки задачи, вы же понимаете, что ваше решение приведет к тому, что рано или поздно пользователи вашего класса наткнуться на то, что хотя бы вот такой простой код не будет работать, хотя все ожидают, что он должен работать:
for (auto it = storage.begin(), end = storage.end(); it != end; it++) {
}

А еще на вашем итераторе не будут работать такие базовые вещи, как например std::advance, что тоже вызовет мягко говоря удивление у пользователей.
> Ну т. е., помимо формулировки задачи, вы же понимаете,

Нет, товарищи, не понимаю и понимать не хочу. Категорически отказываюсь! Все геморрои всегда начинаются именно с этих слов. Нафик-нафик. Есть ТЗ — извольте в рамках договоренностей :)

> что тоже вызовет мягко говоря удивление у пользователей.

У пользователей всегда возникнет удивление. Как бы мы не старались. По другому и быть не может. А вот кому они выставят счет — это вопрос. Самый в общем то интересный и насущный. Если исполнитель будет «нувыпонимать» — ммм ну вы понимаете, кому его переадресует заказчик и каково будет исполнителю :)
Тут нет заказчиков, исполнителей, ТЗ, и прочего. Тут есть обсуждение ranges и для чего они нужны. Я надеюсь теперь стало понятно, что имелось в изначальном тексте.
Нет, товарищи, не понимаю и понимать не хочу. Категорически отказываюсь! Все геморрои всегда начинаются именно с этих слов. Нафик-нафик. Есть ТЗ — извольте в рамках договоренностей :)

Считайте, что с задачей справились, и это ЧТЗ на доработки, если так проще. Как выше сказали, тут нет каких-то ТЗ. Да и само ТЗ обычно формируется с двух сторон, продукт показывает, что он примерно хочет, а дальше ТЗ допиливается реализаторами, где что-то забыли, где что-то неправильно указали, и т.п.
КМК, вы все же не правы в своем отрицании :)
Если уж придираться к словам в ТЗ, то там не написано, вывести объекты, а написано перебрать объекты. Способов перебрать объекты достаточно много и все они оперяются на концепты итераторов. Ваш же итератор, если быть честным, не удовлетворяет ни одному из концептов.

У меня есть два генератора, один генерирует пифагоровы тройки, другой — какие-нибудь другие тройки. И пусть это всё счастье на коллбеках.


Как мне сделать генератор, совмещающий эти элементы как zip?


Как сделать генератор, строящий из списков [p0, p1, p2, ...] и [o0, o1, o2, ...] список [p0, o0, p1, o1, ...]?


Как ограничить количество генерируемых троек?


Если у меня ленивые списки, например, то мне об этом обо всём вообще даже думать не надо.

> Например, мы здесь в Unity любим шутить
Ну да, смотря на качество этого лучшего движка в мире, как-то и не верется что там работают люди, умеющие программировать.
Олег, а вот по поводу этого:
Его мгновенная реакция была вроде: «о, это какой-то старый код в стиле Си?». И я такой: «нет, в точности до наоборот!».

Объективно, недавнему студенту сложнее принять код на С, или навороченный код на С++?
Если дать те примеры, которые ты привёл в статье, студенту, какой из них он быстрее поймёт?

Мне тоже код на современных С++ кажется диким невоспринимаемым ужасом, но, может, они другие?
Может, «поколение зеро» воспринимает лучше именно такие абстракции, но не понимает С (и соответственно, не понимает, что такое «процессор», как он работает)?
int i = 0;
for (int z = 1; ; ++z)
        for (int x = 1; x <= z; ++x)
            for (int y = x; y <= z; ++y)
                if (x*x + y*y == z*z) {
                    printf("(%i,%i,%i)\n", x, y, z);
                    if (++i == 100)
                        goto done;
                }
done:


Чтобы читающих ценителей прекрасного случайно не стошнило от неожиданной встречи с goto можно:
1. Не стесняться проверять i в условиях выхода из циклов либо
2. Не стесняться завести более универсальную переменную done, и в циклах проверять уже её
Код ошибки не забудьте только, там у main возвращаемое значение типа int.
Время выполнения программы, правда, еще не посчитается.
Но ничего, зато Вы return напишете!

Мб имелось в виду, засунуть цикл в функцию и из неё уже сделать return?

Ну посчитайте время перед return.
Или вообще пусть Вам время считает объект в стиле RAII: само посчитается, как бы и где бы не вышли из функции.
Раз уж на c++ пишем.

Или как уже написали: обернуть циклы в функцию, а время считать вне её.

Если бы в плюсах были удобные лямбды, ими чаще бы пользовались, например:


int i = 0;
|| {
   for (int z = 1; ; ++z)
        for (int x = 1; x <= z; ++x)
            for (int y = x; y <= z; ++y)
                if (x*x + y*y == z*z) {
                    printf("(%i,%i,%i)\n", x, y, z);
                    if (++i == 100)
                        return;
                }
}();

Хотя это все равно грязновато, вариант с итераторами сильно удобнее.

(это предварительный синтаксис, потому что в стандарте C++ корутин нет):
Зато есть старый добрый вычисляемый goto switch:
#include <iostream>
#include <tuple>

#define CO_BEGIN \
	{ \
		switch (lineno) \
		{ \
		case 0: \
	//
#define CO_END }}
#define CO_YIELD(value) \
	do { \
		lineno = __LINE__; \
		return (value); \
		case __LINE__:; \
	} while (false) \
	//

class PytripleGenerator
{
public:
	std::tuple<int, int, int> next()
	CO_BEGIN
		for (z = 1; ; ++z)
			for (x = 1; x <= z; ++x)
				for (y = x; y <= z; ++y)
					if (x*x + y*y == z*z)
						CO_YIELD(std::make_tuple(x, y, z));
	CO_END
private:
	int lineno = 0;
	int x = 1, y = 1, z = 1;
};

PytripleGenerator pytriples()
{
	return PytripleGenerator();
}

int main()
{
	PytripleGenerator py;
	for (int i = 0; i < 100; ++i)
	{
		auto [x, y, z] = py.next();
		std::cout << "(" << x << ", " << y << ", " << z << ")\n";
	}
}

Да, а потом читаешь чей-нибудь чужой код (особенно, если не в IDE, а в гитхабе, где GOTO нет), и пытаешься понять, что за магия тут происходит. Все-таки С++ это действительно скорее множество языков, а не один язык.

Это было бы практическим применением той теоремы если бы кто-то и правда преобразовал код в комбинацию простых циклов и ветвлений.

А там просто эксплуатируется способность оператора switch иметь метки внутри вложенных блоков кода.
( Есть какие-то недостатки в Boost?
Boost.Coroutine — created by Oliver Kowalke, is the official released portable coroutine library of boost since version 1.53. The library relies on Boost.Context and supports ARM, MIPS, PowerPC, SPARC and X86 on POSIX, Mac OS X and Windows.
Boost.Coroutine2 — also created by Oliver Kowalke, is a modernized portable coroutine library since boost version 1.59. It takes advantage of C++11 features, but removes the support for symmetric coroutines. )

Это демонстрация отставания на десятки лет даже от Modula
process оператор; ...  
begin
   прогсосрас; оператор  
end прогупрзадач;

То, что Модула небольшой и эффективный язык, не вызывает сомнения. Описанный Холденом и Вандом (1980) компилятор требует всего лишь 16К слов памяти PDP 11 и компилирует п строк программы примерно за (5 + n)/12 секунд на PDP 11/40. Кроме того, язык очень удобен на практике. Он использовался при реализации ряда достаточно сложных систем с большим успехом (см., например, Эндрюс, 1979, Рунсиман, 1980).

не говоря о Modula-2
в Модуле 2 от процессов отказались вовсе в пользу взаимодействующих подпрограмм (Вирт, 1980)


Для демонстрации фичи оптимальность алгоритма обычно не имеет значения.


Хорошо, отложим в сторону алгоритмы...

В оригинале есть хотя бы частичное соответствие MISRA C:

http://ericniebler.com/2014/04/27/range-comprehensions/
for(int z = 1;; ++z)
{
    for(int x = 1; x <= z; ++x)
    {
        for(int y = x; y <= z; ++y)
        {
            if(x*x + y*y == z*z)
            {
                result += (x + y + z);
                ++found;
                if(found == 3000)
                    goto done;
            }
        }
    }
}
done:   


Bartosz Milewski уже предпочитает демонстрировать «Getting Lazy with C»

(
речь о:


--- Bartosz_Milewski_Bad_Style.c	Fri Feb 01 16:19:10 2019
+++ ericniebler.com_2014_04_27_range-comprehensions.c	Fri Feb 01 16:19:00 2019
@@ -1,11 +1,17 @@
 for(int z = 1;; ++z)
+{
     for(int x = 1; x <= z; ++x)
+    {
         for(int y = x; y <= z; ++y)
+        {
             if(x*x + y*y == z*z)
             {
                 result += (x + y + z);
                 ++found;
                 if(found == 3000)
                     goto done;
             }
+        }
+    }
+}
 done:   

)
Всякий раз перед возвратом (CO_YIELD) из «корутины» мы запоминаем место (номер строки), откуда осуществляем возврат. Когда функция вызывается вновь, мы возвращаемся в это место (сразу после оператора return).
Достигается это путём обёртывания всего тела функции в switch и расстановке меток case __LINE__: во всех местах возврата, благо switch в C/C++ позволяет прыгать даже внутрь вложенных блоков (самое известное применение чему — Устройство Даффа).
(это предварительный синтаксис, потому что в стандарте C++ корутин нет):
Зато есть старый добрый вычисляемый goto он же switch

Если желаете, могу попробовать, без эмуляции с помощью GoTo, на Modula-3 ( coroutines сейчас в стадии разработки, если не на них, то на threads).
За основу возьмём:
PROCEDURE NewTriangle(a, b, c: INTEGER; VAR tcount, pcount: INTEGER) =
  VAR perim := a + b + c;      
  BEGIN
    IF perim <= max THEN
      pcount := pcount + 1;
      tcount := tcount + max DIV perim;
      NewTriangle(a-2*b+2*c, 2*a-b+2*c, 2*a-2*b+3*c, tcount, pcount);
      NewTriangle(a+2*b+2*c, 2*a+b+2*c, 2*a+2*b+3*c, tcount, pcount);
      NewTriangle(2*b+2*c-a, b+2*c-2*a, 2*b+3*c-2*a, tcount, pcount);
    END;
  END NewTriangle;
Или «эффективный алгоритм»
Иногда меня посещают мысли изучить С++ хотя бы на каком-то базовом уровне, но потом я вижу что-то подобное и понимаю: нет нет нет нееееееееееееет *убегает за горизонт обратно на Джаву*
ну на самом деле не так все в Плюсах плохо. Тем более после Джавы… На мой взгляд количество граблей с которыми приходится мужественно бороться примерно тоже. Только места другие. Моя старая шутка — в плюсах все пытаются придумать ссылки и manage_code с GC… В Jave и C# изобретают указатели.
В Jave и C# изобретают указатели.

Ни разу не приходилось думать о таком даже. ;)
ну не совсем правильно выразился — «всячески пытаются обойти создание объектов и их копий в GC». Как пример концепция string_view.
Это да, не без этого. Возможно, тут ещё дело в контексте.

Джаву я начал изучать ещё на 5-ой версии, и всё, что добавлялось после, казалось просто крутыми плюшками, которые помогают проще писать код.

На Си я написал пару простых задачек и всё, с какого угла подойти к С++ нет никакого понятия — начинать ли со старых стандартов, или сразу с последней спецификации. Вроде есть куча книг и самоучителей (у меня подписка на библиотеку O'Reilly, так что с доступом проблем никаких), но какой из них выбрать и какой лучше — без понятия.
C++ я тоже начал изучать довольно давно, еще с пре-C++98, и многое из того, что добавлялось после, «казалось просто крутыми плюшками, которые помогают проще писать код» — умные указатели, лямбды, range-based for, auto (в строго определенных случаях, впрочем, пихать его при любой возможности я не люблю — по моему мнению это ухудшает читабельность кода), а некоторыми вещами я пользуюсь довольно редко (всякой compile-time шаблонной магией, которой в статьях, подобных этой, пугают детей). Сейчас я бы изучал C++ примерно в таком же порядке, хронологическом, скажем так. Ну и понятное дело, что необходимость при написании любого кода думать о том, кто этот код потом будет читать и поддерживать, никто не отменял.
И я бы начинал с истоков. На мой взгляд проблема с++ как раз в том что люди пытаются не зная как оно изначально придумывалось вертеть с++ во все стороны. Т.е. те же самые «умные указатели» — это средство как не потерять память, как сделать RAII в «таксебе-RAII» среде. По сути «костыль». А у новичков я часто вижу обратное — «указатель это быстрая версия но усеченая умного указателя»

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

Дело не в «раньше сиденья не было», а в разных уровнях абстракций. Raw pointer — это один уровень абстракции, smart pointer — другой. Нельзя сказать, что smart pointer «однозначно лучше», это зависит от задачи, которая перед тобой стоит.
Не совсем так. Новичек не должен думать, что плюсы это серебряная пуля. Он должен знать и понимать что руль и колеса прикручены изолентой, и что если их развернуть будут проблемы. И саморезы из седла — это лямбды так привернуты…

Если ему неудобно писать на плюсах — это не плюсы плохие, а инструмент для данной задачи не подходит.
Имхо, лучше всего начать с «Дизайн и эволюция С++» Страуструпа, множество вопросов «почему так» отпадут сами собой, разовьётся нормальный стокгольмский синдром станет понятна внутренняя логика и проблемы, лежащие перед этим языком (и внутри него).

Да нет, не пытаются


Периодически появляются в стандартной библиотеке всякие низкоуровневые апи, но обычно они для тех, кому нужен перфоманс в дотнете (кто не хочет с FFI возиться, а какие-то операции у них медленные).


В стандартном C# коде никто не будет париться по поводу лишних копий. Тот же LINQ местами скрывает аллокации (a.GroupBy(x=>x.Foo) например), от этого им не перестают пользоваться.

Не постоянно, конечно, да и далеко не большинство, но как минимум некая часть джавистов думает про то, сколько объектов скрывается за этим сахаром, сколько промежуточных коллекций создастся в этом месте. И, при случае, если выбор стоит между примерно одинаковыми по чтению вариантами, то несомненно выберут вариант с меньшим количеством лишних объектов.
Думаю, в шарпах подобное имеет место быть.

Поэтому, с некоторыми правками, можно сказать, что утверждение не ложно. ;)
Конечно не пытаются. Примерно как и плюсовики которые не все работу с файлами обворачивают в RAII, а указатели в shared_ptr. Потому что не забудут файл закрыть, и память освободить… но мы то с вами знаем…

Из того что понял я, автор жалуется на:


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

Я не спорю что С более удобен. Вот только там тоже проблемы с инструментами когда после перехода на более новый GCC приходится пересобирать половину пакетов и патчить ядро, потому что C99 inline и новые оптимизации. Но при этом соглашусь что чем дальше, тем менее читабельным становится C++, а заглушка читабельности в виде auto не является панацеей от всех проблем.

С++XS, теперь с монобровью и без дырки для наушников (ведь все именно этого хотели, да?)!
Время компиляции «hello world» от 9999мс!
Вот по всем этим причинам и хочу перейти на Rust!
Но смущает сильная порезанность ООП = боюсь, что код легко с C++ на Rust не сконвертируешь. :(

Просто переходить надо постепенно. Просто взять и переписать всё нафиг — подходит для лаб, но серьезный бизнес на этом потеряет деньги, поэтому сразу НЕТ.


Про постепенное переписывание мне нравится этот и этот цикл статей.

А почему проверяется условие только на равенство? Очевидно же, если X×X+Y×Y>Z×Z, то остальные Y проверять для данного X и Z нет смысла.

UFO landed and left these words here
Интересно, а когда стало можно на С в структуры добавлять функции? Или тут С++?
Так никто в структуру функцию не добавляет, она лежит снаружи и передаётся по указателю (по имени). В структуре находится только typedef.
а объявление pyTriples(TripleWasFound tripleWasFound) внутри обявления структуры?
gcc у меня на это ругается, да и на typedef тоже. Вот я и интересуюсь — может версию обновить.
UFO landed and left these words here

Выше уже писали про то, что комбинировать подобные функции просто невозможно, а в этом весь смысл абстракции.

И на C можно все нормально написать (см. ниже) и композиться будет настолько хорошо, насколько это возможно в C.

Программисту нужно «сохранить состояние вычисления между вызовами функции», итераторы, async/await, infinite streams, всякие prolog-style недетерминированные вычисления это уже приложения этой фичи. В Haskell для этого есть монады и ленивые вычисления, в Smalltalk и Scheme есть continuations, в C# есть yield return и async/await.

Я что хочу сказать — если задачу нужно решать генерацией кода, а ваш язык этого не умеет, то нечего огород городить. Врубаем ручной кодогенератор и пишем все сами. Ни один популярный язык нельзя расширять с помощью лямбд и замыканий в любом их виде. Вот простой пример того, почему вы не сможете сделать свой for и любую другую управляющую конструкцию на лямбдах.

array.ForEach(x => if (x == 10) return);
foreach(var x in array) if (x == 10) return;


Контрпример из мира мертвых языков — в Smalltalk придумали нелокальный выход из лямбд и весь язык вокруг этого сделали, в итоге язык действительно можно расширить с помощью замыканий.

Собственно ручной генератор продолжений (continuations) на C в действии.

#include <stdint.h>
#include <time.h>
#include <stdio.h>

typedef struct triple {
	int x;
	int y;
	int z;
} triple;

triple next_pytriple(triple* iter) {	
	for (int z = iter->z; ; ++z) {
		for (int x = iter->x; x <= z; ++x) {
			for (int y = iter->y; y <= z; ++y) {
				if (x*x + y*y == z*z) {
					iter->x = x;
					iter->y = y + 1;
					iter->z = z;
					return (triple){x, y, z};
				}
			}
			iter->y = 1;
		}
		iter->x = 1;
	}
}

void pytriples(size_t n, triple triples[]) {	
	triple iter = {1, 1, 1};
	for (int i = 0; i < n; ++i) {
		triples[i] = next_pytriple(&iter);
	}    
}

int main() {
	clock_t t0 = clock();
	triple triples[100];
	pytriples(100, triples);
	for (int c = 0; c < 100; ++c) {
		printf("(%i,%i,%i)\n", triples[c].x, triples[c].y, triples[c].z);
	}
	clock_t t1 = clock();
	printf("%ims\n", (int)(t1-t0)*1000/CLOCKS_PER_SEC);
	return 0;
}

а я, как дурак, мечтал о шарповом yield return <something>.
кажется будто на C++17 тот самый C++ начало заворачивать куда-то не туда

Помню еще меня расстроил стандарт C++17, так что когда подвернулась возможность поработать на Rust, свалил не раздумывая. А теперь кажется мне, что по крупному в плюсы назад я уже наверное никогда не вернусь. И чем дальше я смотрю на будущие стандарты, тем меньше хочу писать на плюсах. Хотя когда-то давно очень радовался нововведениям в С++11, тогда казалось, что жизнь стала легче и правильнее.
Но блин, в плюсах очень много наследия, которое так просто не выкинешь и которое заставляет вместо простых и хороших решений делать сложные и громозкие просто потому, что иначе никак.
По ходу сумма технического долга уже перешла какую-то красную черту и теперь растет экспоненциально.

«Постойте, постойте господа!»

Это не про алгоритм из статьи?

http://rosettacode.org/wiki/Pythagorean_triples#C
Sample implemention; naive method, patentedly won't scale to larger numbers, despite the attempt to optimize it. Calculating up to 10000 is already a test of patience.


( Tогда уж берём Mercury и получаем минимальный код:
pythTrip(Limit,triple(X,Y,Z)) :-
    nondet_int_in_range(1,Limit,X),
    nondet_int_in_range(X,Limit,Y),
    nondet_int_in_range(Y,Limit,Z),
    pow(Z,2) = pow(X,2) + pow(Y,2).
)

Efficient method, generating primitive triples only as described in the same WP article:

xint total, prim, max_peri;
xint U[][9] =  {{ 1, -2, 2,  2, -1, 2,  2, -2, 3},
        { 1,  2, 2,  2,  1, 2,  2,  2, 3},
        {-1,  2, 2, -2,  1, 2, -2,  2, 3}};
 
void new_tri(xint in[])
{
    int i;
    xint t[3], p = in[0] + in[1] + in[2];
 
    if (p > max_peri) return;
 
    prim ++;
 
    /* for every primitive triangle, its multiples would be right-angled too;
     * count them up to the max perimeter */
    total += max_peri / p;
 
    /* recursively produce next tier by multiplying the matrices */
    for (i = 0; i < 3; i++) {
        t[0] = U[i][0] * in[0] + U[i][1] * in[1] + U[i][2] * in[2];
        t[1] = U[i][3] * in[0] + U[i][4] * in[1] + U[i][5] * in[2];
        t[2] = U[i][6] * in[0] + U[i][7] * in[1] + U[i][8] * in[2];
        new_tri(t);
    }
}


Не имеет ли смысл демонстрировать достоинства новых средств языка не на наивном алгоритме, а на эффективном? ( На rosettacode считают «how many Pythagorean triples there are with a perimeter no larger than 100 and the number of these that are primitive», но всё же ...)

Для демонстрации фичи оптимальность алгоритма обычно не имеет значения. Я не понимаю всех людей, которые налетели "Да ты делаешь лишние итерации" и "да на прологе в одну строчку можно написать".


Смысл же не в этом.

Нет причин с Арасом не соглашаться, но только потому что это все вздохи. Послушали и далее своей головой живем.

Читабельность кода можно улучшать бесконечно так же как и «переиспользуемость» и точно также «значение преувеличено». Лишь бы работало по большому счету.

Код написанный для переиспользования покрыт юнит тестами, бенчмарками — стал черным ящиком — стабильно работает — читать не надо.

Время компиляции конечно важно, но это все же вопрос организации проекта.
Статья вдохновила на извращения
int main()
{
  auto gen = generator(std::tuple<int, int, int>)
  {
    for (int z = 1; ; ++z)
      for (int x = 1; x <= z; ++x)
	for (int y = x; y <= z; ++y)
	  if (x*x + y*y == z*z)
	    co_yield(std::make_tuple(x, y, z));
  };

  for (int i = 0; i < 100 && (bool)gen; i++)
  {
    auto val = gen.next();
    printf("(%i,%i,%i)\n", std::get<0>(val), std::get<1>(val), std::get<2>(val));
  }

  return 0;
}

cpp.sh/8dy27
За 15 лет стажа С++ тоже прихожу к мнению что «С с RAII» был бы идеальным языком

Ну, шарповый пример на Linq в одном месте с причитаниями, что замедление в 10 раз недопустимо — это всегда смешно :)

«Всё отлично, пока вы не хотите изменять или переиспользовать этот код. Но что если, например, вместо распечатывания на экране вы захотите рисовать тройки как треугольники? Или вдруг вы захотели остановиться сразу же, как одно из чисел достигло сотни?»

Калбэк вызывать вместо printf и забыть все эти ленивые вычисления как страшный сон. Аминь.

Самая большая беда C++ в большом сообществе. А это значит, что растёт непонимание и желание странного части сообщества, с точки зрения другой части этого самого сообщества. Часть хочет функцию из трёх строчек, отвечающую за пуск все ядерных ракет в мире, а другой части хочется быстрый и компактный код для встроенной техники, не обременённой гигабайтами оперативки и терабайтами ПЗУ. Главный показатель эффективности кода, для тех, кто учился давно — это скорость выполнения + потребление ресурсов, а никак не время потребное на написание кода. Хотя, с точки зрения манагеров, время написания имеет большое значение, но опытный манагер (или не успевший вовремя отвалить в сторону после внедрения) знает ещё и про время поддержки (багфикс+модернизация), а она сильно зависит от простоты восприятия кода.
Всё новое, упрощающее код, ускоряющее и облегчающее разработку — это безусловное благо, но, по моему скромному мнению, в развитии C++ появляется много узкоспециализированного и фрагментарного.
Смущает отвратительный код на фоне глубокомысленных рассуждений. Автор, похоже, не заморачивается над размышлением по поводу алгоритма, главное, быстро скомпилировать. Так что я как-то очень скептически отнёсся ко всему остальному. Три вложенных цикла — сила!
void printNTriples(int n)
{
    int i = 0;
    for (int z = 5; ; ++z)
        for (int x = 3; ; ++x){
		int y = sqrtl(z*z - x*x);
		if ( y < x ) break;
		if ( y*y + x*x == z*z){
                        printf("%d, %d, %d\n", x, y, z);
			if ( ++i == n) return;
		}
	}
}
автор, похоже, не заморачивается над размышлением по поводу алгоритма
О, Вы тоже заметили...
Мне уже ответили, что здесь:
Для демонстрации фичи оптимальность алгоритма обычно не имеет значения.
P.S. Ссылки на оптимальный алгоритм здесь же в др. комментариях
Ну как-то это очень странно выглядит. Тем более, что алгоритм крайне прост. Всё же разница между двумя и тремя вложенными циклами велика, а где-то в теле статьи автор пишет о временах выполнения. Да и использование рэнжей в данном случае представляется мне слишком искусственным. Кроме того, если автор не в состоянии написать простейшую функцию на С, то почему я должен доверять его умению сделать это на с++.
Я как-то не готов считать использование лишнего внутреннего цикла неоптимальностью. Непониманием да, типа язык знает, думать не умеет.
Не имеет ли смысл демонстрировать достоинства новых средств языка не на наивном
алгоритме, а на эффективном? ( На rosettacode считают «how many Pythagorean triples there are with a perimeter no larger than 100 and the number of these that are primitive», но всё же ...)
Кроме того, если автор не в состоянии написать простейшую функцию
Да, согласен вот ( с оговорками см.выше) на Modula-3:
PROCEDURE NewTriangle(a, b, c: INTEGER; VAR tcount, pcount: INTEGER) =
  VAR perim := a + b + c;      
  BEGIN
    IF perim <= max THEN
      pcount := pcount + 1;
      tcount := tcount + max DIV perim;
      NewTriangle(a-2*b+2*c, 2*a-b+2*c, 2*a-2*b+3*c, tcount, pcount);
      NewTriangle(a+2*b+2*c, 2*a+b+2*c, 2*a+2*b+3*c, tcount, pcount);
      NewTriangle(2*b+2*c-a, b+2*c-2*a, 2*b+3*c-2*a, tcount, pcount);
    END;
  END NewTriangle;


где-то в теле статьи автор пишет о временах выполнения.

Это они ( на Habre — перевод, код «кочует» из статьи в статью годами) несколько преуменьшили, «наивная версия» просто нежизнеспособна:
Calculating up to 10000 is already a test of patience.
P.S.
почему я должен доверять его умению сделать это на с++

Cамое показательное, я просто решил «почитать теорию»: «что за тройки такие,
от самого Пифагора» Ж-) нашёл массу материала, а вот авторы статей-оригиналов — не посчитали нужным?
Концепции, конечно, хорошо, но зачем же так явно демонстрировать пренебрежение наследием, алгоритмической культурой? Или просто самонадеянность: задача-то с виду простая — «перемолотим в цикле» на суперкомпьютере?

Как в известной истории: «Зачем думать, банан доставать надо!»
Как в известной истории: «Зачем думать, банан доставать надо!»
Банан доставал подпрыгивая на стуле, вместо привязывания каната, как раз венец эволюции...
Only those users with full accounts are able to leave comments. Log in, please.
Information
Founded

25 March 2012

Location

Россия

Website

jugru.org

Employees

51–100 employees

Registered

22 August 2013

Habr blog