web-dev-qa-db-ja.com

QtスロットとC ++ 11ラムダ

次のように初期化する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トリガー。ラムダを使用して送信者を取得するにはどうすればよいですか?

48
Addy

簡単な答えは、「できない」です。または、むしろ、sender()を使用したくない(または必要ない!)単にactionをキャプチャして使用します。

_//                                Important!
//                                   vvvv
connect(action, &QAction::triggered, this, [action, this]() {
    // use action as you wish
    ...
});
_

ファンクターのオブジェクトコンテキストとしてthisを指定すると、アクションまたはthisQObject)が存在しなくなった場合にファンクターが呼び出されなくなります。それ以外の場合、ファンクターはダングリングポインターを参照しようとします。

一般に、ダングリングポインター/参照の使用を回避するために、connectに渡されるファンクターのコンテキスト変数をキャプチャする場合、以下を保持する必要があります。

  1. connectのソースおよびターゲットオブジェクトへのポインターは、上記のように値によってキャプチャできます。ファンクターが呼び出されると、接続の両端が存在することが保証されます。

    _connect(a, &A::foo, b, [a, b]{});
    _

    abが異なるスレッドにあるシナリオでは、特別な注意が必要です。ファンクターが入力されると、一部のスレッドがどちらのオブジェクトも削除しないことを保証できません。

    オブジェクトは、その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(); });
    _
  2. 他のオブジェクトへの生のポインタは、それらが指すオブジェクトのライフタイムが接続のライフタイムと重複していることが確実な場合、値によってキャプチャできます。

    _static C c;
    auto p = &c;
    connect(..., [p]{});
    _
  3. オブジェクトへの参照についても同様:

    _static D d;
    connect(..., [&d]{});
    _
  4. QObjectから派生しないコピー不可のオブジェクトは、値によって共有ポインターを介してキャプチャする必要があります。

    _std::shared_ptr<E> e { new E };
    QSharedPointer<F> f { new F; }
    connect(..., [e,f]{});
    _
  5. 同じスレッドにあるQObjectsは、QPointer;によってキャプチャできます。ファンクタで使用する前に、その値を確認する必要があります。

    _QPointer<QObject> g { this->parent(); }
    connect(..., [g]{ if (g) ... });
    _
  6. 他のスレッドにあるQObjectsは、共有ポインターまたはウィークポインターでキャプチャする必要があります。破壊する前に親の設定を解除する必要があります。そうしないと、二重削除が発生します。

    _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) { ... }
    });
    _
73
Kuba Ober

ラムダをスロットとして使用するのは簡単です(たとえば、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) { });
12
MiB_Coder