web-dev-qa-db-ja.com

スタックオブジェクトのQt信号とパラメータを参照として

次のコードを含む「ダングリングリファレンス」を(myQtSignalに接続された最終的なスロットに)使用できますか?

class Test : public QObject
{
    Q_OBJECT

signals:
    void myQtSignal(const FooObject& obj);

public:
    void sendSignal(const FooObject& fooStackObject)
    {
        emit  myQtSignal(fooStackObject);
    }
};

void f()
{
    FooObject fooStackObject;
    Test t;
    t.sendSignal(fooStackObject);
}

int main()
{
    f();
    std::cin.ignore();
    return 0;
}

特に、emitとslotが同じスレッドで実行されていない場合。

25
Guillaume07

2015年4月20日更新

元々スタックに割り当てられたオブジェクトへの参照を渡すことは、そのオブジェクトのアドレスを渡すことと同等であると私は信じていました。したがって、コピー(または共有ポインター)を格納するラッパーがない場合、キューに入れられたスロット接続は、不良データを使用して終了する可能性があります。

しかし、@ BenjaminTと@cgmbによって、Qtには実際にはconst参照パラメーターの特別な処理があることがわかりました。コピーコンストラクターを呼び出し、コピーされたオブジェクトを格納してスロット呼び出しに使用します。渡した元のオブジェクトがスロットの実行までに破壊された場合でも、スロットが取得する参照は完全に異なるオブジェクトになります。

機械的な詳細については、 @ cgmbの回答 を読むことができます。しかし、ここに簡単なテストがあります:

#include <iostream>
#include <QCoreApplication>
#include <QDebug>
#include <QTimer>

class Param {
public:
    Param () {}
    Param (Param const &) {
        std::cout << "Calling Copy Constructor\n";
    }
};

class Test : public QObject {
    Q_OBJECT

public:
    Test () {
        for (int index = 0; index < 3; index++)
            connect(this, &Test::transmit, this, &Test::receive,
                Qt::QueuedConnection);
    }

    void run() {
        Param p;
        std::cout << "transmitting with " << &p << " as parameter\n";
        emit transmit(p);
        QTimer::singleShot(200, qApp, &QCoreApplication::quit);
    }

signals:
    void transmit(Param const & p);
public slots:
    void receive(Param const & p) {
        std::cout << "receive called with " << &p << " as parameter\n";
    }
};

...そしてメイン:

#include <QCoreApplication>
#include <QTimer>

#include "param.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // name "Param" must match type name for references to work (?)
    qRegisterMetaType<Param>("Param"); 

    Test t;

    QTimer::singleShot(200, qApp, QCoreApplication::quit);
    return a.exec();
}

これを実行すると、3つのスロット接続のそれぞれについて、コピーコンストラクターを介してParamの個別のコピーが作成されることがわかります。

Calling Copy Constructor
Calling Copy Constructor
Calling Copy Constructor
receive called with 0x1bbf7c0 as parameter
receive called with 0x1bbf8a0 as parameter
receive called with 0x1bbfa00 as parameter

Qtがとにかくコピーを作成するだけの場合、「参照渡し」がどのように役立つのか疑問に思われるかもしれません。ただし、常にコピーされるとは限りません...接続タイプによって異なります。 Qt::DirectConnectionに変更すると、コピーは作成されません。

transmitting with 0x7ffebf241147 as parameter
receive called with 0x7ffebf241147 as parameter
receive called with 0x7ffebf241147 as parameter
receive called with 0x7ffebf241147 as parameter

そして、値渡しに切り替えた場合、特にQt::QueuedConnectionの場合、実際にはより多くの中間コピーが得られます。

Calling Copy Constructor
Calling Copy Constructor
Calling Copy Constructor
Calling Copy Constructor
Calling Copy Constructor
receive called with 0x7fff15146ecf as parameter
Calling Copy Constructor
receive called with 0x7fff15146ecf as parameter
Calling Copy Constructor
receive called with 0x7fff15146ecf as parameter

しかし、ポインタを渡すことは特別な魔法をしません。そのため、元の回答に記載されている問題があります。これについては以下で説明します。しかし、参照処理は単なる別の獣であることが判明しました。

元の回答

はい、プログラムがマルチスレッドの場合、これは危険な場合があります。そして、そうでなくても、それは一般的に貧弱なスタイルです。実際には、信号接続とスロット接続を介してオブジェクトを値で渡す必要があります。

Qtは「暗黙的に共有される型」をサポートしているため、QImageのようなものを「値で」渡すと、誰かが受け取った値に書き込まない限り、コピーは作成されないことに注意してください。

http://qt-project.org/doc/qt-5/implicit-sharing.html

問題は基本的に信号とスロットとは何の関係もありません。 C++には、オブジェクトがどこかで参照されている間、またはコードの一部が呼び出しスタックで実行されている場合でも、オブジェクトを削除するさまざまな方法があります。この問題は、コードを制御できず、適切な同期を使用していないコードでも簡単に発生する可能性があります。 QSharedPointerの使用などの手法が役立ちます。

削除シナリオをより適切に処理するためにQtが提供するその他の便利な機能がいくつかあります。破棄したいオブジェクトがあるが、それが現在使用されている可能性があることに気付いている場合は、QObject :: deleteLater()メソッドを使用できます。

http://qt-project.org/doc/qt-5/qobject.html#deleteLater

それは私にとって数回役に立ちます。もう1つの便利なことは、QObject :: destroyed()シグナルです。

http://qt-project.org/doc/qt-5/qobject.html#destroyed

対象を何年も続けて申し訳ありませんが、それはグーグルに出てきました。将来の読者を誤解させる可能性があるため、HostileForkの回答を明確にしたいと思います。

信号/スロット接続が機能する方法のおかげで、Qt信号への参照を渡すことは危険ではありません。

  • 接続が直接の場合、接続されたスロットは直接呼び出されます。 emit MySignal(my_string)が、直接接続されているすべてのスロットが実行されたことを返す場合。
  • 接続がキューに入れられている場合、Qtは参照のコピーを作成します。したがって、スロットが呼び出されると、参照によって渡された変数の独自の有効なコピーがあります。ただし、これは、パラメーターをコピーするために、パラメーターがQtが認識しているタイプでなければならないことを意味します。

http://qt-project.org/doc/qt-5.1/qtcore/qt.html#ConnectionType-enum

23
Benjamin T

いいえ、ぶら下がっている参照に遭遇することはありません。少なくとも、スロットが通常の機能でも問題を引き起こすようなことをしない限り、そうではありません。

Qt :: DirectionConnection

これらのスロットはすぐに呼び出されるため、これは直接接続では問題にならないことを一般的に受け入れることができます。すべてのスロットが呼び出されるまで、信号の放出はブロックされます。それが発生すると、emit myQtSignal(fooStackObject);は通常の関数と同じように戻ります。実際、myQtSignal(fooStackObject);は通常の関数です!放出キーワードは完全にあなたの利益のためです-それは何もしません。シグナル関数は、そのコードがQtのコンパイラーmocによって生成されるため、特別なものです。

Qt :: QueuedConnection

ベンジャミンTは、議論がコピーされていることをドキュメントで指摘していますが、これが内部でどのように機能するかを調査することは啓発的だと思います(少なくとも第4四半期)。

プロジェクトをコンパイルし、生成されたmocファイルを検索することから始めると、次のようなものを見つけることができます。

_// SIGNAL 0
void Test::myQtSignal(const FooObject & _t1)
{
    void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
_

したがって、基本的には、QObject、QObjectの型のメタオブジェクト、シグナルID、シグナルが受け取った各引数へのポインターなど、いくつかのものを_QMetaObject::activate_に渡します。

_QMetaObject::activate_ を調べると、qobject.cppで宣言されていることがわかります。これは、QObjectsの動作に不可欠なものです。この質問に関係のないものをいくつか閲覧した後、キューに入れられた接続の動作を見つけました。今回は、QObject、シグナルのインデックス、シグナルからスロットへの接続を表すオブジェクト、および引数を使用して _QMetaObject::queued_activate_ を呼び出します。

_if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
    || (c->connectionType == Qt::QueuedConnection)) {
    queued_activate(sender, signal_absolute_index, c, argv ? argv : empty_argv);
    continue;
_

Queued_activateに到達したので、ついに質問の真髄に到達しました。

まず、シグナルから接続タイプのリストを作成します。

_QMetaMethod m = sender->metaObject()->method(signal);
int *tmp = queuedConnectionTypes(m.parameterTypes());
_

QueuedConnectionTypesで重要なことは、QMetaType::type(const char* typeName)を使用して、シグナルのシグネチャから引数タイプのメタタイプIDを取得することです。これは2つのことを意味します:

  1. タイプにはQMetaTypeIDが必要であるため、 qRegisterMetaType で登録されている必要があります。

  2. タイプは 正規化 です。これは、「constT&」と「T」がTのQMetaTypeIDにマップされることを意味します。

最後に、queued_activateはシグナル引数のタイプと指定されたシグナル引数を _QMetaType::construct_ に渡して、スロットが別のスレッドで呼び出されるまで続く存続期間を持つ新しいオブジェクトをコピー構築します。イベントがキューに入れられると、シグナルが返されます。

そしてそれは基本的に話です。

15
cgmb

オブジェクトが存在するスコープが終了し、それが使用されると、破壊されたオブジェクトが参照され、未定義の動作が発生します。スコープが終了するかどうかわからない場合は、newを介してフリーストアにオブジェクトを割り当て、shared_ptrなどを使用してその存続期間を管理することをお勧めします。

0
Seth Carnegie