From 0f78e85421de113d73edf0d58b34e12ad188417c Mon Sep 17 00:00:00 2001 From: Sona Kurazyan Date: Mon, 30 Mar 2020 10:46:00 +0200 Subject: [PATCH] Add support of failure handler callbacks to QFuture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added QFuture::onFailed() method, which allows attaching handlers for exceptions that may occur in QFuture continuation chains. Task-number: QTBUG-81588 Change-Id: Iadeee99e3a7573207f6ca9f650ff9f7b6faa2cf7 Reviewed-by: MÃ¥rten Nordheim --- src/corelib/thread/qfuture.h | 24 ++ src/corelib/thread/qfuture.qdoc | 83 +++++ src/corelib/thread/qfuture_impl.h | 175 ++++++++- src/corelib/thread/qfutureinterface.h | 10 + .../corelib/thread/qfuture/tst_qfuture.cpp | 333 ++++++++++++++++++ 5 files changed, 624 insertions(+), 1 deletion(-) diff --git a/src/corelib/thread/qfuture.h b/src/corelib/thread/qfuture.h index 0507cfb59e..bfca16b641 100644 --- a/src/corelib/thread/qfuture.h +++ b/src/corelib/thread/qfuture.h @@ -162,6 +162,12 @@ public: template QFuture> then(QThreadPool *pool, Function &&function); +#ifndef QT_NO_EXCEPTIONS + template::HasExtraArgs>> + QFuture onFailed(Function &&handler); +#endif + class const_iterator { public: @@ -275,6 +281,11 @@ private: template friend class QtPrivate::Continuation; +#ifndef QT_NO_EXCEPTIONS + template + friend class QtPrivate::FailureHandler; +#endif + using QFuturePrivate = std::conditional_t, QFutureInterfaceBase, QFutureInterface>; @@ -335,6 +346,19 @@ QFuture::template ResultType> QFuture::then(QTh return promise.future(); } +#ifndef QT_NO_EXCEPTIONS + +template +template +QFuture QFuture::onFailed(Function &&handler) +{ + QFutureInterface promise(QFutureInterfaceBase::State::Pending); + QtPrivate::FailureHandler::create(std::forward(handler), this, promise); + return promise.future(); +} + +#endif + inline QFuture QFutureInterface::future() { return QFuture(this); diff --git a/src/corelib/thread/qfuture.qdoc b/src/corelib/thread/qfuture.qdoc index 1519f20cf0..8b354fe114 100644 --- a/src/corelib/thread/qfuture.qdoc +++ b/src/corelib/thread/qfuture.qdoc @@ -846,6 +846,8 @@ \note If the parent future gets canceled, its continuations will also be canceled. + + \sa onFailed() */ /*! \fn template template QFuture::ResultType> QFuture::then(QtFuture::Launch policy, Function &&function) @@ -877,6 +879,8 @@ future.then(QtFuture::Launch::Async, [](int res){ ... }) .then(QtFuture::Launch::Inherit, [](int res2){ ... }); \endcode + + \sa onFailed() */ /*! \fn template template QFuture::ResultType> QFuture::then(QThreadPool *pool, Function &&function) @@ -888,4 +892,83 @@ computations if desired. When the asynchronous computation represented by this future finishes, \a function will be invoked in a separate thread taken from the QThreadPool \a pool. + + \sa onFailed() +*/ + +/*! \fn template template QFuture QFuture::onFailed(Function &&handler) + + \since 6.0 + + Attaches a failure handler to this future, to handle any exceptions that may + have been generated. Returns a QFuture of the parent type. The handler will + be invoked only in case of an exception, in the same thread as the parent + future has been running. \a handler is a callable which takes either no argument + or one argument, to filter by specific error types similar to + \l {https://en.cppreference.com/w/cpp/language/try_catch} {catch} statement. + + For example: + + \code + QFuture future = ...; + auto resultFuture = future.then([](int res) { + ... + throw Error(); + ... + }).onFailed([](const Error &e) { + // Handle exceptions of type Error + ... + return -1; + }).onFailed([] { + // Handle all other types of errors + ... + return -1; + }); + + auto result = resultFuture.result(); // result is -1 + + \endcode + + If there are multiple handlers attached, the first handler that matches with the + thrown exception type will be invoked. For example: + + \code + QFuture future = ...; + future.then([](int res) { + ... + throw std::runtime_error("message"); + ... + }).onFailed([](const std::exception &e) { + // This handler will be invoked + }).onFailed([](const std::runtime_error &e) { + // This handler won't be invoked, because of the handler above. + }); + \endcode + + If none of the handlers matches with the thrown exception type, the exception + will be propagated to the resulted future: + + \code + QFuture future = ...; + auto resultFuture = future.then([](int res) { + ... + throw Error("message"); + ... + }).onFailed([](const std::exception &e) { + // Won't be invoked + }).onFailed([](const QException &e) { + // Won't be invoked + }); + + try { + auto result = resultFuture.result(); + } catch(...) { + // Handle the exception + } + \endcode + + \note You can always attach a handler taking no argument, to handle all exception + types and avoid writing the try-catch block. + + \sa then() */ diff --git a/src/corelib/thread/qfuture_impl.h b/src/corelib/thread/qfuture_impl.h index 903b3787e3..4a57ba7846 100644 --- a/src/corelib/thread/qfuture_impl.h +++ b/src/corelib/thread/qfuture_impl.h @@ -109,6 +109,64 @@ struct ResultTypeHelper< using ResultType = std::invoke_result_t>; }; +// Helpers to resolve argument types of callables. +template +struct ArgsType; + +template +struct ArgsType +{ + using First = Arg; + static const bool HasExtraArgs = (sizeof...(Args) > 0); +}; + +template<> +struct ArgsType<> +{ + using First = void; + static const bool HasExtraArgs = false; +}; + +template +struct ArgResolver : ArgResolver::operator())> +{ +}; + +template +struct ArgResolver : public ArgsType +{ +}; + +template +struct ArgResolver : public ArgsType +{ +}; + +template +struct ArgResolver : public ArgsType +{ +}; + +template +struct ArgResolver : public ArgsType +{ +}; + +template +struct ArgResolver : public ArgsType +{ +}; + +template +struct ArgResolver : public ArgsType +{ +}; + +template +struct ArgResolver : public ArgsType +{ +}; + template class Continuation { @@ -186,6 +244,37 @@ private: QThreadPool *threadPool; }; +#ifndef QT_NO_EXCEPTIONS + +template +class FailureHandler +{ +public: + static void create(Function &&function, QFuture *future, + const QFutureInterface &promise); + + FailureHandler(Function &&func, const QFuture &f, + const QFutureInterface &p) + : promise(p), parentFuture(f), handler(std::forward(func)) + { + } + +public: + void run(); + +private: + template + void handleException(); + void handleAllExceptions(); + +private: + QFutureInterface promise; + const QFuture parentFuture; + Function handler; +}; + +#endif + template void Continuation::runFunction() { @@ -297,7 +386,7 @@ void Continuation::create(Function &&fun p.setLaunchAsync(launchAsync); - auto continuation = [continuationJob, policy, launchAsync]() mutable { + auto continuation = [continuationJob, launchAsync]() mutable { bool isLaunched = continuationJob->execute(); // If continuation is successfully launched, AsyncContinuation will be deleted // by the QThreadPool which has started it. Synchronous continuation will be @@ -337,6 +426,90 @@ void Continuation::create(Function &&fun f->d.setContinuation(continuation); } +#ifndef QT_NO_EXCEPTIONS + +template +void FailureHandler::create(Function &&function, QFuture *future, + const QFutureInterface &promise) +{ + Q_ASSERT(future); + + FailureHandler *failureHandler = new FailureHandler( + std::forward(function), *future, promise); + + auto failureContinuation = [failureHandler]() mutable { + failureHandler->run(); + delete failureHandler; + }; + + future->d.setContinuation(std::move(failureContinuation)); +} + +template +void FailureHandler::run() +{ + Q_ASSERT(parentFuture.isFinished()); + + promise.reportStarted(); + + if (parentFuture.d.exceptionStore().hasException()) { + using ArgType = typename QtPrivate::ArgResolver::First; + if constexpr (std::is_void_v) { + handleAllExceptions(); + } else { + handleException(); + } + } else { + if constexpr (!std::is_void_v) + promise.reportResult(parentFuture.result()); + } + promise.reportFinished(); +} + +template +template +void FailureHandler::handleException() +{ + try { + parentFuture.d.exceptionStore().throwPossibleException(); + } catch (const ArgType &e) { + try { + // Handle exceptions matching with the handler's argument type + if constexpr (std::is_void_v) { + handler(e); + } else { + promise.reportResult(handler(e)); + } + } catch (...) { + promise.reportException(std::current_exception()); + } + } catch (...) { + // Exception doesn't match with handler's argument type, propagate + // the exception to be handled later. + promise.reportException(std::current_exception()); + } +} + +template +void FailureHandler::handleAllExceptions() +{ + try { + parentFuture.d.exceptionStore().throwPossibleException(); + } catch (...) { + try { + if constexpr (std::is_void_v) { + handler(); + } else { + promise.reportResult(handler()); + } + } catch (...) { + promise.reportException(std::current_exception()); + } + } +} + +#endif // QT_NO_EXCEPTIONS + } // namespace QtPrivate QT_END_NAMESPACE diff --git a/src/corelib/thread/qfutureinterface.h b/src/corelib/thread/qfutureinterface.h index c8298d0742..04828609ba 100644 --- a/src/corelib/thread/qfutureinterface.h +++ b/src/corelib/thread/qfutureinterface.h @@ -63,6 +63,11 @@ class QFutureWatcherBasePrivate; namespace QtPrivate { template class Continuation; + +#ifndef QT_NO_EXCEPTIONS +template +class FailureHandler; +#endif } class Q_CORE_EXPORT QFutureInterfaceBase @@ -158,6 +163,11 @@ private: template friend class QtPrivate::Continuation; +#ifndef QT_NO_EXCEPTIONS + template + friend class QtPrivate::FailureHandler; +#endif + protected: void setContinuation(std::function func); void runContinuation() const; diff --git a/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp b/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp index 4e20741055..1caa386638 100644 --- a/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp +++ b/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp @@ -104,6 +104,8 @@ private slots: #ifndef QT_NO_EXCEPTIONS void thenOnExceptionFuture(); void thenThrows(); + void onFailed(); + void onFailedTestCallables(); #endif void takeResults(); void takeResult(); @@ -2149,6 +2151,337 @@ void tst_QFuture::thenThrows() } } +void tst_QFuture::onFailed() +{ + // Ready exception void future + { + int checkpoint = 0; + auto future = createExceptionFuture().then([&] { checkpoint = 1; }).onFailed([&] { + checkpoint = 2; + }); + + try { + future.waitForFinished(); + } catch (...) { + checkpoint = 3; + } + QCOMPARE(checkpoint, 2); + } + + // std::exception handler + { + QFutureInterface promise; + + int checkpoint = 0; + auto then = promise.future() + .then([&](int res) { + throw std::exception(); + return res; + }) + .then([&](int res) { return res + 1; }) + .onFailed([&](const QException &) { + checkpoint = 1; + return -1; + }) + .onFailed([&](const std::exception &) { + checkpoint = 2; + return -1; + }) + .onFailed([&] { + checkpoint = 3; + return -1; + }); + + promise.reportStarted(); + promise.reportResult(1); + promise.reportFinished(); + + int res = 0; + try { + res = then.result(); + } catch (...) { + checkpoint = 4; + } + QCOMPARE(checkpoint, 2); + QCOMPARE(res, -1); + } + + // then() throws an exception derived from QException + { + QFutureInterface promise; + + int checkpoint = 0; + auto then = promise.future() + .then([&](int res) { + throw DerivedException(); + return res; + }) + .then([&](int res) { return res + 1; }) + .onFailed([&](const QException &) { + checkpoint = 1; + return -1; + }) + .onFailed([&](const std::exception &) { + checkpoint = 2; + return -1; + }) + .onFailed([&] { + checkpoint = 3; + return -1; + }); + + promise.reportStarted(); + promise.reportResult(1); + promise.reportFinished(); + + int res = 0; + try { + res = then.result(); + } catch (...) { + checkpoint = 4; + } + QCOMPARE(checkpoint, 1); + QCOMPARE(res, -1); + } + + // then() throws a custom exception + { + QFutureInterface promise; + + int checkpoint = 0; + auto then = promise.future() + .then([&](int res) { + throw TestException(); + return res; + }) + .then([&](int res) { return res + 1; }) + .onFailed([&](const QException &) { + checkpoint = 1; + return -1; + }) + .onFailed([&](const std::exception &) { + checkpoint = 2; + return -1; + }) + .onFailed([&] { + checkpoint = 3; + return -1; + }); + + promise.reportStarted(); + promise.reportResult(1); + promise.reportFinished(); + + int res = 0; + try { + res = then.result(); + } catch (...) { + checkpoint = 4; + } + QCOMPARE(checkpoint, 3); + QCOMPARE(res, -1); + } + + // Custom exception handler + { + struct TestException + { + }; + + QFutureInterface promise; + + int checkpoint = 0; + auto then = promise.future() + .then([&](int res) { + throw TestException(); + return res; + }) + .then([&](int res) { return res + 1; }) + .onFailed([&](const QException &) { + checkpoint = 1; + return -1; + }) + .onFailed([&](const TestException &) { + checkpoint = 2; + return -1; + }) + .onFailed([&] { + checkpoint = 3; + return -1; + }); + + promise.reportStarted(); + promise.reportResult(1); + promise.reportFinished(); + + int res = 0; + try { + res = then.result(); + } catch (...) { + checkpoint = 4; + } + QCOMPARE(checkpoint, 2); + QCOMPARE(res, -1); + } + + // Handle all exceptions + { + QFutureInterface promise; + + int checkpoint = 0; + auto then = promise.future() + .then([&](int res) { + throw QException(); + return res; + }) + .then([&](int res) { return res + 1; }) + .onFailed([&] { + checkpoint = 1; + return -1; + }) + .onFailed([&](const QException &) { + checkpoint = 2; + return -1; + }); + + promise.reportStarted(); + promise.reportResult(1); + promise.reportFinished(); + + int res = 0; + try { + res = then.result(); + } catch (...) { + checkpoint = 3; + } + QCOMPARE(checkpoint, 1); + QCOMPARE(res, -1); + } + + // Handler throws exception + { + QFutureInterface promise; + + int checkpoint = 0; + auto then = promise.future() + .then([&](int res) { + throw QException(); + return res; + }) + .then([&](int res) { return res + 1; }) + .onFailed([&](const QException &) { + checkpoint = 1; + throw QException(); + return -1; + }) + .onFailed([&] { + checkpoint = 2; + return -1; + }); + + promise.reportStarted(); + promise.reportResult(1); + promise.reportFinished(); + + int res = 0; + try { + res = then.result(); + } catch (...) { + checkpoint = 3; + } + QCOMPARE(checkpoint, 2); + QCOMPARE(res, -1); + } + + // No handler for exception + { + QFutureInterface promise; + + int checkpoint = 0; + auto then = promise.future() + .then([&](int res) { + throw QException(); + return res; + }) + .then([&](int res) { return res + 1; }) + .onFailed([&](const std::exception &) { + checkpoint = 1; + throw std::exception(); + return -1; + }) + .onFailed([&](QException &) { + checkpoint = 2; + return -1; + }); + + promise.reportStarted(); + promise.reportResult(1); + promise.reportFinished(); + + int res = 0; + try { + res = then.result(); + } catch (...) { + checkpoint = 3; + } + QCOMPARE(checkpoint, 3); + QCOMPARE(res, 0); + } +} + +template +bool runForCallable(Callable &&handler) +{ + QFuture future = createExceptionResultFuture() + .then([&](int) { return 1; }) + .onFailed(std::forward(handler)); + + int res = 0; + try { + res = future.result(); + } catch (...) { + return false; + } + return res == -1; +} + +int foo() +{ + return -1; +} + +void tst_QFuture::onFailedTestCallables() +{ + QVERIFY(runForCallable([&] { return -1; })); + QVERIFY(runForCallable(foo)); + QVERIFY(runForCallable(&foo)); + + std::function func = foo; + QVERIFY(runForCallable(func)); + + struct Functor1 + { + int operator()() { return -1; } + static int foo() { return -1; } + }; + QVERIFY(runForCallable(Functor1())); + QVERIFY(runForCallable(Functor1::foo)); + + struct Functor2 + { + int operator()() const { return -1; } + static int foo() { return -1; } + }; + QVERIFY(runForCallable(Functor2())); + + struct Functor3 + { + int operator()() const noexcept { return -1; } + static int foo() { return -1; } + }; + QVERIFY(runForCallable(Functor3())); +} + #endif // QT_NO_EXCEPTIONS void tst_QFuture::testSingleResult(const UniquePtr &p)