QtDay 17

今更聞けないシグナル・スロット総整理


1. 初めに

Qt5になってから、シグナルとスロットを接続するconnect関数のバリエーションが増えました。

しかし、公式サイトのSignals & Slotsを読むと、このページの記述だけでは説明が不十分だと感じました。

実際仕事で使用すると、わからないことが多く、Stack Overflow等のWebサイトを参考にしたり、自分でQt SDKのコードを確認したりとやっていて・・・結局公式サイトのDifferences between String-Based and Functor-Based Connectionsのページがあることに気付きました。

このページは、「signals and slots」キーワードでQtのサイトを検索すると2ページ目に出てきます。

Differences between String-Based and Functor-Based Connectionsが1ページの上の方に出てくれば、あるいは、Signals & Slotsのページからリンクがあれば、もっと楽にシグナル/スロットのことを理解できた、と思わずにはいられません。



<-- 2018/09/09 追記 start -->

2018/09/09現在では検索結果の1ページ目に表示されるようになったようです。

<-- 2018/09/09 追記 end -->

せっかく調べたので、初心に帰って、Qt初心者が引っ掛かりそうなところも含めてシグナル・スロットを説明してみたいと思います。


2. シグナルの特性


  • シグナルは関数ではない

    見た目は返り値がvoidの関数ですが、関数の実体はありません。

    従って、クラスのメンバ関数のようにoverrideすることはできません。


  • スコープ

    シグナルは「public」等のアクセス修飾子が付きません。

    スコープとしては原則、publicとして動作します。

    1点注意する必要があるのは、emitを使用するスコープです。

    publicなシグナルなので、「文法的には」シグナルやスロットを実装していない第3者がシグナルをemitすることは可能です。

    しかし使い方としては、protected、つまり、

    シグナルを宣言したクラス及びそのサブクラスのみemitすることができる

    という使い方が望ましいと公式サイトにも記述されています。

    ※このルールがないと、どこでシグナルをemitしたのかコードの解析が困難になる為、必ず守った方がよいルールだと思います。



    <-- 2017/12/20 追記 start -->

    公式リファレンスとしては記述がありませんが、

    QPrivateSignal型の仮引数を最後に追加することで、emitできる場所をprivateメンバーにアクセスできる場所に制限できるようです。

    自前でProtectedSignalの構造体を定義することにより、protectedなアクセス制限も可能になります。

    詳しくは、コメント欄をご覧ください。

    @tetsurom さんに感謝します。

    <-- 2017/12/20 追記 end -->


  • 別のシグナルへの接続

    シグナルはconnect関数を用いて、別のシグナルに接続することが可能です。



3. スロットの特性


  • スロットはメンバ関数の拡張

スロットは、クラスのメンバ関数の特性を持ち、かつ、「シグナルと接続する」という特性を追加で持ったものです。

従って、アクセス修飾子を付けたり、overrideすることも可能です。


4. connect関数

connect関数のバリエーションは、従来のStringベースの「Qt4スタイル」の他に、Functorベースの3つのスタイルが追加されました。

Qt5においても「Qt4スタイル」はDuplicated対象にはならず、むしろ「Qt4スタイル」でないとできないことがあります。


4.1. Stringベースのconnect関数とFunctorベースのconnect関数の違い

Stringベースは、実行時に解釈されます。言い方を変えると、実行されるまで文法が誤っていても気付くことができません。

Functorベースは、ビルド時に解釈されます。文法誤りはビルド時に気付くことができます。


4.2. Stringベースのconnect関数でしかできないこと

Stringベースのconnect関数でしかできないことは2つあります。


  • (1)デフォルト引数設定時の引数なしに対応できる

connect関数は、引数の数や型を合わせることが原則ですが、デフォルト引数による引数省略について、

Stringベースでは対応できます。

class MyObject1 : public QObject

{
signals:
void valueChanged();

class MyObject2 : public QObject
{
public slots:
void printNumber(int number = 42) { //デフォルト引数を使用.
qDebug() << __FUNCTION__ << number;
}

    m_object1 = new MyObject1(this),

m_object2 = new MyObject2(this);

connect(m_object1, SIGNAL(valueChanged()),
m_object2, SLOT(printNumber()));


4.3. Functorベースのconnect関数でしかできないこと

Functorベースのconnect関数でしかできないことは2つあります。


  • (1)暗黙的な型変換に対応できる

Differences between String-Based and Functor-Based Connectionsに記述されているサンプルを一部編集して説明します。

class Q_WIDGETS_EXPORT QAbstractSlider : public QWidget

{
Q_SIGNALS:
void valueChanged(int value);

class Q_WIDGETS_EXPORT QDoubleSpinBox : public QAbstractSpinBox

{
public Q_SLOTS:
void setValue(double val);

class Q_WIDGETS_EXPORT QAbstractSlider : public QWidget

{
Q_SIGNALS:
void valueChanged(int value);

    auto slider = new QSlider(this);

auto doubleSpinBox = new QDoubleSpinBox(this);

// OK: The compiler can convert an int into a double
connect(slider, &QSlider::valueChanged,
doubleSpinBox, &QDoubleSpinBox::setValue);

上記は、int型の引数を持つシグナルをdouble型の引数を持つスロットに接続しています。

このような暗黙的な型変換はQt4スタイルではできません。


  • (2)ラムダ式に対応している

次項で説明するようにラムダ式に対応しているのは、Functorベースのスタイルのみです。


4.4. connect関数:スタイル一覧

それぞれのスタイルの呼称は、私が適当に決めたものです。

(1) Qt4スタイル[Stringベース]


QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *slot,
Qt::ConnectionType type )


(2) Qt5スタイル[Functorベース]


QObject::connect(const QObject *sender, PointerToMemberFunction signal,
const QObject *receiver, PointerToMemberFunction slot, Qt::ConnectionType type )


(3) Lambdaスタイル1[Functorベース]


QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor method )


(4) Lambdaスタイル2[Functorベース]


QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor method, Qt::ConnectionType type )


Qt::ConnectionTypeについては、hermit4さんの記事によくまとめられていますので、そちらをご参照ください。

今どのスレッドにいるのかを確認するには、QThread::currentThreadId()をログ出力しながら確認すれば、動作を理解しやすくなると思います。

それでは、具体的な例を出しながら、各スタイルの特徴を見ていきます。


4.5. connect関数の使用例:引数なしの場合

サンプルコードとして、シグナルを送信するクラスをMyObject1、スロットを実装するクラスをMyObject2とします。


myobject1.h

class MyObject1 : public QObject

{
Q_OBJECT
public:
explicit MyObject1(QObject *parent = nullptr);
signals:
void valueChanged();


myobject2.h

class MyObject2 : public QObject

{
Q_OBJECT
public:
explicit MyObject2(QObject *parent = nullptr);
public slots:
void setValue();


Qt4スタイル

    connect(m_object1, SIGNAL(valueChanged()), m_object2, SLOT(setValue()));

Qt4スタイルはSIGNALマクロとSLOTマクロを使用します。シグナル、スロット両方とも関数名の後に()が必要です。


Qt5スタイルのconnect関数

    connect(m_object1, &MyObject1::valueChanged, m_object2, &MyObject2::setValue);

Qt5スタイルでは、()や引数は記述する必要がなく、その代わり、シグナル名とスロット名の頭に「&クラス名::」が付きます。(仮に第1引数や第3引数がthisでも必要です)

一見、Qt4スタイルよりも(特にクラス名が長いと)野暮ったいように感じるかもしれませんが、Qt Creatorでエディットする場合、第2引数と第4引数を楽に入力できます。

上記の例だと、

「connect(m_object1, &」まで入力すると、「MyObject1」が候補として表示され、さらに、Enterキー入力→「::」入力後、シグナルの候補が表示される為、スムーズに入力できます。


Lambdaスタイル1

    connect(m_object1, &MyObject1::valueChanged,

[=]() {
qDebug() << "Lambda Style1 signal received.";
});

シグナルを送信する側をSender、スロット側をReceiverと呼びますが、Lambdaスタイル1と次のLambdaスタイル2はReceiverが存在しないスタイルです。

ラムダ式に慣れていないと、最初は面食らうかもしれませんが、Lambdaスタイル1は

「シグナルされたかどうかキャッチしてログを出してみたい」といった時に、いちいちスロットを作成しなくてすむので重宝します。

また、引数にQt::ConnectionTypeがありません。Lambdaスタイル1はQt::ConnectionTypeがQt::DirectConnection固定になります。

※このことについて記述されているWebサイトは見つかりませんでしたが、Qt SDKのコードを確認し、動作確認した結果、この結論になりました。

send()関数によって、Senderを取得することもできません。


Lambdaスタイル2

    connect(m_object1, &MyObject1::valueChanged, this,

[=]() {
qDebug() << "Lambda Style2 signal received.";
}, Qt::QueuedConnection);

Lambdaスタイル2はQt5.2で追加されたスタイルです。

Lambdaスタイル1と比べると、第3引数にコンテキストが追加され、第5引数にQt::ConnectionTypeが追加された形式です。Qt::ConnectionTypeが指定できるので、当然Qt::DirectConnection以外の動作も可能です。send()関数も使用できます。

Lambdaスタイル1、Lambdaスタイル2共に、例では、ラムダ式のところに[=]を使用していますが、[=]以外のキャプチャ記法も使用できます。

他のキャプチャ記法ついては、cpprefjp - C++日本語リファレンスのサイトでご確認ください。

第3引数のコンテキスト、これの扱いが一番やっかいです。

Stack Overflow先生のお力を借りて説明します。

まず、非ラムダ式のconnect関数においては「切断」されるタイミングが3つあります。


  • Senderが解放されるタイミング

  • Receiverが解放されるタイミング

  • connect関数を読んでいるクラスのインスタンスが解放されるタイミング

これに対してラムダ式ではReceiverは存在しません。Lambdaスタイル2では、Receiverが存在しない代わりに、第3引数コンテキストの生存区間が接続されている区間になるようです。

上記のサンプルコードでは、thisがコンテキストになっている為、thisが解放されたタイミングで切断されます。

ラムダ式の処理内では、このthisのクラスのメンバにアクセスすることが可能です。


4.6. connect関数の使用例:引数ありの場合


myobject1.h

class MyObject1 : public QObject

{
Q_OBJECT
signals:
void value2Changed(int value);


myobject2.h

class MyObject2 : public QObject

{
Q_OBJECT
public slots:
void setValue2(int value);


Qt4スタイル

    connect(m_object1, SIGNAL(value2Changed(int)),

m_object2, SLOT(setValue2(int)));


Qt5スタイル

    connect(m_object1, &MyObject1::value2Changed,

m_object2, &MyObject2::setValue2);


Lambdaスタイル1

    connect(m_object1, &MyObject1::value2Changed,

[=](int value) {
qDebug() << "Lambda Style1 signal received." << value;
});


Lambdaスタイル2

    //Lambda Style2

connect(m_object1, &MyObject1::value2Changed, this,
[=](int value) {
qDebug() << "Lambda Style2 signal received." << value;
}, Qt::QueuedConnection);


4.7. Functorベースのconnect関数におけるオーバーロード(overload)対策

「connect関数の使用例:引数ありの場合」の例では、あえて避けたことがあります。

それは、シグナルやスロットにオーバーロードが複数存在した場合です。(シグナルは正確にはメンバ関数ではないのでオーバーロードという言い方は微妙ですが気にしない方向で・・・)

Functorベースのconnect関数では、ラムダ式の部分を除いて引数を記述する必要がありませんが、オーバーロードが存在するとコンパイラが判断できなくなります。

例を見てみます。


myobject1.h

class MyObject1 : public QObject

{
Q_OBJECT
signals:
void value3Changed(int value);
void value3Changed(const QString &value);


myobject2.h

class MyObject2 : public QObject

{
Q_OBJECT
public slots:
void setValue3(int value);
void setValue3(const QString &value);

まず、Qt4スタイルで記述してみます。

    connect(m_object1, SIGNAL(value3Changed(int)),

m_object2, SLOT(setValue3(int)));

Qt4スタイルでは、引数の型も合わせた記述スタイルである為、オーバーロードがあっても問題ありません。

このコードをQt CreatorのリファクタでQt5スタイルに変更してみます。

connect上にマウスを移動し、右クリック→「リファクタリング」→「connect()をQt5スタイルに変換」を選択。すると、

    connect(m_object1, &MyObject1::value3Changed,

m_object2, &MyObject2::setValue3);

のように変換されます。これをビルドすると。。。

C:\workspace\SignalSlotSample\widget.cpp:110: エラー: no matching function for call to 'Widget::connect(MyObject1*&, <unresolved overloaded function type>, MyObject2*&, <unresolved overloaded function type>)'

m_object2, &MyObject2::setValue3);
^

のように、ビルドエラーとなります。

シグナル、スロット両方に「int型の引数」と「const QString &型の引数」が存在するのだから解釈できそうに見えますが、残念ながらこのままでは解釈できません。。。

この問題に対する解決策は、

Differences between String-Based and Functor-Based Connectionsに記述されているように4つの解決策があります。


解決策1:static_castする

    connect(m_object1, static_cast<void (MyObject1::*)(int)>(&MyObject1::value3Changed),

m_object2, static_cast<void (MyObject2::*)(int)>(&MyObject2::setValue3));


解決策2:関数ポインタ化する

    void (MyObject1::*mySignal)(int) = &MyObject1::value3Changed;

void (MyObject2::*mySlot)(int) = &MyObject2::setValue3;
connect(m_object1, mySignal, m_object2, mySlot);


解決策3:QOverload<型>::ofを使用する[C++11以上が必要]

    connect(m_object1, QOverload<int>::of(&MyObject1::value3Changed),

m_object2, QOverload<int>::of(&MyObject2::setValue3));


解決策4:qOverload<型>を使用する[C++14以上が必要]

    connect(m_object1, qOverload<int>(&MyObject1::value3Changed),

m_object2, qOverload<int>(&MyObject2::setValue3));

下に行けば行くほど、すっきりした形になりますが、C++ Versionの要求が上がります。

上記例では、シグナル、スロットの両方ともオーバーロードがあった為、どちらも対策が必要でしたが、

片方のみオーバーロードが存在するのであれば、もちろん片方だけ対策を取ればよいです。

現実的には解決策3辺りがちょうどいいでしょうか?

現状のQt Creatorのリファクタ機能を使用するとビルドエラーになるのですから、いずれかの解決策の形に変換できるよう対応していただきたいです。


5. 最後に

Qtの基本機能と言えるシグナル・スロットと言えども、侮れないことがわかりました。

Qt5になってから変更された機能、追加された機能については、まだまだドキュメントが不足していると感じています。

2017年6月にQt5.9LTS(Long Term Support)がリリースされ、最近Qt5.10もリリースされましたが、未だに、国内にQt5/C++の本が存在していません。少なくとも2018年前半までにはなんらかの対策を講じないと、Qtは盛り下がっていくのではと心配しています。

私自身もQtユーザーの一人として、何かできることはないか考えていくつもりです。