Skip to content

Commit

Permalink
QPromise from Qt signal helpers (enhancement simonbrunel#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
pwuertz committed May 22, 2018
1 parent af32f5b commit 51146f2
Show file tree
Hide file tree
Showing 10 changed files with 474 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Requires [Qt 5.6](https://www.qt.io/download/) (or later) with [C++11 support en
* [Getting Started](qtpromise/getting-started.md)
* [Thread-Safety](qtpromise/thread-safety.md)
* [QtConcurrent](qtpromise/qtconcurrent.md)
* [Signals](qtpromise/qtsignals.md)
* [API Reference](qtpromise/api-reference.md)

## License
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### QtPromise for C++
* [Getting Started](qtpromise/getting-started.md)
* [QtConcurrent](qtpromise/qtconcurrent.md)
* [Signals](qtpromise/qtsignals.md)
* [Thread-Safety](qtpromise/thread-safety.md)
* [API Reference](qtpromise/api-reference.md)
* [QPromise](qtpromise/qpromise/constructor.md)
Expand Down
4 changes: 3 additions & 1 deletion docs/qtpromise/helpers/qpromise.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ auto promise = qPromise(42); // QPromise<int>
auto promise = qPromise(QString("foo")); // QPromise<QString>
```

This method also allows to convert `QFuture<T>` to `QPromise<T>` delayed until the `QFuture` is finished ([read more](../qtconcurrent.md#convert)).
This method also allows to create promises from other asynchronous sources:
- `QPromise<T>` from `QFuture<T>`, resolved after `QFuture` finished ([read more](../qtconcurrent.md#convert))
- `QPromise<T>` from Qt signals ([read more](../qtsignals.md))
86 changes: 86 additions & 0 deletions docs/qtpromise/qtsignals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
## Qt Signals

QtPromise supports creating promises that are resolved or rejected by regular Qt signals.

Use the [`qPromise()`](helpers/qpromise.md) helper to create a `QPromise<T>` resolved by a signal emitted from an object, passing on the signal's value if it has one:
```cpp
// signal: void Object::stringSignal(const QString&)
QPromise<QString> p = qPromise(
obj, &Object::stringSignal
);
p.then([](const QString& value) {
// resolved with `value` from stringSignal
});
```

A rejection signal is added to the promise by appending a second object/signal pair:
```cpp
// signal: void Object::intRejectSignal(int)
QPromise<void> p = qPromise(
obj, &Object::voidResolveSignal,
obj, &Object::intRejectSignal,
);
p.fail([](int value) {
// rejected with `value` from intRejectSignal
});
```

Since promises require a rejection reason, void rejection signals need to be followed by a rejection value:
```cpp
// signal: void Object::voidRejectSignal()
QPromise<void> p = qPromise(
obj, &Object::voidResolveSignal,
obj, &Object::voidRejectSignal, QString("Reject reason")
);
p.fail([](const QString& reason) {
// rejected with `reason` assigned to voidRejectSignal
});
```

`qPromise()` allows constructing promises from multiple rejection signals:
```cpp
QPromise<void> p = qPromise(
obj, &Object::voidResolveSignal,
obj, &Object::voidRejectSignal1, QString("Reason 1")
obj, &Object::intRejectSignal2,
obj, &Object::destroyed, QException()
);
p.fail([](const QString& reason) {
// rejected with `reason` assigned to voidRejectSignal1
}).fail([](int value) {
// rejected with `value` from intRejectSignal2
}).fail([](const QException&) {
// QObject destroyed before promise resolved
});
```

## Manually creating `QPromise<T>` from signals

The `qPromise()` helper may not cover all possible use cases. If a situation requires a manual promise construction from signals, care must be taken to guarantee that all signals involved in the promise creation are disconnected after fulfillment.

The easiest option is to simply delete the signal source after the promise is handled:
```cpp
// in promise returning method
return QPromise<void>([&](const auto& resolve, const auto& reject) {
QObject::connect(obj, &Object::signal, [=]() {
// {... resolve/reject ...}
object->deleteLater();
});
});
```

Sometimes the deletion of the signal source may not be possible. In this case, all signal connections must be tracked and disconnected manually. QtPromise provides the `QPromiseConnections` helper to handle this conveniently:
```cpp
// in promise returning method
QPromiseConnections connections;
return QPromise<void>([&](const auto& resolve, const auto& reject) {
connections << QObject::connect(obj1, &Object::signal1, [=]() {
// {... resolve/reject ...}
});
connections << QObject::connect(obj2, &Object::signal2, [=]() {
// {... resolve/reject ...}
});
}.finally([=]() {
connections.disconnect();
});
```
1 change: 1 addition & 0 deletions include/QtPromise
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "../src/qtpromise/qpromise.h"
#include "../src/qtpromise/qpromisefuture.h"
#include "../src/qtpromise/qpromisesignals.h"
#include "../src/qtpromise/qpromisehelpers.h"

#endif // ifndef QTPROMISE_MODULE_H
161 changes: 161 additions & 0 deletions src/qtpromise/qpromisesignals.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#ifndef QTPROMISE_QPROMISESIGNALS_H
#define QTPROMISE_QPROMISESIGNALS_H

#include <QObject>
#include <memory>
#include "qpromise.h"


namespace QtPromise {

class QPromiseConnections : private std::shared_ptr<std::vector<QMetaObject::Connection>>
{
using Connections = std::vector<QMetaObject::Connection>;
using Base = std::shared_ptr<Connections>;

public:
QPromiseConnections() : Base(new Connections, [](Connections* c) {
if (!c->empty()) {
qWarning("QPromiseConnections: destroyed with unhandled connections");
disconnectConnections(c);
}
delete c;
}) { }

void operator<<(QMetaObject::Connection &&other) const
{
get()->emplace_back(std::move(other));
}

size_t count() const { return get()->size(); }

void disconnect() const { disconnectConnections(get()); }

private:
static void disconnectConnections(Connections* c) Q_DECL_NOEXCEPT
{
for (const auto& connection: *c) { QObject::disconnect(connection); }
c->clear();
}
};

} // namespace QtPromise


namespace QtPromisePrivate {

// Deduce promise types from Qt signals
// TODO: Suppress QPrivateSignal trailing private signal args
// TODO: Support deducing tuple from args once MSVC 2017 is required
template<typename... Args>
using TFromSignalArgs = Unqualified<typename ArgsTraits<Args...>::first>;

template<typename... Args>
using PromiseFromSignalArgs = typename QtPromise::QPromise<TFromSignalArgs<Args...>>;

// Connect void signal to QPromiseResolve
template<class Sender, typename SignalClass>
void connectSignalToResolve(const QtPromise::QPromiseConnections& connections,
const QtPromise::QPromiseResolve<void>& resolve,
const Sender* sender,
void (SignalClass::* signal)())
{
connections << QObject::connect(sender, signal, [=]() {
resolve();
connections.disconnect();
});
}

// Connect signal with one argument to QPromiseResolve
template<class Sender, typename SignalClass, typename... SignalArgs, typename T>
typename std::enable_if<(ArgsTraits<SignalArgs...>::count >= 1)>::type
connectSignalToResolve(const QtPromise::QPromiseConnections& connections,
const QtPromise::QPromiseResolve<T>& resolve,
const Sender* sender,
void (SignalClass::* signal)(SignalArgs... args))
{
connections << QObject::connect(sender, signal, [=](const T& value) {
resolve(value);
connections.disconnect();
});
}

template<typename T>
static inline void
connectSignalsToReject(const QtPromise::QPromiseConnections&,
const QtPromise::QPromiseReject<T>&)
{ }

// Connect void signal & reject reason to QPromiseReject
template<typename T, class Sender, typename SignalClass, typename Reason, typename... Args>
static inline void
connectSignalsToReject(const QtPromise::QPromiseConnections& connections,
const QtPromise::QPromiseReject<T>& reject,
const Sender* sender,
void (SignalClass::* signal)(),
Reason&& reason,
Args&&... args)
{
// TODO: Capture `reason` by move/forward once QtPromise requires C++14
connections << QObject::connect(sender, signal, [=]() {
reject(reason);
connections.disconnect();
});
connectSignalsToReject(connections, reject, std::forward<Args>(args)...);
}

// Connect signal with one argument to QPromiseReject
template<typename T, class Sender, typename SignalClass, typename... SignalArgs, typename... Args>
static inline typename std::enable_if<(ArgsTraits<SignalArgs...>::count >= 1)>::type
connectSignalsToReject(const QtPromise::QPromiseConnections& connections,
const QtPromise::QPromiseReject<T>& reject,
const Sender* sender,
void (SignalClass::* signal)(SignalArgs...),
Args&&... args)
{
using Value = typename ArgsTraits<SignalArgs...>::first;
connections << QObject::connect(sender, signal, [=](const Value& value) {
reject(value);
connections.disconnect();
});
connectSignalsToReject(connections, reject, std::forward<Args>(args)...);
}

} // namespace QtPromisePrivate


namespace QtPromise {

// QPromise<T> from resolve signal
template<class Sender, typename SignalClass, typename... SignalArgs>
static inline typename QtPromisePrivate::PromiseFromSignalArgs<SignalArgs...>
qPromise(const Sender* sender,
void (SignalClass::* signal)(SignalArgs...))
{
using T = typename QtPromisePrivate::PromiseFromSignalArgs<SignalArgs...>::Type;

return QPromise<T>([&](const QPromiseResolve<T>& resolve) {
QPromiseConnections connections;
QtPromisePrivate::connectSignalToResolve(connections, resolve, sender, signal);
});
}

// QPromise<T> from resolve and reject signals
template<class Sender, typename SignalClass, typename... SignalArgs, typename... Args>
static inline QtPromisePrivate::PromiseFromSignalArgs<SignalArgs...>
qPromise(const Sender* sender,
void (SignalClass::* signal)(SignalArgs...),
Args&&... args)
{
using T = typename QtPromisePrivate::PromiseFromSignalArgs<SignalArgs...>::Type;

return QPromise<T>([&](const QPromiseResolve<T>& resolve, const QPromiseReject<T>& reject) {
QPromiseConnections connections;
QtPromisePrivate::connectSignalToResolve(connections, resolve, sender, signal);
QtPromisePrivate::connectSignalsToReject(connections, reject, std::forward<Args>(args)...);
});
}

} // namespace QtPromise

#endif // QTPROMISE_QPROMISESIGNALS_H
3 changes: 2 additions & 1 deletion src/qtpromise/qtpromise.pri
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ HEADERS += \
$$PWD/qpromiseerror.h \
$$PWD/qpromisefuture.h \
$$PWD/qpromiseglobal.h \
$$PWD/qpromisehelpers.h
$$PWD/qpromisehelpers.h \
$$PWD/qpromisesignals.h
1 change: 1 addition & 0 deletions tests/auto/qtpromise/qtpromise.pro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ TEMPLATE = subdirs
SUBDIRS += \
benchmark \
future \
signals \
helpers \
qpromise \
requirements \
Expand Down
4 changes: 4 additions & 0 deletions tests/auto/qtpromise/signals/signals.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
TARGET = tst_signals
SOURCES += $$PWD/tst_signals.cpp

include(../qtpromise.pri)
Loading

0 comments on commit 51146f2

Please # to comment.