Qt の値型のクラスで使われている Implicit Sharing 技術の紹介と実装

はじめに

みなさんこんにちは。@task_jp です。好きなクラスは QSharedDataPointer、嫌いなクラスは QTextDocument です。

昨日は @helicalgear さんによる 日本Qtユーザー会WebSiteのコンテンツについて
でした。

様々な Qt ユーザー会のインフラを紹介していただきましたが、実は ci.qt-users.jp に Jenkins おじさんがいて、Qt ユーザー会のウェブサイトのリポジトリ にコミットがマージされると http://qt-users.jp/ が自動的に更新されるようになっていたりします。

今回は 2015年に書いた d-pointer 化で開発効率を向上させよう! の応用で、2016 年 に書こうと思っていたものを2年ぶりに書いてみようと思います。

値型のクラスとオブジェクト型のクラスとは

Qt はオブジェクト指向のクラスライブラリとして様々なクラスを提供しています。
これらのクラスは大きく分けて2つに分類する事ができます。

オブジェクト型のクラス

QObject を直接/間接的に継承しているクラスを オブジェクト型のクラス と呼びます。

Qt では C++ のクラスの基本機能に加え、以下のような独自の機能を提供しています。

基本的な使い方は以下のようになります

  • new でヒープにインスタンスを生成する
    • 例外もあり、Q*Application や QFile などは慣例的にスタックに生成します
  • 生成時の引数に親の QObject のインスタンスのポインターを指定する
    • 自分自身が親となって this を渡すケースが多い
      • new Class(*this*);
  • メソッドの返り値や引数、プロパティなどはポインター(もしくは const ポインター)を利用する
    • QLayout *QWidget::layout() const
    • void QWidget::setLayout(QLayout *layout)
  • インスタンスのコピーはしない(できない)ようにする

以下が、代表的な QObject をルートとするクラスの継承ツリーになります。

値型のクラス

QByteArrayQStringQVariant のようなクラスや、QListQVectorQHash といった コンテナクラス は QObject を継承していません。

これらが 値型のクラス と呼ばれるもので、以下のような使い方をします。

  • new を使わずにスタック上にインスタンスを生成する
    • QByteArray ba("abc");
  • メソッドの返り値や引数などでは値もしくは const 参照を利用する
    • void QLabel::setText(const QString &text);
  • コピーが可能

以下が代表的な値型のクラスになります。

Implicit Sharing

Qt ではこのような値型のクラスの利便性と実行時のパフォーマンスを考慮し、Implicit Sharing というテクニックを利用して実装されています。

一般的には Copy on Write(CoW) と呼ばれるテクニックで、値のコピーや代入時にはデータをコピーせずに、共通のデータを両方から参照するようにし、データの変更時にはじめて実際のデータのコピーを行います。

QPixmap p1, p2;
p1.load("image.bmp");
p2 = p1; // p1 と p2 は同じデータを共有している

QPainter paint;
paint.begin(&p2);  // データの変更時に p1 とのデータの共有をやめコピーを行っている
paint.drawText(0,50, "Hi");
paint.end();

Qt が用いられるようなプログラムでは値のコピーの回数が値の変更の回数よりも多い という性質を利用して、コストの高い実際のデータの複製処理をデータの変更時まで保留することで、実際動作するプログラムの値のコピーや代入のコストを低く抑えています。

明示的な値のコピーや代入はもちろんのこと、関数の呼び出しの際の暗黙的なコピーのコストも非常に重要です。

引数や返り値の型 コピー
std::string 深い
const std::string & なし
QString 浅い
const QString & なし
QList<QString> 浅い
const QList<QString> & なし

値型の独自クラスを作ってみよう

では実際に下記のような構造体を Implicit Shared クラスにしてみます。

struct Place {
    QByteArray guid;
    QString name;
    double latitude;
    double longitude;
    double altitude;
};

Qt ではこれを実現するために、QSharedDataPointerQSharedData という2つのクラスが用意されています。

place.h
#ifndef PLACE_H
#define PLACE_H

#include <QtCore/QSharedDataPointer>

class Place
{
public:
    Place();
    Place(const Place &other);
    ~Place();

    Place &operator =(const Place &other);
    void swap(Place &other) Q_DECL_NOTHROW { qSwap(d, other.d); }

    QByteArray guid() const;
    void setGuid(const QByteArray &guid);
    QString name() const;
    void setName(const QString &name);
    double latitude() const;
    void setLatitude(double latitude);
    double longitude() const;
    void setLongitude(double longitude);
    double altitude() const;
    void setAltitude(double altitude);

    const void *debug() const;
private:
    class Private;
    QSharedDataPointer<Private> d;
};

Q_DECLARE_SHARED(Place)

#endif // PLACE_H

コンストラクタ、コピーコンストラクタ、デストラクタ、代入演算子、swap() メソッドと、各項目に対する設定/取得メソッドがかかれています。

d-pointer に QSharedDataPointer が使われているのが今回のポイントです。
これにより、const 修飾子がついていないメソッドの中で d が使われると、d のデータのコピーが行われます。

place.cpp
#include "place.h"

class Place::Private : public QSharedData
{
public:
    Private();
    Private(const Private &other);
    QByteArray guid;
    QString name;
    double latitude;
    double longitude;
    double altitude;
};

Place::Private::Private()
    : QSharedData()
    , latitude(0.0)
    , longitude(0.0)
    , altitude(0.0)
{
}

Place::Private::Private(const Private &other)
    : QSharedData(other)
    , guid(other.guid)
    , name(other.name)
    , latitude(other.latitude)
    , longitude(other.longitude)
    , altitude(other.altitude)
{
}

Place::Place()
    : d(new Private)
{

}

Place::Place(const Place &place)
    : d(place.d)
{
}

Place::~Place()
{
}

Place &Place::operator =(const Place &other)
{
    d = other.d;
    return *this;
}

QByteArray Place::guid() const
{
    return d->guid;
}
void Place::setGuid(const QByteArray &guid)
{
    d->guid = guid;
}
QString Place::name() const
{
    return d->name;
}

void Place::setName(const QString &name)
{
    d->name = name;
}

double Place::latitude() const
{
    return d->latitude;
}

void Place::setLatitude(double latitude)
{
    d->latitude = latitude;
}

double Place::longitude() const
{
    return d->longitude;
}

void Place::setLongitude(double longitude)
{
    d->longitude = longitude;
}

double Place::altitude() const
{
    return d->altitude;
}

void Place::setAltitude(double altitude)
{
    d->altitude = altitude;
}

const void *Place::debug() const
{
    return d.constData();
}

値型のクラスなので値の変更時にシグナルを発生させたりはしていませんが、いたって普通の実装になっていると思います。

それでは、この Place クラスを使ってみましょう。

main.cpp
#include "place.h"
#include <QDebug>

int main()
{
    Place p1;
    p1.setGuid("123");
    Place p2(p1);
    Place p3 = p1;
    qDebug() << p1.debug() << p2.debug() << p3.debug(); // (1)
    p2.setGuid("abc");                                  // (2)
    qDebug() << p1.debug() << p2.debug() << p3.debug();
    p1.setLatitude(0.12);                               // (3)
    qDebug() << p1.debug() << p2.debug() << p3.debug();
    p2.setLongitude(0.11);
    p3.setAltitude(-1.0);
    qDebug() << p1.debug() << p2.debug() << p3.debug(); // (4)
    return 0;
}

(1) の時点では p1 も p2 も p3 も同じ d を共有しています。
(2) で p2 の値を変える際に、p2 が現在の d を複製し、新たな d を独自に保持します。
(3) も同様で、ここで p1 と p3 の d の共有が終了します。
(4) (3) 以降は p1 も p2 も p3 もそれぞれの d を持つため、各インスタンスの値を変更してもそれぞれの d のポインタは変わりません。

0x9c1690 0x9c1690 0x9c1690 # (1)
0x9c1690 0x9c1e20 0x9c1690
0x9c2050 0x9c1e20 0x9c1690
0x9c2050 0x9c1e20 0x9c1690 # (4)

最後に、QSharedDataPointer の実装 を少しだけ見てみましょう。

qtbase/src/corelib/tools/qsharedpointer.cpp
template <class T> class QSharedDataPointer
{
public:
    typedef T Type;
    typedef T *pointer;

    inline void detach() { if (d && d->ref.load() != 1) detach_helper(); }
    inline T &operator*() { detach(); return *d; }
    inline const T &operator*() const { return *d; }
    inline T *operator->() { detach(); return d; }
    inline const T *operator->() const { return d; }
    inline operator T *() { detach(); return d; }
    inline operator const T *() const { return d; }
    inline T *data() { detach(); return d; }
    inline const T *data() const { return d; }
    inline const T *constData() const { return d; }

const 修飾子がついていないメソッドで d-> が呼ばれると detach() が実行され、他のインスタンスと d が共有されている場合は共有が解除されます。

まとめ

今回は、Qt の 値型 でよく使われている「Implicitly Sharing」という技術の紹介と、この技術を利用したクラスを実際に実装して動作の確認を行いました。

QObject はとても高機能で便利な半面、パフォーマンス的なデメリットも存在します。クラスを設計する際にオブジェクト型にするか値型にするかは難しい問題ですが、なんでもかんでもオブジェクト型にするのではなくて、値型であるべきところは値型として実装し、必要があれば今回のように「Implicitly Shared」なクラスにしておくことをお勧めします。

明日も @task_jp さんですね。お楽しみに!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.