Qt 5 でのメタオブジェクトシステムの変更点

この記事は Qt Blog の "Changes to the Meta-Object System in Qt 5" を翻訳したものです。
執筆: Kent Hansen, 2012年6月22日

Qt 5 では メタオブジェクトシステム に対する変更が行われました。内部的な変更だけではなく、API にも変更があります。Qt 4 とのソースコード互換性のない変更も行われました。この記事では、これらの変更の詳細とどのような対応が必要になるのかについて紹介したいと思います。また、QMetaMethod に新たに加えられた便利な API の紹介もあわせてしたいと思います。

Qt 4 でのメタオブジェクトのバージョンのサポートの削除

(moc によって生成される)メタオブジェクトにはデータの(形式や順番などの)内容やメタオブジェクトの機能を特定するためのバージョン番号が含まれています。Qt の(マイナー)リリースでメタオブジェクトに(クラスのコンストラクタを検索可能にするような)新機能を追加した際には、このメタオブジェクトのバージョンを上げる対応が必要になります。そして、Qt 4 での最終的なメタオブジェクトのバージョンは 6 でした。

Qt のマイナーリリースでは、後方互換性の維持のために古いのバージョンもメタオブジェクトもサポートしていました。内部のコードでは、それぞれのメタオブジェクトに対してバージョンをチェックし、それに応じた処理が行われていました。

Qt 5 は Qt 4 とのバイナリ互換性を保持しないため、メタオブジェクトの以前のバージョンのサポートを打ち切るちょうどいい機会になりました。Qt 5 では Qt 4 で対応してきた様々なバージョンのメタオブジェクトが生成されることはないので、それらのバージョンに対応するためのコードを削除しました。Qt 5 での現在のメタオブジェクトのバージョンは 7 になります。

Qt 4 の moc で生成されたコードは Qt 5 ではコンパイルできません。万が一、moc の出力結果に手を加えるようなコードを書いている場合には書き直す必要があります。Qt 自身のコードにはこのような "特別な" メタオブジェクトを提供するためのコードが数カ所含まれていました。

Qt 内部の(QMetaObjectBuilder や QtDBus、ActiveQt など)実行時にメタオブジェクトの情報を生成しているようなコードも、バージョン 7 のメタオブジェクト情報に対応する形で修正されました。これらは内部的な修正なので、みなさんへの影響はないはずです。(我々以外にはこのようなコードを書いている人がいないことを祈っています :) )

文字列によるメソッドの特定はもうしない

Qt 5 までは、メタオブジェクトにはクラスのシグナル、スロット、Q_INVOKABLE なメソッドの 正規化されたシグネチャ が '' を終端とする文字列を繋げた形で保存されていました。これは QObject::connect() が (SIGNAL() や SLOT() マクロによる) 文字列をベースとしていたためです。現在は テンプレートを使用した QObject::connect() に対応したこともあり、完全なシグネチャを保持する理由は少なくなってきました。(Qt 4 では、QObject::connectNotify() のために内部的にシグネチャを文字列で保持する必要がありました。詳細は次のセクションで。)

もう一つのよくあるメソッド検索のユースケースは(QML や QtScript の)動的バインディングです。この場合も正規化されたシグネチャは理想的な表現ではありません。メソッド名や引数の型を直接操作できた方が(実行時に文字列の解析が必要ないため)実装はよりシンプルで効率もよくなります。

Qt 5 では、QMetaMethod に name() と parameterCount()、parameterType(int index) という関数が追加されました。必要なシグネチャを別の情報から実行時に生成するように変更したため、完全なメソッドのシグネチャをそのままの形でメタオブジェクトのデータに保存されることはなくなりました。(メソッド名だけが保存されます。)

既存の QMetaMethod::signature() 関数の返り値の型は const char * で、これは完全なシグネチャを文字列で保持していたことを示しています。また、返り値の型を QByteArray に変更することは、以下のようなコードに(警告もなしに)影響がでるため、簡単にはできませんでした。

// もし signature() が QByteArray を返した場合、この行の後にスコープ外になって(破棄されて)しまう。
const char *sig = someMethod.signature();
// sig に対して何かの処理をした場合、なにかおかしなことが起こる可能性が ...

Qt 5 では、これを解決するために新しい関数 QMetaMethod::methodSignature() を追加しました。QMetaMethod::signature() は使用できなくなっています。呼び出した場合には signature() は名前が変わったという旨のコンパイルエラーになります。既存のコードは QMetaMethod::methodSignature() に書き換える必要があります。新しく追加された QMetaMethod::name() なども利用可能ですが、デバッグなどで完全なメソッドのシグネチャを必要とする場合もあるでしょう。

connectNotify() と disconnectNotify()

QObject::connectNotify() と disconnectNotify() は滅多に再実装されない仮想関数ですが、クラスのシグナルが接続もしくは切断された際の処理を書くことができます。例えば、パブリックなシグナルと内部的なバックエンドオブジェクト間の接続を簡易的なプロキシのような形でリレーさせるようなケースが考えられます。qtsystems モジュールには実際にこのようなコードがたくさん存在しています。

Qt 5 以前は、connectNotify() は文字列ベースの QObject::connect() のみを想定していて、接続されたシグナルを特定するための正規化されたシグネチャを const char * 型で受け取っていました。テンプレートベースの QObject::connect() や QML や QtScript での特殊な接続において、通常は再実装されていない仮想関数を呼ぶためだけに文字列での表現を用意するのは理想からかけ離れています。

さらに言うと、const char * 型の connectNotify() は、なんと言うか… Qt らしくありません。たとえ正規化された形のシグネチャを使用する場合でも、以下のような罠引っかかってしまうことがあります (実際 Qt 内部でもこれがありました)。

void MyClass::connectNotify(const char *signal)
{
if (signal == SIGNAL(mySignal())) {
// 文字列ではなくポインタで比較が行われるため、ここは絶対に通らない......

ドキュメントには signal 引数を QLatin1String で囲むように書かれていますが、忘れることが多いため、このような間違いの起こらない API を提供する必要がありました。

Qt 5 では、QObject::connectNotify() と disconnectNotify() は char * ではなく QMetaMethod を受け取ります。QMetaMethod は QMetaObject のポインタとインデックス用の小さなラッパーで、接続の確立の方法には依存していません。

この変更により、connectNotify() と disconnectNotify() の呼び出しの実装を、内部的なインデックスをベースとした(Qt 内のいくつかの場所で使われている QMetaObject::connect() による)connect() と disconnect() に変えることができました。というわけで、再実装された connectNotify() は期待どおりの動作をするようになり、(例えば connectSlotsByName() などにより) QObject::connect() を明示的に呼び出していない場合でも呼び出されるようになりました。これにより、ようやく Qt の中のインデックスベースの connect() に関連して connectNotify(const char*) を手動で実行するような恥ずかしいコードから解放されました。

connectNotify() や disconnectNotify() を再実装している既存のコードは新しい API に対応する必要があります。その際は、新しく追加された QMetaMethod::fromSignal() と QObject::isSignalConnected() を使用すると比較的簡単にできるでしょう。

QMetaMethod::fromSignal()

QMetaMethod::fromSignal() は新たに追加されたスタティック関数で、メンバ関数(シグナル)を1つ引数にとり、対応する QMetaMethod を返します。これは新しく生まれ変わった connectNotify() で以下のように使用することができます。

void MyClass::connectNotify(const QMetaMethod &signal)
{
if (signal == QMetaMethod::fromSignal(&MyClass::mySignal)) {
// mySignal が接続された ...

connectNotify() が呼ばれる度に毎回特定のシグナルの検索を行わないよう、QMetaMethod::fromSignal() の結果をスタティック変数に保持しておくことも可能です。

fromSignal() のもう一つの隠れた使い道は、シグナルの遅延発生を行うことです。QMetaMethod::invokeMethod() でもこれは可能ですが、文字列ベースでの処理になってしまいます。

QMetaMethod::fromSignal(&MyClass::mySignal)
.invoke(myObject, Qt::QueuedConnection /* 引数がある場合はここに追加 ... */);

QObject::isSignalConnected()

QObject::isSignalConnected() も新たに追加された関数で、あるシグナルが接続されているかを調べるために使われます。これは QObject::receivers() の QMetaMethod での代替方法になります。

void MyClass::disconnectNotify(const QMetaMethod &signal)
{
if (signal == QMetaMethod::fromSignal(&MyClass::mySignal)) {
// mySignal の接続が切断された
if (!isSignalConnected(signal)) {
// このシグナルへの接続はこれ以上ないので、ここでリソースの解放などを行うことができるはず ...

シグナルの発生にアプリケーションのパフォーマンスに影響があるような複雑な計算が必要な場合には、この関数により接続されていないことを確認ができればそれらの処理を省略することが可能です。

QTBUG-4844: QObject::disconnectNotify() is not called when receiver is destroyed(レシーバーのオブジェクトが破棄された際に QObject::disconnectNotify() が呼ばれない)

アップデート: https://codereview.qt-project.org/29423 で修正されました。

このバグは残念ながら現時点では修正されていません。 disconnectNotify() が "不完全" である(見方によっては "壊れている" とも言える)ことをはっきりと示しているため、修正が望まれます。

通常の明示的な disconnect とは異なり、レシーバーのオブジェクトが破棄された時点で該当するシグナルの ID を取得することができないため、このバグを修正するには、接続自体がシグナルの ID を覚えている必要があるかもしれません。すべての接続に(整数型1つ分のサイズの)オーバーヘッドが発生するでしょう。もしちゃんとした解決方法があれば、5.0 にはまだ間に合います(マイナーリリースで対応するには大きすぎる変更になるでしょう)。

メソッドの返り値の型

Qt 4 では、メソッドの返り値の型は QMetaMethod::typeName() を使用して const char * の形式で取得する必要がありました。void 型の場合、QMetaType::typeName() は "void" を返すのですが、この QMetaMethod::typeName() は空文字列を返していました。以下のような慎重で冗長なコードを書く人が現れたのは、この一貫性のない仕様のせいでしょう。

if (!method.typeName() || !*method.typeName() || !strcmp(method.typeName(), "void")) {
// 返り値の型は void ...

Qt 5 では、QMetaMethod::returnType() という新たに追加された関数を代わりに使用することができ、これは メタタイプ の ID を返します。

if (method.returnType() == QMetaType::Void) {
返り値の型は void ...

Qt 4 では、QMetaType::Void を使用して void と未登録の型を区別することができませんでした。これはどちらも値が 0 で実装されていたためです。Qt 5 では、QMetaType::Void は実際に void を表し、Qt の型システムに未登録の型を表す QMetaType::UnknownType という値が追加されました。

(余談ですが、型の ID を QMetaType::Void もしくは 0 と比較するようなコードを書いている場合には、Qt 5 への移行時にはそのロジックを再確認することをお勧めします。(それらの型は void もしくは未登録の型のどちらか、もしくは両方でしょう。))

QMetaType との一貫性を保つため、Qt 5 の QMethod::typeName() は返り値の型が void の場合には "void" という文字列を返します。typeName() が空の場合に void 型としているコードは (returnType() と QMetaType::Void を比較するような) 適切な対応が必要です。

メソッドの引数

QMetaMethod::returnType() と同じように、QMetaMethod::parameterType() という新たに追加された関数が引数のメタタイプの ID を返します。QMetaMethod::parameterCount() は引数の数を返します。引数の型を文字列の配列として返す以前の QMetaMethod::parameterTypes() の代わりにこの2つを使うことを推奨します。

(moc の処理時における組み込み型のように)メタオブジェクトの定義時に型が分かっている場合、型の ID はメタオブジェクトのデータに直接埋め込まれるため、探索もとても高速に行うことができます。その他の型では、ID は文字列での探索になりますが、それでも以前ほどは遅くありません。(経過をキャッシュすることでさらに改善できるかもしれません。)

まとめ

Qt 5 ではメタオブジェクトシステム(メタタイプシステムもですが、詳細は別の記事にしましょう)に変更が加えられました。メソッドは (C の) 文字列ではなく、適切に生成された適切な表現で扱われます。Qt 4 とのソースコード互換性も多くは保たれています(「壊れてないものは直すな」という言葉がありますが、Qt 4 ではいくつかおかしかった点があり、それらを直さざるをえなかったのです)。いくつかのもう必要のない実装は削除されました。いくつかの Qt のモジュールではこの新しい機能をすでに使用していて、コードも簡潔で高速なものになりました。メタシステムに対して一番批判的な方でさえ満足するものになったことでしょう。


Blog Topics:

Comments