Вот это кстати меня тоже всегда морозило в QFuture:
When the context object is destroyed, cancellation happens immediately. Previous futures in the chain are not cancelled and keep running until they are finished.
Это прямое следствие того, что QFuture пытается быть всем сразу.
Про then для future-like объекта, возвращаемого из run — а там нет then, в этом-то и смысл. При большом желании можно туда приделать notify, который по завершению будет посылать Qt'шный сигнал с Qt::QueuedConnection, и проблемы как с QFuture там не будет.
Про лямбды — на лямбды в этом примере Task не заменить потому что Network::request должен нетривиально взаимодействовать с event loop'ом. В общем, как я написал в статье — коротких ответов нет, если в этот пример глубоко погружаться и честно и качественно дизайнить, то это отдельная большая тема.
Этот пример был про "давайте подумаем, как мог бы выглядеть клиентский код, чтобы было удобно," на идеальность предложенного API я конечно не претендую, да я и сам API не показал по сути X). За проработанном решением этой конкретной проблемы надо конечно идти к Eric Niebler — он над этим думает последние лет 5.
По сути run возвращает как раз такой токен, и консьюмит Task.
На тему добавления then к этому токену — это хороший вопрос, но надо смотреть на конкретный юзкейс. Мне в проектах на Qt обычно хватало примерно такого API:
Это, кстати, какой-то очень распространенный мем среди тех, кто настрадался с кодом, в котором кто-то наархитектурил. На практике я не видел ни одного примера, когда адекватный вдумчивый рефакторинг делал хуже конечным пользователям.
То есть — да, можно плохо код писать, а можно плохо рефакторить. И то, и другое — плохо.
На деле в кодовой базе на на несколько сотен тысяч строк кода без нормальных абстракций вы будете все время офигевать. Не говоря уже о кодовой базе на 1M+ LOC и больше.
Я полностью согласен с тем, что после C# возвращаться к плюсовому STL — это боль. std::ranges из C++20 и C++23 делают жизнь лучше, превращая алгоритмы в компонуемые абстракции — если еще не попробовали, то рекомендую.
Но до удобства LINQ плюсам конечно очень далеко. Попробуйте набагать в коде с ranges, компилятор выплюнет ошибок на мегабайт. В общем, плюсам есть над чем работать.
Это все не отменяет того, что STL попилен на абстракции очень правильно, а для своего времени вообще был гениальной библиотекой. В очень многих своих проектах я применял схожий с STL подход — данные отдельно, логика отдельно — и это помогало. И этот подход даже не про C++, случайный пример из головы — Redux из JS.
В данном конкретном примере даункаст по enum'у эффективнее, но можно и dynamic_cast использовать, концептуально это ничего не изменит.
std::variant здесь скорее всего не подойдет, потому что в реальности у вас будет несколько десятков типов событий, может быть даже сотня, засовывать это все в один std::variant — оверкилл.
В run передается контекст выполнения, в котором будет выполняться Task целиком. Идея здесь в том, что пока run не вызван, ничего не происходит — запрос по сети не отправляется и т.п. Это означает, что в пользовательском коде вы сначала можете спокойно выстроить цепочку вычислений через then, и уже после этого отправить ее на выполнение — например, в глобальном пуле потоков.
О, спасибо за ссылку! Ждем Qt 6.6!
Вот это кстати меня тоже всегда морозило в
QFuture
:Это прямое следствие того, что
QFuture
пытается быть всем сразу.Спасибо за комментарии!
Про
then
для future-like объекта, возвращаемого изrun
— а там нетthen
, в этом-то и смысл. При большом желании можно туда приделатьnotify
, который по завершению будет посылать Qt'шный сигнал сQt::QueuedConnection
, и проблемы как сQFuture
там не будет.Про лямбды — на лямбды в этом примере
Task
не заменить потому чтоNetwork::request
должен нетривиально взаимодействовать с event loop'ом. В общем, как я написал в статье — коротких ответов нет, если в этот пример глубоко погружаться и честно и качественно дизайнить, то это отдельная большая тема.Этот пример был про "давайте подумаем, как мог бы выглядеть клиентский код, чтобы было удобно," на идеальность предложенного API я конечно не претендую, да я и сам API не показал по сути X). За проработанном решением этой конкретной проблемы надо конечно идти к Eric Niebler — он над этим думает последние лет 5.
По сути
run
возвращает как раз такой токен, и консьюмитTask
.На тему добавления
then
к этому токену — это хороший вопрос, но надо смотреть на конкретный юзкейс. Мне в проектах на Qt обычно хватало примерно такого API:request(url)
.then(&parse)
.notify(object, &Object::method)
.run(globalThreadPool());
Где
notify()
работает так же, какQObject::connect
.Это, кстати, какой-то очень распространенный мем среди тех, кто настрадался с кодом, в котором кто-то наархитектурил. На практике я не видел ни одного примера, когда адекватный вдумчивый рефакторинг делал хуже конечным пользователям.
То есть — да, можно плохо код писать, а можно плохо рефакторить. И то, и другое — плохо.
На деле в кодовой базе на на несколько сотен тысяч строк кода без нормальных абстракций вы будете все время офигевать. Не говоря уже о кодовой базе на 1M+ LOC и больше.
Я полностью согласен с тем, что после C# возвращаться к плюсовому STL — это боль.
std::ranges
из C++20 и C++23 делают жизнь лучше, превращая алгоритмы в компонуемые абстракции — если еще не попробовали, то рекомендую.Но до удобства LINQ плюсам конечно очень далеко. Попробуйте набагать в коде с ranges, компилятор выплюнет ошибок на мегабайт. В общем, плюсам есть над чем работать.
Это все не отменяет того, что STL попилен на абстракции очень правильно, а для своего времени вообще был гениальной библиотекой. В очень многих своих проектах я применял схожий с STL подход — данные отдельно, логика отдельно — и это помогало. И этот подход даже не про C++, случайный пример из головы — Redux из JS.
В данном конкретном примере даункаст по
enum
'у эффективнее, но можно иdynamic_cast
использовать, концептуально это ничего не изменит.std::variant
здесь скорее всего не подойдет, потому что в реальности у вас будет несколько десятков типов событий, может быть даже сотня, засовывать это все в одинstd::variant
— оверкилл.Кстати да, ключевое слово type erasure я не назвал, а это именно оно!
В
run
передается контекст выполнения, в котором будет выполнятьсяTask
целиком. Идея здесь в том, что покаrun
не вызван, ничего не происходит — запрос по сети не отправляется и т.п. Это означает, что в пользовательском коде вы сначала можете спокойно выстроить цепочку вычислений черезthen
, и уже после этого отправить ее на выполнение — например, в глобальном пуле потоков.