次のように初期化するQActionアイテムがあります。
QAction* action = foo->addAction(tr("Some Action"));
connect(action, SIGNAL(triggered()), this, SLOT(onSomeAction()));
そして、onSomeActionは次のようになります。
void MyClass::onSomeAction()
{
QAction* caller = qobject_cast<QAction*>(sender());
Q_ASSERT(caller != nullptr);
// do some stuff with caller
}
これはうまく機能し、caller
オブジェクトを取得し、期待どおりに使用できます。次に、C++ 11の方法で次のようにオブジェクトを接続します。
connect(action, &QAction::triggered, [this]()
{
QAction* caller = qobject_cast<QAction*>(sender());
Q_ASSERT(caller != nullptr);
// do some stuff with caller
});
ただし、caller
は常にnullであるため、Q_ASSERT
トリガー。ラムダを使用して送信者を取得するにはどうすればよいですか?
簡単な答えは、「できない」です。または、むしろ、sender()
を使用したくない(または必要ない!)単にaction
をキャプチャして使用します。
_// Important!
// vvvv
connect(action, &QAction::triggered, this, [action, this]() {
// use action as you wish
...
});
_
ファンクターのオブジェクトコンテキストとしてthis
を指定すると、アクションまたはthis
(QObject
)が存在しなくなった場合にファンクターが呼び出されなくなります。それ以外の場合、ファンクターはダングリングポインターを参照しようとします。
一般に、ダングリングポインター/参照の使用を回避するために、connect
に渡されるファンクターのコンテキスト変数をキャプチャする場合、以下を保持する必要があります。
connect
のソースおよびターゲットオブジェクトへのポインターは、上記のように値によってキャプチャできます。ファンクターが呼び出されると、接続の両端が存在することが保証されます。
_connect(a, &A::foo, b, [a, b]{});
_
a
とb
が異なるスレッドにあるシナリオでは、特別な注意が必要です。ファンクターが入力されると、一部のスレッドがどちらのオブジェクトも削除しないことを保証できません。
オブジェクトは、そのthread()
で、またはthread() == nullptr
の場合は任意のスレッドでのみ破棄されるのが慣用的です。スレッドのイベントループがファンクターを呼び出すため、b
の場合、nullスレッドが問題になることはありません。スレッドがないとファンクターは呼び出されません。残念ながら、a
のスレッド内のb
の寿命についての保証はありません。したがって、代わりにアクションの必要な状態を値でキャプチャする方が安全です。したがって、a
の有効期間は問題になりません。
_// SAFE
auto aName = a->objectName();
connect(a, &A::foo, b, [aName, b]{ qDebug() << aName; });
// UNSAFE
connect(a, &A::foo, b, [a,b]{ qDebug() << a->objectName(); });
_
他のオブジェクトへの生のポインタは、それらが指すオブジェクトのライフタイムが接続のライフタイムと重複していることが確実な場合、値によってキャプチャできます。
_static C c;
auto p = &c;
connect(..., [p]{});
_
オブジェクトへの参照についても同様:
_static D d;
connect(..., [&d]{});
_
QObject
から派生しないコピー不可のオブジェクトは、値によって共有ポインターを介してキャプチャする必要があります。
_std::shared_ptr<E> e { new E };
QSharedPointer<F> f { new F; }
connect(..., [e,f]{});
_
同じスレッドにあるQObject
sは、QPointer
;によってキャプチャできます。ファンクタで使用する前に、その値を確認する必要があります。
_QPointer<QObject> g { this->parent(); }
connect(..., [g]{ if (g) ... });
_
他のスレッドにあるQObject
sは、共有ポインターまたはウィークポインターでキャプチャする必要があります。破壊する前に親の設定を解除する必要があります。そうしないと、二重削除が発生します。
_class I : public QObject {
...
~I() { setParent(nullptr); }
};
std::shared_ptr<I> i { new I };
connect(..., [i]{ ... });
std::weak_ptr<I> j { i };
connect(..., [j]{
auto jp = j.lock();
if (jp) { ... }
});
_
ラムダをスロットとして使用するのは簡単です(たとえば、QSpinboxからのイベントの場合)。
connect(spinboxObject, &QSpinBox::editingFinished, this, [this]() {<do something>});
ただし、これは信号が過負荷になっていない場合にのみ機能します(つまり、同じ名前で引数が異なる複数の信号があることを意味します)。
connect(spinboxObject, &QSpinBox::valueChange, this, [this]() {<do something>});
valueChanged(int)とvalueChanged(const QString&)の2つのオーバーロードされた信号が存在するため、コンパイルエラーが発生します。したがって、使用するバージョンを限定する必要があります。
connect(spinboxObject, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this](int newValue){ });
QOverload を使用すると、少し短くなります(読みやすくなります)。
connect(spinboxObject, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int newValue) { });