QObjectPrivateにアクセスする方法


Qt Advent Calendar 2019 21日目の記事です。

Qtの新しめのモジュールなどを使ってみたりすると、APIが足りないことが稀によくあります。
内部では明らかに機能を持ってそうなのに、マニュアルに無いしヘッダにも無い。
コードを見たら該当の機能は存在してるので、書き換えてビルドし直したら使えるけど...面倒臭い。
という時に使えるかもしれない、 QObjectPrivate という真っ当なQtプログラマーは知らなくてもよい機能を無駄に紹介します。

※ 闇のコードの紹介なので、活用は自己責任でお願いします。

D-Pointer

まずはD-Pointerの説明からです。
QtのD-Pointerは一般的にはpimplやOpaque Pointerと呼ばれる、実装をcpp側に隠蔽するテクニックです。
メリットはいろいろありますが、Qtの場合はABI互換性を保つという側面が大きいようです。
ちなみにデータポインターの略らしいです。

QObjectとQObjectPrivateの関係

QObjectに対する内部実装をQObjectPrivateという形で保持しています。(確認したら正確にはQObjectDataを継承してのQObjectPrivateでした。)
QObjectはd_ptrというメンバー変数を一つだけ持っています。親子関係やらプロパティやらシグナルやらと言ったQObjectの基本機能はQObjectPrivate側で持っています。QObjectPrivateからはq_ptrという形でQObjectへのポインタを持っています。

これくらいならC++にはよくある実装ですが、さらに継承することで複雑化していきます。
QObjectを継承したQWidgetの場合、QObjectPrivateを継承したQWidgetPrivateクラスを持ちます。

こんな感じの構造になります。これが継承の数だけ続きます...
各Qtクラスに対応するPrivateクラスをincludeして使うことで強引に内部機能が活用できることになります。

参考

Qiita: d-pointer 化で開発効率を向上させよう!
Qt Wiki: D-Pointer

継承してQObjectDataを使う

QObectPrivateの継承元であるQObjectDataから説明します。
これはQObjectのヘッダに書いてあるから一応ここへのアクセスまではそこまで邪道というわけでは無いのかな?
でもドキュメントは無いし、互換性の保証もあるのか知りません。
QObjectを継承するとprotectedなメンバ変数であるd_ptrにアクセスできます。ここには親子関係と(多分高速化のための)各種フラグ、メタオブジェクトの抽象クラス(データ無し)が含まれます。

情報出力クラスのコード

class A:public QObject
{
    Q_OBJECT
public:
    A()
    {
        qDebug()<<"this"<<this;
        qDebug()<<"q ptr"<<this->d_ptr->q_ptr;
        qDebug()<<"parent"<<this->d_ptr->parent;
        qDebug()<<"unused"<<this->d_ptr->unused;
        qDebug()<<"blockSig"<<this->d_ptr->blockSig;
        qDebug()<<"children"<<this->d_ptr->children;
        qDebug()<<"isWidget"<<this->d_ptr->isWidget;
        qDebug()<<"isWindow"<<this->d_ptr->isWindow;
        qDebug()<<"wasDeleted"<<this->d_ptr->wasDeleted;
        qDebug()<<"postedEvents"<<this->d_ptr->postedEvents;
        qDebug()<<"sendChildEvents"<<this->d_ptr->sendChildEvents;
        qDebug()<<"deleteLaterCalled"<<this->d_ptr->deleteLaterCalled;
        qDebug()<<"isDeletingChildren"<<this->d_ptr->isDeletingChildren;
        qDebug()<<"receiveChildEvents"<<this->d_ptr->receiveChildEvents;
        qDebug()<<"metaObject"<<this->d_ptr->metaObject;
    }
};

出力結果

this QObject(0x7fe730847e50)
q ptr QObject(0x7fe730847e50)
parent QObject(0x0)
unused 0
blockSig 0
children ()
isWidget 0
isWindow 0
wasDeleted 0
postedEvents 0
sendChildEvents 1
deleteLaterCalled 0
isDeletingChildren 0
receiveChildEvents 1
metaObject 0x0

※ Qt5.14.0時点での動作を確認しています。

QObjectPrivateを使う

QObjectPrivateは完全に互換性の保証外なのでちょっと隠されてます。コード中にも W A R N I N G --- This file is not part of the Qt API.とか書いてあります。再度の注意ですが、ご利用は自己責任で!

include

Qtのインクルードフォルダを探すとprivateフォルダがあって、その中にQxxxPrivateの実装が含まれています。
CMakeを使う場合は以下のようにパスを通せます。

target_include_directories(myproject PRIVATE ${Qt5Widgets_PRIVATE_INCLUDE_DIRS})

そうすると以下のようにインクルードして使えるようになります。

#include <private/qobject_p.h>

さて、徐々に怪しげな雰囲気が出てきましたね...

使い方

QObjectを継承している場合、メンバとして持っているQObjectData型のd_ptrをキャストするか、後述のマクロで使えます。

継承してない場合は QObjectPrivate::getで取り出せます。

QObjectPrivateを取り出すコード

        auto obj = new QObject;
        auto p = QObjectPrivate::get( obj );

        qDebug()<<"extraData"<<p->extraData;
        qDebug()<<"threadData"<<p->threadData;
        qDebug()<<"connections"<<p->connections;
        qDebug()<<"sharedRefcount"<<p->sharedRefcount;

出力結果

extraData 0x0
threadData 0x7fe495604bd0
connections 0x0
sharedRefcount 0x0

スレッド以外nullになってますが、例えばQObject::setObjectName()で名前を設定すると、extraDataが作成されてその中に名前情報を保持します。

QWidgetPrivate::getもあるので、対応するgetが用意されてるっぽいです。
特にQt3D周りとかQML周りとかの設計がAPIをシンプルにするために内部がかなり隠蔽されてるので、デバッグのためにアクセスしたい時に活用できます。デバッグのためなら仕方ない。

ちなみにsizeofしてみると、QObjectは16、QObjectPrivateは88バイトでした。厳密じゃ無いけどだいたいこれくらいがQObject一つ分の最小限のメモリコストっぽいです。

自作クラスで使う

コミュニティでの質問を見ると、ユーザーがQObjectPrivateを継承して使っても一応問題は無いそうです。もちろん互換性保証が無いのでそこは知らないよ、っていう感じですが。

Qtのコードを読む

複雑な継承関係をシンプルに見せるためにマクロの出番です。普通にメンバにアクセスすると抽象クラスなので、具体クラスにアクセスするためにいちいちキャストを書かなくて済むように以下のようなマクロを使っています。

#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()

QObjectでPrivateクラスを使うためにQ_D(QObject)を、PrivateクラスからQObjectを使うためにQ_Q(QObject)を使います。そうするとその関数内では、それぞれdqの変数名で具体クラスにアクセスできるというわけです。
Qt5.14.0時点のqobject.cppで、Q_Qは6件、Q_Dは31件ありました。実際ほとんどのメンバ関数の最初に使われていて、正直これは初見時のハードルを上げていますと思います。

こういうのは歴史的経緯でなかなか変えられないとは思いますが、再設計したらもっと綺麗になるのか気になる部分ですね。

闇の活用方法

せっかくなので活用方法も考えてみました。

QObjectに子の変更イベントはあるけど、親の変更イベントを受け取る機能を見つけられなかったので、それをできるようにしてみました。

//多分QMLとかで使ってる何かにコールバックを設定(超適当)
QAbstractDeclarativeData::parentChanged
            = []( QAbstractDeclarativeData*, QObject* p, QObject* c ) { qDebug() << "parentChanged" << p << c; };

//内部でnullじゃないことをチェックしてる雰囲気なので超雑に回避
auto child = new QObject();
auto p = QObjectPrivate::get( child );
p->declarativeData = new QAbstractDeclarativeData;

//親の変更イベントをフックできる
auto parent = new QObject;
child->setParent( parent );

一応動いた...

利用ケースとしては、QObjectをスマートポインタで管理してる時に親を設定される(二重解放になる)のを禁止したい時とかに使えます(使うな)。というかこれの正規の方法があったら教えてください。

まとめ

  • Qtのクラスのデータは、クラス名に対応するQxxxPrivateクラスに書かれている
  • QObjectを継承するとある程度使える
  • パスを通すと普通に使える
  • ご利用は自己責任で