Let's take an example to show why the rule/design principle is important.
Requirements: Read a file by a thread, then upload to a server. It should show a loading spinner until it is finished.
QFuture<void> ExampleClass::readAndUpload(QString file) { auto read = [](QString file) -> QByteArray { // ... }; auto upload = [](QByteArray data) { // ... return observe(reply, &QNetworkReply::finished) }; auto cleanup = []() { qDebug() << "Done"; // Now it only print a done on finished, but it could be changed to do validation / state changes }; return observe(QtConcurrent::run(read, file)).subscribe(upload).subscribe(cleanup).future() }That is a typical example of an asynchronous flow with multiple steps. That may look complicated, but this kind of problem is complicated anyway. Alternative solutions (except async/await) are usually more troublesome to manage.
AsyncFuture solves this by chaining the callback functions. A callback function may take input from the previous step and return another QFuture for next step to wait for (optional).
Therefore, it is very important that a chain function (subscribe) should return a new object to represent a new step. As it doesn't know how many steps are needed, user terminate the chain when he/she think that is enough.
observe(f1).combine(f2).combine(f3) and observe(future).onCompleted().onCanceled() are bad because they returns an existing object instead of create a new (No matter that is QFuture or custom Promise). The new API are inconsistent with subscribe . But subscribe is more important because it solves the most troublesome problem in asynchronous programming.
p.s That should be fine.
auto observer = observe(future); observer.onCompleted(callback1); observer.onCanceled(callback2); // observer.onCompleted(callback1).onCanceled(callback2) are not equivalent to above code