読者です 読者をやめる 読者になる 読者になる

Atsushi4のブログ

へたれQtプログラマの備忘録的ななにか

Qt Quickで縦と横のレイアウトを切り替える

Qt勉強会 #40 @Tokyo での自分のもくもく内容です。
qt-users.connpass.com

Qt Quickアプリケーションを自分のAndroid端末で動かしてみよう、というのが当日のターゲットでした。
ここで、Androidアプリなら画面の縦横切替が欲しいなーと思ってちょっと悩んだのでその内容です。

つまりこんな感じで縦横を切り替えたい、ということです。


f:id:Atsushi4:20161025091819p:plain


f:id:Atsushi4:20161025091833p:plain

結局また Tasuku Suzuki (@task_jp) | Twitter せんせーの助言を受けつつ
とにかく動いたので上げておきます。

GridLayoutのflowで縦横レイアウト方向が変えられるようです。
ブログ用に1ファイルで書いています。良い子は真似しない。

あと、縦横の判定はScreen.orientationが使えるぽいですが
今回はデスクトップでも動くように縦横比較にしました。

嘘です。jsでこのフラグの書き方が良くわかりません。


MainWindow.qml とか。Qt Creatorから 「ツール」-「外部」-「Qt Quick」-「Qt Quick 2 Preview」で動くと思います。

import QtQuick 2.5
import QtQuick.Layouts 1.1

Item {
    GridLayout {
        id: layout
        property int spacing: 9
        columnSpacing: spacing
        rowSpacing: spacing
        anchors.fill: parent
        anchors.margins: 15
        GridLayout {
            id: buttons
            property int buttonSize: (Math.max(width, height) - (spacing * 8)) / 9
            property int spacing: 9
            signal pressed(string buttonText)
            flow: GridLayout.TopToBottom
            onPressed: text.text = buttonText
            Layout.preferredWidth: buttonSize
            Layout.preferredHeight: parent.height
            rowSpacing: spacing
            columnSpacing: spacing
            onWidthChanged: console.debug(width, height, buttonSize)
            Repeater {
                model: 9
                Rectangle {
                    color: '#CC9'
                    Layout.preferredWidth: buttons.buttonSize
                    Layout.preferredHeight: buttons.buttonSize
                    radius: buttons.buttonSize / 5
                    Text {
                        text: index + 1
                        font.pixelSize: 24
                        anchors.centerIn: parent
                    }
                    MouseArea {
                        anchors.fill: parent
                        onClicked: buttons.pressed(index + 1)
                    }
                }
            }
        }
        Rectangle {
            color: '#9CC'
            Layout.fillWidth: true
            Layout.fillHeight: true
            Text {
                id: text
                text: "Hello Layout"
                font.pixelSize: 36
                anchors.centerIn: parent
            }
        }
    }
    state: width < height ? "vertical" : ""
    states: [
        State {
            name: "vertical"

            PropertyChanges {
                target: layout
                flow: GridLayout.TopToBottom
            }

            PropertyChanges {
                target: buttons
                Layout.preferredWidth: layout.width
                Layout.preferredHeight: buttonSize
                flow: GridLayout.LeftToRight
            }
        }
    ]
}

おしまい。

QStringとchar*との変換に警告を出す

概要

QStringLiteralやQString::fromUtf8などを使うべきところで使っていない箇所にコンパイル時warningまたはerrorを出す。

Qt勉強会#38@Tokyoでそんなお話をしていたので調べてみました。

結論

.proファイル等でdefineを追加します。


warningが出るようになる 2016/09/16追記:VCコンパイラでは出ないようです。

DEFINES += QT_ASCII_CAST_WARNINGS


errorになる(どちらか1行)

DEFINES += QT_NO_CAST_FROM_ASCII
DEFINES += QT_RESTRICTED_CAST_FROM_ASCII

QT_RESTRICTED_CAST_FROM_ASCII ではchar定数配列を引数にとるimplicitなコンストラクタおよび代入演算子は提供される。ポインタはダメ。


Qt自身のビルド時は load(qt_module) で
QT_NO_CAST_TO_ASCII と QT_ASCII_CAST_WARNINGS がdefineされる模様。


プロダクトコードには
DEFINES += QT_NO_CAST_TO_ASCII
推奨、というカンジでしょうか。


(Qt5.7.0, minGW5.3とVS2015で確認しています)

QListの全数処理を計測する

概要

Qt勉強会#38@Tokyoで、QListの全数処理の処理速度を比較してみました。
Qt 勉強会 #38 @ Tokyo - connpass

結論

  • とにかくconst付けるのが大事
  • C++11のrange based forが早い
  • ++の前置とか後置とかどうでもいい

docs.google.com

その他

QTestLibの _data関数で計測対象関数を渡すと、ケース名と計測結果をずらずらっと出力できるので思ったよりいいカンジでした。

実行ファイルに-xmlオプションをつけてファイルに出力するとMicrosoft Excelで直接開けるんじゃないかと思います。
やってみた方がいらしたら結果を教えて頂きたいです。

テストコード

こんなコードでした。

tst_benchmark_foreachtest.cpp

#include <QString>
#include <QtTest>
#include <functional>
#include <algorithm>

using Func = std::function<void(const QPointF&)>;
using Solver = std::function<void (QList<QPointF>, Func)>;

Q_DECLARE_METATYPE(Solver)

class Benchmark_foreachTest : public QObject
{
    Q_OBJECT
public:
    Benchmark_foreachTest();

private Q_SLOTS:
    void initTestCase();
    void doTest_data();
    void doTest();

private:
    QList<QPointF> source;
    static constexpr int count = 1000000;
};

Benchmark_foreachTest::Benchmark_foreachTest()
{
}

void Benchmark_foreachTest::initTestCase()
{
    source.clear();
    for (int i = 0; i < count; ++i) {
        source << QPointF(i, i);
    }
}

void Benchmark_foreachTest::doTest_data()
{
    QTest::addColumn<Solver>("solver");

    QTest::newRow("iterator forward increment") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                   for (auto i = list.begin(); i != list.end(); ++i) {
                                                       func(*i);
                                                   }
                                               });
    QTest::newRow("iterator backward increment") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                   for (auto i = list.begin(); i != list.end(); i++) {
                                                       func(*i);
                                                   }
                                               });
    QTest::newRow("const iterator forward increment") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                   for (auto i = list.cbegin(); i != list.cend(); ++i) {
                                                       func(*i);
                                                   }
                                               });
    QTest::newRow("iterator forward increment const end") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                   const auto end = list.end();
                                                   for (auto i = list.begin(); i != end; ++i) {
                                                       func(*i);
                                                   }
                                               });
    QTest::newRow("const iterator forward increment const end") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                   const auto end = list.cend();
                                                   for (auto i = list.cbegin(); i != end; ++i) {
                                                       func(*i);
                                                   }
                                               });
    QTest::newRow("std foreach") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                    std::for_each(list.begin(), list.end(), func);
                                              });
    QTest::newRow("std foreach const") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                    std::for_each(list.cbegin(), list.cend(), func);
                                              });
    QTest::newRow("Q_FOREACH") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                  Q_FOREACH(auto i, list) {
                                                      func(i);
                                                  }
                                              });
    QTest::newRow("Q_FOREACH (const)") << static_cast<Solver>([](const QList<QPointF> &list, Func func) {
                                                  Q_FOREACH(const auto &i, list) {
                                                      func(i);
                                                  }
                                              });
    QTest::newRow("operator []") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                    for (int i = 0; i < list.size(); ++i) {
                                                        func(list[i]);
                                                    }
                                              });
    QTest::newRow("at") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                    for (int i = 0; i < list.size(); ++i) {
                                                        func(list.at(i));
                                                    }
                                              });
    QTest::newRow("range based for") << static_cast<Solver>([](QList<QPointF> list, Func func) {
                                                    for (const auto &i : list) {
                                                        func(i);
                                                    }
                                              });
    QTest::newRow("range based for (const)") << static_cast<Solver>([](const QList<QPointF> &list, Func func) {
                                                    for (const auto &i : list) {
                                                        func(i);
                                                    }
                                              });
}

void Benchmark_foreachTest::doTest()
{
    QFETCH(Solver, solver);

    QBENCHMARK {
        qint64 min = INT_MAX;
        qint64 max = INT_MIN;
        qint64 sum = 0;

        solver(source, [&](const QPointF i) {
            if (i.x() < min)
                min = i.x();
            if (i.y() > max)
                max = i.y();
            sum += i.x();
        });
        QCOMPARE(min, 0);
        QCOMPARE(max, count - 1);
        QCOMPARE(sum, (min + max) * count / 2);
    }
}


QTEST_GUILESS_MAIN(Benchmark_foreachTest)

#include "tst_benchmark_foreachtest.moc"

Qt Quickでマウスによる範囲選択をC++で実装する

概要

Qt勉強会#38@Tokyoで、Qt Quickのマウスによる矩形範囲選択がうまくいかない、というお話を聞いてやってみた内容です。
Qt 勉強会 #38 @ Tokyo - connpass

現象

図のように、矩形の軌跡が残ってしまいます。
f:id:Atsushi4:20160910132433g:plain
よく見ると、ペン幅5pixelを指定しているのに左と上は3pixel、右と下は2pixelになっている、という問題もあります。

再現コード

QQuickPaintedItemを継承、QPainterで描きます。

myitem.h

class MyItem : public QQuickPaintedItem
{
...
private:
    QPoint begin;
    QRect band;
    int penWidth = 5;
};

mousePressで始点を保存、mouseMoveで矩形更新してpaintで描画します。

myitem.cpp

void MyItem::mousePressEvent(QMouseEvent *event)
{
    begin = event->pos();
    band = QRect();
    update();
}
void MyItem::mouseMoveEvent(QMouseEvent *event)
{
    if (begin.isNull()) return;

    band = QRect(begin, event->pos()); // --- (1)
    update(band); // --- (2)
}
void MyItem::paint(QPainter *painter)
{
    painter->fillRect(this->boundingRect(), Qt::green);
    if (band.isNull()) return;

    painter->save();
    QPen pen(QBrush(Qt::red), penWidth);
    painter->setPen(pen);
    painter->drawRect(band);
    painter->restore();
}
void MyItem::mouseReleaseEvent(QMouseEvent *event)
{
    Q_UNUSED(event)
    begin = QPoint();
}

原因と修正

原因

(1) 更新領域の指定に使用している終点はマウスが動いた後のposなので、矩形が小さくなる時に前回の領域が残ってしまう
(2) penWidthを考慮してマージンを入れる必要がある(参考:QPainter::drawRect
http://doc.qt.io/qt-5/qpainter.html#drawRect

修正

というわけでmouseMoveを修正します。

myitem.cpp (diff)

void MyItem::mouseMoveEvent(QMouseEvent *event)
{
    if (begin.isNull()) return;

-   band = QRect(begin, event->pos());
-   update(band);
+   const auto pre = band; // 前回の矩形
+   band = QRect(begin, event->pos()); // 今回の矩形

+   const int topLeftMargin = penWidth / 2;
+   const int bottomRightMargin = (penWidth + 1) / 2; // penWidthが奇数のときは右下にはみ出る
+   const QMargins margins(topLeftMargin, topLeftMargin, bottomRightMargin, bottomRightMargin);

+   update((band | pre) + margins); // 前回+今回+margin --- (3)
}

うまく動くようになりました。
f:id:Atsushi4:20160910145110g:plain

その他

アブノーマルなQRectの算術演算子

(3)で

update((band + margins) | (pre + margins)); // --- (4)

とやると、等価な式に見えるのですがうまくいきません。
QRectのwidthやheightがマイナスの時にマージンを足すと、想定した挙動にならないからです。

QRect r1(30, 40, -10, -30); // not equal (20, 10, 10, 30)
QRect r2 = r1 + QMargins(10, 10, 10, 10); // r2 == QRect(40, 50, 10, -10)

(3)と等価にするには

update((band.normalized() + margins) | (pre.normalized() + margins));

と書く必要があります面倒です意味わかりません何とかしt

ええと、幅-10にマージン20乗せたら幅10だよね、とか、今の仕様も分かるのですが、やっぱり(3)と(4)の結果が等しくないというのは違和感があります。
修正したら誰か困るんかナー。

今回書いた以外の実装

QWidgetアプリケーションでQGraphicsViewを使う場合は

view->setDragMode(QGraphicsView::RubberBandDrag);

でいけると思います。
他にQRubberBandというクラスもあるようですが、使ったことがないのでよくわかりません。

パフォーマンスがあまり気にならないのであれば、
QWidgetではQGraphicsRectItemを、Qt QuickではRectangleを置いてしまう、という手もあると思います。

ついでに

そんなことをしていたら、Qt Creatorでシンボルの名前変更中に「コピー」しようとすると「ペースト」されるという変なバグを踏みまして、勉強会の最後にお話ししたところ早速task_jpさんが直してくれました。ありがとうございます。

Visual StudioでQtをデバッグする

概要

この記事はQt Advent Calendar 2015 - Qiitaの20日の記事です。19日はIoriさんのQt Champions for 2015に選ばれました! - 理ろぐでした。すごい!!超おめでとうございます!!

さて、今回はQt4の始めのころから気になっていた日本語入力のバグをやっとこさ退治したのでそれにまつわるお話を。今回はInput Method回りのデバッグだったので、トレースポイントを使うためにVisual Studioデバッグをしていました。Visual StudioでQtアプリケーションをデバッグするための手順や、実際にやった事を書いていきたいと思います。

バグの内容

バグの内容はhttps://bugreports.qt.io/browse/QTBUG-49955のとおりで、こんな風にPre-Editの文字が残ってしまう、というバグです。
f:id:Atsushi4:20151220112426g:plain

変換候補を表示した状態で次の一文字目のキーを押した時のイベントがうまくハンドリングできていない模様。QWindowsInputContextの問題なので、QtのWidgetsおよびQMLの全ての日本語入力で起きていました。

Qt Creatorでは何故かその後ろにテキストカーソルが固定されて、どこを編集しているのか分からなくなるという事態も。

デバッグ環境

Visual Studio Community 2013を使用しました。

キーボードの入力イベントを追いかけたいので、ブレイクポイントは使えません(フォーカスがVSに移って別のイベントが出てしまうので)。qDebugを埋め込むという手も有るけど、デバッグ出力の内容を変えるたびに再ビルドしないといけなくなります。それなら(最近やっと覚えた)トレースポイントの出番です。GDBでも使えるらしいけどIDEから操作できないし。。。

環境構築

とりあえずこれだけ必要だと思います。括弧内は私が実際に使用した環境です。

  1. Visual Studioのインストール(Visual Studio Community 2013)
  2. Qt Visual Studio Add-inのインストール(Qt5 Visual Studio Add-in 1.2.4)
  3. Qtのソースコード準備(Qt 5.5.1 MSVC 2013 32bit opensource)

ターゲットはQWindowsInputContext(5.5/Src/qtbase/src/plugins/platforms/windows/qwindows.vcxroj内)なのでqtbaseだけビルドできればOKとしました。

Visual Studio

Add-inの使えるものが良いです。Qt Visual Studio Add-inが入れられないとQStringの中身とか見るのが大変で泣けます。最近はCommunityが使えるので嬉しいです。

Qt Visual Studio Add-in

VSで.proファイルを開いたり、QStringやQListの中身を見やすく表示したり、といった機能を提供してくれるらしいです*1。とにかくインストール。Qtダウンロードページの下の方、Other Downloadsに有ります。
http://www.qt.io/download-open-source/#section-7

Qtのソースコード

ソースコードをダウンロードしてきてConfigureしてビルドすれば良いのですが、色々苦い思い出があるのでインストーラが入れてくれた環境を使おうと思い、5.5をインストール(srcのインストールを追加選択)した環境を使おうと思いました。でも結構ハマりました。

結果的にはQt5.5の32bit版をインストールして、5.5/msvc2013/libディレクトリを5.5/Src/qtbase/libにコピーして何とかなりました。

  • 始めは64bitをインストールしたのですがプロジェクト設定は32bitになっていて64bitのLIBやDLLを参照にいっちゃったりしてしっちゃかめっちゃか。面倒くさいのでやりなおし。
  • Src/qtbase/libに入っていないqtfreetyped.libとかが無い、といってリンクエラーになります。プロジェクトの出力先が5.5/msvc2013/のbinとかlibになっているのに参照先は5.5/Src/qtbase/libになっていました。

デバッグ

とりあえずqwindowsinputcontext.cppのあちこちの関数にトレースポイント貼りまくって、呼ばれる関数や参考になりそうな変数を探しました。それからトレースポイントに条件設定したりして絞り込みました。トレースポイントはプログラムを実行しながら付けたり消したり、一時的に無効にしたり、出力内容を変更したりできるのでこういう時には本当に便利だと思います。頻繁に使う場合はTools-Option-Environment-KeyboardでEditorContextMenus.CodeWindow.Breakpoint.InsertTracepointに適当にキーを割り当てましょう。

呼び出し元関数を出力したり、書式指定でdefine名を表示したりできるのは今回初めて知りました。参考にさせて頂いたブログズミさんに感謝。
ブログズミ: [Visual Studio]コードを修正せずに変数の値をログ出力する方法
ブログズミ: [Visual Studio] ウォッチをより便利に使う10の書式

今回のMVPトレースポイントさんはこちら
Location: qwindowscontext.cpp, line 1272
Condition: et==InputMethodRequest||et==InputMethodEndCompositionEvent||et==InputMethodCompositionEvent
Message: et={et}, handled={handled}, message={message}, wParam={wParam}, lParam={lParam}
(書式指定のXとか付けた方が良かった気がしています)

終わりに

ガリガリとコードを書く時はQt Creatorのお世話になる事が多いですが、バグを追いかける時はVSを使う事が多いです。WindowsでQtを使っている(多分少数派の)皆さん、デバッグのお供にVisual Studioはいかがでしょうか。


ちなみにこのバグのパッチは私がpushするbranchを間違えたため5.6には間に合わず5.7でマージされる事になるようですごめんなさいごめんなさいごめんなさい。
追記: たすくせんせーのおかげで、5.6にマージされました。いつもありがとうございます!


明日はynumaさんhttp://qiita.com/ynumaのQt Testです。お楽しみに!

*1:今回の環境ではVSに「Qt」メニューが追加されないのでどこから.proファイルを開けばいいのか分かりませんでした。

QWidgetでもプロパティバインディングがしたい!

はじめに

この記事はQt Advent Calendar 2014 - Qiitaの19日目のエントリーです。

タイトルのとおり、QWidgetベースのアプリケーションでプロパティバインディングできたらいいな、という事で、やってみた内容を書いていきます。
ちなみに元々の動機は、なんかメンテナンス性を上げるためにMVVMで作れ!とかなんとかそんな圧力とかなんとかそんな感じです。
あと、QMetaType周りってちょっと便利そうだけどあんまり使い方が分からないので使ってみたいとかそんな欲求もありました。


早速実際にやってみた

とにかくやってみました。まぁ何とか動くところまではいった、というところでしょうか。

SignalとSlotのQMetaMethodを探してなんとかする

とりあえずSignalとSlotをQMetaObjectのMethodから探して何とかしようと思いました。

static QString findMethod(const QMetaObject *meta, QByteArray propertyName)
{
    for (int i = 0; i < meta->methodCount(); ++i) {
        auto method = meta->method(i);
        if (method.name().toLower() == ("set" + propertyName).toLower())
        {
            return method.methodSignature();
        }
    }
    return QString();
}

void MainWindow::bind(QObject *from, const char *read,
    QObject *to, const char *write) const
{
    auto meta1 = from->metaObject();
    auto meta2 = to->metaObject();

    // Set current value
    to->setProperty(write, from->property(read));

    auto prop1 = meta1->property(meta1->indexOfProperty(read));
    if (!prop1.hasNotifySignal()) return;

    auto prop2 = meta2->property(meta2->indexOfProperty(write));
    QString signature = findMethod(meta2, prop2.name());
    if (signature.isEmpty()) return;

    connect(from, prop1.notifySignal(),
        to, meta2->method(meta2->indexOfMethod(signature.toLatin1())));
}

とりあえずこれで一方のプロパティの変更を他方のプロパティに反映することができるようになりました。ただし以下の制限があります。

  • 二つのプロパティの型が同一であること
  • from側の変更通知シグナルがあること
  • to側にset[プロパティ名]という名前のset用Slotがあること

Qtのクラスは大体3番目の条件を満たしているので、ViewModelのプロパティの型を合わせてnotifySignalを定義すればそれなりに使えそうな気がします。

一旦別のクラスで受ける

上の方法ではちょっと制限がきついので、もう少し考えてみました。

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
public slots:
    void checkChanged();
private:
    void bind(QObject *from, const char *read, QObject *to, const char *write);
private:
    struct BindMap
    {
        QObject *from;
        QByteArray read;
        QObject *to;
        QByteArray write;
    };
private:
    Ui::MainWindow *ui;
    QMultiMap<QObject*,BindMap> objects;
};

void MainWindow::bind(QObject *from, const char *read,
    QObject *to, const char *write)
{
    objects.insertMulti(from, {from, read, to, write});
    to->setProperty(write, from->property(read));

    auto meta1 = from->metaObject();
    auto prop1 = meta1->property(meta1->indexOfProperty(read));
    if (!prop1.hasNotifySignal()) return;

    auto meta = this->metaObject();
    connect(from, prop1.notifySignal(),
            this, meta->method(meta->indexOfMethod("checkChanged()")));
}

これならfrom側にnotifySignalがあってto側のpropertyがwritableならbindできます。型も同一でなくてもQVariantベースで変換できれば変換してくれます。シグナルが関数ポインタで取れればラムダでちょちょいっと書けそうなのですが、それらしきAPIは見つかりませんでした。

まぁあとはちゃんと専用クラスに分離して、途中でdeleteされるクラスの始末とかエラー処理とか入れればそれなりに使える・・・かも?


おわりに

QMetaObjectとか使うとconnectよりも柔軟にpropertyの接続とかできるかもしれません。大量のコントロールのobjectNameをproperty名に合わせておいて全部まとめてset、getするとか(そもそもそれは設計が悪いとか言わない)。

インターフェースクラスと抽象クラスについて

はじめに

これはETロボコン Advent Calendar 2014 - Adventarの15日目のエントリーです。13,14日目はOkazakiYuhei (@Y_uuu) | Twitterさんによるモックのお話でした。
今日はインターフェースクラスと抽象クラスについてのお話です。東海地区大会2日目のモデリングワークショップにおいて、本部審査委員長の渡辺博之さんがとても分かりやすく説明してくださったので共有したいと思って書いています。
ご指摘、質問歓迎します。


抽象クラス、インターフェースクラスとは

さっそく説明を見ていきます。

渡辺さんの説明

例えば動物を抽象化した抽象クラス「動物」と車を抽象化した「車」クラスがあるとして、どちらも「走る」という振舞いを持っているとすると、この「走る」という振舞いをインターフェースクラスとして抜き出せるかもしれないね、というお話でした。クラス図を書くと下のようになります。

fig.1 インターフェースクラス使用前
f:id:Atsushi4:20141215082930p:plain

fig.2 インターフェースクラス使用後
f:id:Atsushi4:20141215083607p:plain

渡辺さんの説明はここまでです。

抽象クラスとは

抽象クラスとは、上の「動物」クラスや「車」クラスのように、共通の性質を持つクラス群を抽象化したクラスです。抽象クラスの特徴は次のとおりです。

インターフェースクラスとは

インターフェースクラスとは、上の「走るインターフェース」クラスのように、通常異なる性質を持つ複数のクラス間において、共通する振舞いのインターフェースを規定するクラスです。インターフェースクラスの特徴は次のとおりです。

  • インターフェースクラスのインスタンスを生成することはできない。
  • インターフェースクラスは実装を持たない。

C++の実装から見た抽象クラスとインターフェースクラス

C++ではそれぞれのクラスがどのように実装されるのか見ていきます。

抽象クラスの実装例

抽象クラスの実装例は次のとおりです。

class Animal
{
protected:
    Animal(){}
public:
    virtual ~Animal(){}
public:
    virtual void run();
};

C++の実装における抽象クラスの特徴は次のとおりです。

  • 抽象クラスのコンストラクタはprotectedにする。
  • 抽象クラスのデストラクタは、抽象クラスのポインタを介して破棄することを許容しない場合はprotected非仮想に、それ以外はpublic virtualにする。
  • 抽象クラスはメンバー変数、メンバー関数(非仮想、仮想、純粋仮想)のいずれも持つことができる。

インターフェースクラスの実装例

インターフェースクラスの実装例は次のとおりです。

class IRunnable
{
public:
    virtual ~IRunnable() {}
public:
    virtual void run() = 0;
};

C++の(望ましい)実装におけるインターフェースクラスの特徴は次のとおりです。

  • publicな純粋仮想関数(なるべく1つ)のみを持つ。
  • インターフェースクラスのデストラクタは、インターフェースクラスのポインタを介して破棄することを許容しない場合はprotected非仮想に、それ以外はpublic virtualにする。

*1デストラクタの実装は上の方に書いた「インターフェースクラスは実装を持たない」に反しますが、C++では派生クラスのメンバーをリークさせないために必要ですね。

インターフェースクラスの利点

例えば動物クラスと車クラスの例では、全てのインスタンスを走らせるために2つのリストをtemplateを使って走査しなければならないかもしれません。

template<typename T>
void doRun(T object) {
    object.run();
}

Animal animals[10];
Car       cars[5];

void runAll() {
    std::for_each(animals, animals + 10, doRun<Animal>);
    std::for_each(cars, cars + 5, doRun<Car>);
}

インターフェースクラスを使うと、1つのリストで扱えます。

void doRun(IRunnable *runnable) {
    runnable->run();
}

Animal animals[10];
Car       cars[5];
IRunnable *objects[15];

void runAll() {
    std::for_each(objects, objects + 15, doRun);
}

インターフェースクラスを使うと振舞いのインターフェースを厳密に決めることができ、戻り値の違いやデフォルト引数などによる関数のシグネチャの違いが無い事を保障することができます。templateを使った例では、runを関数ポインタのpublicメンバー変数にするようなトリッキーなこともできちゃいます。(そっちの方が柔軟だ、という人もいるかもしれませんが)


おわりに

抽象クラスとインターフェースクラスについて書きました。ETロボコンモデリングにおいて、「動物」や「車」(あるいは「銀行」:P)のような、モノを抽象化したクラスに<interface>と書いたら、「あれ、設計がおかしいかもしれない」と見直してみると良いかもしれません。
ちなみにうちのチームのモデルシートは<interface>の人が集約の全体クラスになったりしていてあばばばばば。

2回続けて記事の公開が翌日になってしまいましたごめんなさい。

*1:12/17 デストラクタの記述を追記