Комментарии 238
Спасибо. Читал в оригинале, поэтому пролистал — вроде все ок ))))
В интернетах ходит шутка о стадиях развития программиста на C/C++. Я помню, как был на средней стадии где-то лет 16 назад.
У нас контора сейчас находится где-то примерно между 4 и 5 этапом.
Большинство программистов — даже миновав стадию 3.
А часть вообще смотрит в сторону js.
Реально с++ развивается куда-то не туда.
Появляются полезные штуки, но ещё больше полезных — не появляется. Даже в перспективе.
Зато вместо этого ширится и глубится «шаблонная магия».
Только это не отменяет того, что в языке не появляются нужные для работы вещи.
Т.е. речь идёт не о субъективной «скучности» языка, а об объективной «нужности».
C++ становится интереснее, выразительнее наверное.
Но… не становится более нужным.
Ну вот ренжи решают вполне конкретную проблему, на этом примере — переиспользуемость, избегание дублирования, разделение сущности и алгоритма обработки. У вас нет такой проблемы? Есть ещё некая проблема записи в "функциональном стиле".
А какие фичи бы вам хотелось иметь, что вам нужно согласно критерию "объективной нужности"?
Хочется нормальной интроспекции.
Т.е. хочется больше фич в runtime, а у c++ все новые навороты — в compile-time.
Не знаю что стандарт С говорит о двоичной совместимости, но на ней уже лет дцать построена и работает тьма проектов.
А с c++ ты даже за собственный проект не можешь ручаться: будет ли подключаться какая-нибудь СВОЯ библиотека при смене версии компилятора.
Про compile-time интроспекцию не совсем понятно. Интроспекция же это получение информации о внутренней структуре объекта в runtime. Этого не хватает.
Ладно, давайте так:
Могу я взять либу, скомпилированную неизвестно кем неизвестно чем (но из c++ исходника), создать объект класса, который она экспортирует и пошариться по его внутренностям, повызывать методы, пообращаться к переменным?
Замечу, оверхед — по памяти.
В плане производительности, если тебе это не надо, то ты "за это не платишь".
Более того, это можно было бы вообще отключать опциями компиляции.
Тогда выдавалась бы ошибка, что интроспекция недоступна.
А так, c++ остаётся эдаким средством строительства высокоэффективных "кирпичей" (монолитов). Вся гибкость языка — в compile-time, а если нужно универсальное взаимодействие на двоичном уровне, то… C-style интерфейс. И это частично, неуклюже так, прикроет только один вопрос.
Информация о внутреннем устройстве это всё-таки принадлежность класса, а не объекта. Так что объект как был маленьким, так и останется.
И — да: отличие в том, что интроспекция по умолчанию включена, а не отключена, а самое главное — стандартна, а не то что один программист использует одну библиотеку, другой — другую.
Да, какая-то отсылка на класс в объекте должна быть, Вы правы.
Но это всё-таки указатель, а не массивная структура данных.
Вспомните, что таблица виртуальных функций тоже по умолчанию не генерируется.
В случае невиртуального класса можно добавить ещё один невиртуальный метод, который будет давать доступ к "классу объекта" с информацией о полях и методах. Конечно, если указатель на невиртуальный класс скастовать к void*, то не получится восстановить информацию, но это следствие гибкости С++ и с этим ничего не поделать. Информацию о полях и методах класса можно будет положить куда-нибудь в секцию констант рядом с описаниями других классов — и если к ним никто не будет обращаться, то, возможно, они даже в память не будут загружены. Единственный минус — бинарники будут больше.
Через некоторое время систему А решили перевести на модерн С++. Ну в самом деле, GCC 4.x, C++98 и все такое уже почти 10ть лет. Доколе? ОК, взяли последний GCC (что мелочиться? чтобы сразу C++17) немного потрахались но перевели. Заработало. И стало вдруг всем тепло уютно и сухо.
Но вот незадача. Система Б без которой уже никуда под модерн С++ жить отказалась наторез. Даже собираться. Отказаться от ней нельзя. Поменять её нельзя. Как-либо повлиять на неё нельзя. В общем что дали с тем и живите.
ОК. Решили — а давайте поженим нашу А уже под модерт С++ с ихней Б собранной тем, древним, от мамонта. Собрали. И даже успешно слинковали что всех удивило. Но вот незадача: начало все валиться к чертям собачьим. Патамучта что-то там не так с разницей реализации в std::string и прочими мелкими ништяками STL. А они задействованы в API между А и Б.
В общем, пока не обрезали API почти до уровня plain C — ничего не заработало. А ведь казалось бы ну что нам стоит? Есть два проекта оба на С++ оба вроде почти вменяемые даже оба в исходниках — взять и поженить! А. Шас…
Вброшу свою ложечку. Судя по https://old.reddit.com/r/cpp/comments/akihlv/c_modules_might_be_deadonarrival/, эти самые констрейнты заключаются в "шоб мы могли свой код времён Кеннеди перенести в новый стандарт в 2 строчки".
Т.е. module lookup хотят сделать зависимым от содержимого файлов (а там имя модуля может задаваться в зависимости от макроссов, упс...) просто потому, что, грубо говоря, есть ОС, которая не умеет больше одного слоя директорий в своей ФС. Или ОС, которая не знает, что такое расширение. Или в байте 9 3/4 бит...
P.S. Блин, не долистал до конца ветки, уже сказали.
Это меня в Scala в свое время очень выбешивало. Я пытался использовать ее просто как java-с-плюшками, но постоянно натыкался на то, что популярные в Scala-мире библиотеки делают даже простейшие вещи какими-то очень сложными и неочевидными фичами языка, и мне волей-неволей приходится это все разбирать и изучать, чтобы их использовать.
С С++ я распрощался уже лет пять назад, и рад этому. Уже тогда это был перегруженный фичами язык с переусложненным синтаксисом, во что он там сейчас превратился мне страшно подумать.
Хотя вам просто дают дополнительные возможности, которые следует применять там, где это оправдано.Осталось только узнать, какие фичи нужно использовать, и когда они оправданы. Тому кто варится в плюсах нонстопом последние лет 10 это может и не сложно. А я вот на них около 6 лет почти не писал, и когда недавно решил чуть освежиться и открыл cppreference.com, у меня глаза на лоб полезли от объема информации. Просто чтобы перечислить все новые термины, фичи, инструкции и функции нужно с десяток A4 газетным шрифтом. Каким образом новичку вообще определить, какую из них стоит изучить, а какую просто пропустить, не входя в рекурсию? Нанимать сеньора и проходить с ним по всему списку, чтобы он отметил все, что реально используют на практике и могут спросить на собеседовании?
Очень рад увидеть статью, в которой вижу единомышленников. Спасибо.
А чо сразу рублём-то. Может, вначале пообщаться с авторами Стандарта? Или свою фичу подать на рассмотрение? У нас в России, кстати, есть три человека из Комитета. И один из них будет выступать на ближайшей C++ Russia.
я написал, предложил именование которое меня устроит, antoshka перевел и написал автору либы. Автор либы ответил что он лучше знает, и если у нас в голове что-то путается, это наши проблемы =) Дальше желание «улучшать стандарт С++» у меня пока отпало… если только сильно не припрет)
То есть автор пропозала потратил время, ответил вам развёрнуто что, где и как, а вы представляете это "мне автор ответил, что он лучше знает". Я верно вас понял?
Думаю, что Rust недостаточно плох, чтобы стать действительно популярным и составить конкуренцию C++.
Вообще лучше иметь какую-то возможность, чем не иметь, вас же никто не заставляет всем этим пользоваться для любой задачи, в этом и прелесть, можете писать в стиле С, если так по вашему проще, а можете какие-нибудь лямбды с ренджами завернуть при случае, если это сократит код в 10 раз.
Хороший пример, это собеседования… Когда вместо написания кода просят пояснить чем make_shared отличается от конструктора shared_ptr. Зачем forward, если есть move и прочее. Т.е. просят понимания ньюансов языка. При этом вопросы про ньюансы работы всяких «вызов виртуального метода в конструкторе» — никуда не ушли.
Идеология C++ заключается в том, что "ты не платишь за то, что не используешь". Никто не заставляет писать все в стиле C++ 20, выбирайте по месту те механизмы из наличия, которые в данном случае с вашей точки зрения будут удобнее и нагляднее. А безумный лапшеобразный код одинаково легко пишется на любом более-менее развитом языке, это просто вопрос квалификации пишущего. Если человек неграмотный, то никакой Rust тут не спасёт.
Идеология C++ заключается в том, что «ты не платишь за то, что не используешь».Но в итоге все равно платишь, например когда код стал компилироваться дольше, а IDE стала монструозно тяжелой и медленной, но ты не можешь взять старую, потому что в проекте кто-то где-то использовал новую фичу или либу с таковой.
Я хочу сказать, что с C99 кодом такая IDE будет работать вряд ли сильно быстрее, чем с C++20 кодомВ этом то и дело. Даже если писать C99 код, необходимость поддержки IDE новых стандартов плюсов с кучей их фич ведет к ее монстеризации и замедлению. И с каждым новым стандартом это сказывается все сильнее и сильнее. При этом не всегда можно просто взять старую IDE, даже если пишешь на C99. Возвращаясь к изначальному утверждению, мы платим в том числе за то, что не используем, но за потенциальную возможность это использовать.
Всякие возможности рефакторинга и навигации существуют параллельно со всем этим.
Я как раз таки сильно сомневаюсь, что "монстеризация и замедление" и "поддержка новых стандартов C++" так уж связаны. Вон новые версии Скайпа не поддерживают ни одного стандарта C++, а все равно еле ворочаются на средненьком ноутбуке пятилетней давности, потому что Электрон, вот это вот все :) Сейчас вообще тенденции в разработке ЛЮБОГО ПО направлены в сторону "монстеризации и замедления" (под лозунгом "ускорим разработку ценой скорости работы готового продукта", что, кстати, хорошо видно по некоторым комментариям к этой статье), а IDE просто следуют в общем тренде.
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, если ">" заменить на "<", показывает signed char. Но опять же, IDE скорее всего делает это гораздо реже, чем компилятор — например, в случае с constexpr auto IDE вряд ли будет утруждать себя лишними вычислениями. То есть скорее всего принцип «ты не платишь за то, что не используешь» все же работает и здесь.
это свежевыдуманный синтаксис, потому что в стандарте C++ корутин нет
Это не свежевыдуманный синтаксис, а синтаксис из актуального Coroutines TS
А в реальном проекте как это всё будет? Отнюдь я относительно мало взаимодействую с плюсами по работе, но кмк, реальный проект с этим будет собираться не сильно медленнее, а при умеренном использовании даже будет более читабильно.
А пример из разряда, что будет если взять все известные паттерны и соединить их вместе.
>>> 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 для ручного управления памятью… Может быть, для большинства задач, где нужна производительность, в итоге проще сразу взять С++ (?).
Например, взглянув на приведённую выше функцию, можно заметить, что каждый раз делать умножения 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 например, например.
В реальной жизни в реальных системах сишный код являет собой мегабайты жуткой лапши из классов, библиотек, шаблонов, выделений/освобождений памяти и прочего ужаса (сам этим занимался несколько лет кряду). Здесь не до всяких сопливых О(), здесь лишь бы не падало, и на том спасибо.
Мне кажется вы несколько перегибаете. Переписывание именно мегабайтов кода на статически типизируемом языке (С++, например), как правило, не в пример легче переписывания кода на динамически типизируемом языке (Python). Особенно если на это накладывается условный NumPy с его вольными правилами бродкастинга.
Проблемы с утечками, по большей части, решаются использованием инструментов типа valgrind, и соблюдением правил выделения/освобождения ресурсов.
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 — ручное создание итератора с нуля, даже с ручным сохранением состояния вроде.
Конечно с этим я согласен! Задачи разные. Я лишь заметил, что когда в очередной раз приводят вроде как спасительный код на серебряной пуле он почему-то на деле оказывается совсем другого цвета. Ни чуть не лучше вот этого всего что плавает рядом.
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);
}
}
Компилируется за 0.5 секунды, поиграть можно на годболте
Прошу прощения, оптимизатор выкинул всё нафиг, потому что годболт компилирует в режиме библиотеки. Вот правильная ссылка: https://rust.godbolt.org/z/DiS1CC
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);
}
}
Школьник и правда не поймёт, но должен ли?
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)
Интересно, что попытки оптимизации, вроде массива квадратов, чтоб не возводить всё время, приводили лишь к значительному увеличению времени выполнения.
Однако, этот код заставил меня задуматься над тем, что может стоит глянуть в сторону Хаскеля и изучить его. Спасибо за приведённый пример. ;)
На windows компиляция и линковка haskell выглядят удручающе долго. Насколько я понимаю, все статически линкуется в exe файл, с размером в несколько мегабайт. Это чисто windows проблемы? Данные проблемы можно купировать флагами компиляции/линковки?
P.s. если я не ошибаюсь, то haskell полностью рассахаривает take 100 pytha
в список троек, так как они вычислимы на этапе компиляции. Получается в примере посчитана производительность операции print
.
Как написали выше, пифагоровы тройки — не очень удачный пример. Вообще же вы подняли старую проблему 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 все элементарно и просто."
Соглашусь с тем, что заниматься этим мне, допустим, тоже часто не приходится. Какбы ни в первый раз. Тем не менее,
> Вот очень легко формулируемая задача, которая решается в пару строк на 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
можно понять только зарывшись в контекст.
Версия 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 — это что-то типа слайса, итератор с возможностью индексированного доступа?
> А еще ваш итератор удовлетворяет критериям только ForwardIterator, но не BidirectionalIterator или RandomAccessIterator, т. е. будет работать только с частью алгоритмов (когда вы сделаете его пригодным для использования с алгоритмами), а с некоторыми алгоритмами будет работать медленнее, чем можно было бы.
А он и не должен. Поскольку:
> Задача формулируется так. Есть класс, содержащий внутри коллекцию std::unique_ptr. Надо выпихнуть наружу возможность пользователям класса перебирать объекты, содержащиеся в коллекции, но без возможности изменить их.
Чему с моей точки зрения приведенный мною пример вполне удовлетворяет, разве нет?
Не-не-не, господа, позвольте! Задача четко сформулирована в одном единственном предложении. Всякие «то-есть» или «ну может быть» и «было бы неплохо» — это уже за рамками ТЗ. Их можно учитывать а можно не учитывать. На усмотрение исполнителя.
Я сейчас, конечно же, не про итераторы и не про ренжи. Я сейчас сугубо про культуру постановки задачи :) Хотите, чтобы моторная лодка имела функцию вертикального взлета? Пожалуйста! Преодолевала звуковой барьер? Не вопрос! Укажите это явным образом в ТЗ. В противном случае она будет плавать но не более того.
Мы видимо рассматриваем проблему языка с разных колоколен. Я смотрю с сугубо практической стороны. Мне редко приходится на практике применять сложные языковые конструкции C++. Зато я видел тонны, просто тонны кода — как открытого так и в основном коммерческого — где разработчики имели очень, очень отдаленное представление как о культуре ООП так и о культуре С++ в частности. Когда std::auto_ptr<> является недостижимой вершиной а сокрытие данных в классе — назойливой мухой от которой все отмахиваются. Результат подхода легко предсказуем и всегда сбывается на практике. И потом вот это вот все «управляет атомной станцией». Некоторые вещи, кстати, почти без преувеличения.
А вы про какие-то ренжи… Я понимаю желание сделать язык лучше. Но в ситуации, когда львиная доля реальных конечных пользователей языка не в состоянии принять элементарные идиомы которым уже буквально четверть века — какие там нафик ренжи…
for (auto it = storage.begin(), end = storage.end(); it != end; it++) {
}
А еще на вашем итераторе не будут работать такие базовые вещи, как например std::advance, что тоже вызовет мягко говоря удивление у пользователей.
Нет, товарищи, не понимаю и понимать не хочу. Категорически отказываюсь! Все геморрои всегда начинаются именно с этих слов. Нафик-нафик. Есть ТЗ — извольте в рамках договоренностей :)
> что тоже вызовет мягко говоря удивление у пользователей.
У пользователей всегда возникнет удивление. Как бы мы не старались. По другому и быть не может. А вот кому они выставят счет — это вопрос. Самый в общем то интересный и насущный. Если исполнитель будет «нувыпонимать» — ммм ну вы понимаете, кому его переадресует заказчик и каково будет исполнителю :)
Нет, товарищи, не понимаю и понимать не хочу. Категорически отказываюсь! Все геморрои всегда начинаются именно с этих слов. Нафик-нафик. Есть ТЗ — извольте в рамках договоренностей :)
Считайте, что с задачей справились, и это ЧТЗ на доработки, если так проще. Как выше сказали, тут нет каких-то ТЗ. Да и само ТЗ обычно формируется с двух сторон, продукт показывает, что он примерно хочет, а дальше ТЗ допиливается реализаторами, где что-то забыли, где что-то неправильно указали, и т.п.
Если уж придираться к словам в ТЗ, то там не написано, вывести объекты, а написано перебрать объекты. Способов перебрать объекты достаточно много и все они оперяются на концепты итераторов. Ваш же итератор, если быть честным, не удовлетворяет ни одному из концептов.
Ну да, смотря на качество этого лучшего движка в мире, как-то и не верется что там работают люди, умеющие программировать.
Его мгновенная реакция была вроде: «о, это какой-то старый код в стиле Си?». И я такой: «нет, в точности до наоборот!».
Объективно, недавнему студенту сложнее принять код на С, или навороченный код на С++?
Если дать те примеры, которые ты привёл в статье, студенту, какой из них он быстрее поймёт?
Мне тоже код на современных С++ кажется диким невоспринимаемым ужасом, но, может, они другие?
Может, «поколение зеро» воспринимает лучше именно такие абстракции, но не понимает С (и соответственно, не понимает, что такое «процессор», как он работает)?
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, и в циклах проверять уже её
Можно просто 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++ корутин нет):Зато есть старый добрый
#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.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:
)
Достигается это путём обёртывания всего тела функции в 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;
Или «эффективный алгоритм»В Jave и C# изобретают указатели.
Ни разу не приходилось думать о таком даже. ;)
Джаву я начал изучать ещё на 5-ой версии, и всё, что добавлялось после, казалось просто крутыми плюшками, которые помогают проще писать код.
На Си я написал пару простых задачек и всё, с какого угла подойти к С++ нет никакого понятия — начинать ли со старых стандартов, или сразу с последней спецификации. Вроде есть куча книг и самоучителей (у меня подписка на библиотеку O'Reilly, так что с доступом проблем никаких), но какой из них выбрать и какой лучше — без понятия.
Как по мне свежий подход это как раз лучше. Это как новичок, который жалуется, что велосипед постоянно из стороны в сторону колбасит, и руль неудобный, а ему старожил говорит, что раньше вообще сиденья не было, так что новичок просто "истоков" не вкусил.
Если ему неудобно писать на плюсах — это не плюсы плохие, а инструмент для данной задачи не подходит.
Да нет, не пытаются
Периодически появляются в стандартной библиотеке всякие низкоуровневые апи, но обычно они для тех, кому нужен перфоманс в дотнете (кто не хочет с FFI возиться, а какие-то операции у них медленные).
В стандартном C# коде никто не будет париться по поводу лишних копий. Тот же LINQ местами скрывает аллокации (a.GroupBy(x=>x.Foo)
например), от этого им не перестают пользоваться.
Думаю, в шарпах подобное имеет место быть.
Поэтому, с некоторыми правками, можно сказать, что утверждение не ложно. ;)
Из того что понял я, автор жалуется на:
- Новые стандарты, пользоваться которыми его никто не обязывает;
- Конкретные реализации стандартных библиотек;
- Скорость работы инструментов, которая опять же имеет больше отношения к конкретной реализации, чем к языку.
Я не спорю что С более удобен. Вот только там тоже проблемы с инструментами когда после перехода на более новый GCC приходится пересобирать половину пакетов и патчить ядро, потому что C99 inline и новые оптимизации. Но при этом соглашусь что чем дальше, тем менее читабельным становится C++, а заглушка читабельности в виде auto не является панацеей от всех проблем.
Но смущает сильная порезанность ООП = боюсь, что код легко с C++ на Rust не сконвертируешь. :(
Выше уже писали про то, что комбинировать подобные функции просто невозможно, а в этом весь смысл абстракции.
Программисту нужно «сохранить состояние вычисления между вызовами функции», итераторы, 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
Ну, шарповый пример на Linq в одном месте с причитаниями, что замедление в 10 раз недопустимо — это всегда смешно :)
время выполнения увеличилось до 300 миллиМекунд
Калбэк вызывать вместо printf и забыть все эти ленивые вычисления как страшный сон. Аминь.
Всё новое, упрощающее код, ускоряющее и облегчающее разработку — это безусловное благо, но, по моему скромному мнению, в развитии 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. Ссылки на оптимальный алгоритм здесь же в др. комментариях
Я как-то не готов считать использование лишнего внутреннего цикла неоптимальностью. Непониманием да, типа язык знает, думать не умеет.
Не имеет ли смысл демонстрировать достоинства новых средств языка не на наивномДа, согласен вот ( с оговорками см.выше) на Modula-3:
алгоритме, а на эффективном? ( На rosettacode считают «how many Pythagorean triples there are with a perimeter no larger than 100 and the number of these that are primitive», но всё же ...)
Кроме того, если автор не в состоянии написать простейшую функцию
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амое показательное, я просто решил «почитать теорию»: «что за тройки такие,
от самого Пифагора» Ж-) нашёл массу материала, а вот авторы статей-оригиналов — не посчитали нужным?
Концепции, конечно, хорошо, но зачем же так явно демонстрировать пренебрежение наследием, алгоритмической культурой? Или просто самонадеянность: задача-то с виду простая — «перемолотим в цикле» на суперкомпьютере?
Как в известной истории: «Зачем думать, банан доставать надо!»
«Современный» C++: сеанс плача с причитаниями