d-pointer 化で開発効率を向上させよう!

  • 12
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

この記事は Qt Advent Calendar 2015 25日目のエントリとなります。

みなさんこんにちは。今年は Qt Champion の防衛に失敗した @task_jp です。そして今年も好きなクラスは QSharedDataPointer、嫌いなクラスは QTextDocument です。

妻が里帰り出産で札幌に帰省中のため、今年のクリスマスはぼっちでした。そろそろ産まれてもいいころらしいのですが、中の人はヒキコモリがちな私に似たのか、なかなか外に出たがらないようです。。。

d-pointer とは?

一般的には Opaque Pointer とか Pimpl と呼ばれるテクニックですが、Qt に最初にこのテクニックを導入した当時の Trolltech の Arnt Gulbrandsen さんが「d-pointer(ディー・ポインター)」と呼んでいたことから Qt 周辺では d-pointer と呼ばれるのが慣例になっています。

Qt の内部で非常によく使われているテクニックですが、みなさんが中規模以上のプログラムを開発する際に非常に役に立つ気がするので今回紹介したいと思います。

まず初めに d-pointer を使わないでクラスを作成する場合と、d-pointer を使ってクラスを作成する場合2つのコードを示します。動作は(基本的には)同じです。

非 d-pointer 版 Object クラス

object.h
#ifndef OBJECT_H
#define OBJECT_H

#include <QtCore/QObject>

class Object : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
    explicit Object(QObject *parent = nullptr);
    ~Object() Q_DECL_OVERRIDE;

    int value() const;

public slots:
    void setValue(int value);

signals:
    void valueChanged(int value);

private slots:
    void printValueChanged(int value);

private:
    int m_value;
    Q_DISABLE_COPY(Object)
};

#endif // OBJECT_H
object.cpp
#include "object.h"
#include <QtCore/QDebug>

Object::Object(QObject *parent)
    : QObject(parent)
    , m_value(0)
{
    QObject::connect(this, &Object::valueChanged, this, &Object::printValueChanged);
}

Object::~Object()
{
}

int Object::value() const
{
    return m_value;
}

void Object::setValue(int value)
{
    if (m_value == value) return;
    m_value = value;
    emit valueChanged(value);
}

void Object::printValueChanged(int value)
{
    qDebug() << value;
}

いたって普通ですね。みなさんもこんな感じでクラスを作ることが多いのではないでしょうか。

d-pointer 版 Object クラス

object.h
#ifndef OBJECT_H
#define OBJECT_H

#include <QtCore/QObject>

class Object : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
    explicit Object(QObject *parent = nullptr);
    ~Object() Q_DECL_OVERRIDE;

    int value() const;

public slots:
    void setValue(int value);

signals:
    void valueChanged(int value);

private:
    class Private;
    Private *d;
    Q_DISABLE_COPY(Object)
};

#endif // OBJECT_H
object.cpp
#include "object.h"
#include <QtCore/QDebug>

class Object::Private
{
public:
    Private(Object *parent);

private:
    void printValueChanged(int value);

private:
    Object *q;
public:
    int value;
};

Object::Private::Private(Object *parent)
    : q(parent)
    , value(0)
{
    connect(q, &Object::valueChanged, [&](int value) { printValueChanged(value); });
}

void Object::Private::printValueChanged(int value)
{
    qDebug() << value;
}

Object::Object(QObject *parent)
    : QObject(parent)
    , d(new Private(this))
{
}

Object::~Object()
{
    delete d;
}

int Object::value() const
{
    return d->value;
}

void Object::setValue(int value)
{
    if (d->value == value) return;
    d->value = value;
    emit valueChanged(value);
}

ヘッダファイルがシンプルになった代わりに、ソースファイルは記述が増えました。なんか少し工夫されている感じがありますね。

d-pointer 化の変更点は以下になります。

  • ヘッダファイル
    • private な Private という内部クラスを宣言
    • Private クラスのポインタをメンバ変数として保持
    • プライベートな処理(スロット)を削除
  • ソースファイル
    • Private クラスの定義と実装を追加
    • プライベートな変数や処理を Private クラスに実装
    • Private クラスのインスタンスの生成と破棄

内部の実装のための変数や関数はソースコードに書く、ヘッダファイルには書かない」というのが基本方針です。

なんでこんなことするの?

1. ヘッダファイルがシンプルになる

外部のインターフェース定義だけが書かれている、そのクラスを使う人にとてもやさしいヘッダファイルになります。

また、ヘッダファイルのファイルサイズが相対的に減るため、複数回 include されている場合にはビルド時の総処理時間が短縮されるでしょう(要確認)。

2. ヘッダファイルの変更頻度が減る

内部処理用の変数や関数の追加はソースファイル側で行われるため、ヘッダファイルが変更される頻度は d-pointer を使用しない場合に比べて圧倒的に低くなります。このため、ヘッダファイルに依存している(#include している)数々のソースファイルを再コンパイルする頻度も低くなるため、開発サイクルにおけるコンパイル時間の大幅な短縮が実現できます。

3. 変更の性質がわかりやすくなる

インターフェースを変更しない場合にはソースファイルだけ変更する、インターフェースを変更する際は「ヘッダファイルも」変更する、という方針により開発を進める際のミスを減らすことができるでしょう。レビュー時には、ヘッダファイルが変更されている場合は「インターフェースが変わるんだな」ソースファイルのみの変更の場合は「実装の修正だな」といった形でパッチやコミットの性質に応じた視点でのチェックが簡単にできるようになります。

4. バイナリ互換性

Qt ではメジャーバージョン間でバイナリの後方互換を保証しています。例えば、Qt 4.0.0 上でビルドされたバイナリファイルは Qt 4.8.3 上でもそのまま動くはずで、Qt 5.0.0 上でビルドされたバイナリファイルは、Qt 5.6.0 上でもそのまま動作します。Qt の新しいバージョンが出てもライブラリを差し替えるだけで実行が可能で、ビルドをしなおす必要はありません。(過去にこの互換性が一部壊れていたこともありました1

バイナリ互換性の定義、メリット、実現の具体的な方法については Binary Compatibility Issues With C++ を参照してください。d-pointer を導入することでバイナリ互換を実現しやすくなることがわかるでしょう。

ライブラリを開発する場合、特に長期的に開発を継続したりメンテを考えている場合に非常に重要になる問題ですね。

なんで d って名前の変数なの?

最初は d-pointer っていうから d なんでしょ?と思っていましたが、よく考えたら本末転倒で、なんかの理由があって d を使いはじめて、それで d-pointer と呼ばれるようになったと思うのですが、理由はわかりません。Private クラス内部の Public クラス用のメンバ変数は q を使うのがこちらも慣例なのですが、これもどうしてかは不明です。

Qt 内部での d-pointer の使い方

今回のサンプルコードはとても簡単なものにしましたが、Qt 内部ではさらに進んだ(複雑な?)使い方をしています。
QObjectPrivateQWidgetPrivate のように「パブリックなクラス名 + Private」という名前でプライベートクラスを用意し、さらに派生クラスの Private クラスが基底クラスの Private クラスを継承するといった作りになっていたり、Q_QQ_D などのマクロを使用してパブリックなクラスとプライベートなクラスの橋渡しをしています。

興味のある方は Qt Wiki にある「D-Pointer」という記事を読んでみてください。

おわりに

今年の Qt Advent Calendar を作成した当初は「今年は全部埋まらないんじゃないかな?」という雰囲気でしたが、@hermit4 さんをはじめとしたみなさんのおかげで今年も無事最終日を迎えることができました。めでたいめでたい。

初心者向けの素晴らしい記事から、普段自分では使わないような Qt の機能の紹介、ピンポイントですごく役に立ちそうなテクニックの解説など、バラエティに富んで素晴らしいですね。

それぞれの記事が日本の Qt ユーザーのみなさんの役に立つことと、一人でも多くの人が「来年は自分も Qt Advent Calendar に参加してみよう!」と思ってくれるとうれしいです。

注釈


  1. MSVC 2012 及び MSVC 2013 を使用した場合、Qt 5.3.2 と Qt 5.4.0 でバイナリ互換性が壊れていました。これは Qt 5.4.1 で修正されました23。 

  2. http://code.qt.io/cgit/qt/qtbase.git/tree/dist/changes-5.4.1#n23 

  3. https://codereview.qt-project.org/102419