Qt のコンテナクラスについて

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

はじめに

@luyikei です。 Qt が大好きな人間です。最近あまりやってないのですが...
Qt Advent Calendar 2014 も最終日ですね。少し寂しくなりますね。今日はクリスマスです。皆さん楽しんでいますか?!今日は Qt のコンテナクラスについての記事を書きます。元々は競技プログラミングを Qt でやる記事を書こうと思いましたが考えてみると多くの部分が Qt のコンテナクラスによって改善されると思ったのでコンテナクラスに特化した記事にしました。多分少しやっている皆さんならば既知の事項が多いと思いますがよろしくおねがいします。

多くの部分は Container Classes の参考です。興味があればそちらも参照してください。

コンテナクラス

Qt のコンテナクラスは多くが C++ の STL のものを発展させたものです。例えば、 STL の vector<T> は Qt の QVector<T> に相当します。では何故、 Qt で独自に再設計する必要があるのでしょうか。それはとてもシンプルで公式ドキュメントにあるように

These container classes are designed to be lighter, safer, and easier to use than the STL containers. If you are unfamiliar with the STL, or prefer to do things the "Qt way", you can use these classes instead of the STL classes.

いいことずくめですね!やはり Qt でプログラミングするならば多くの場合は Qt のコンテナクラスを使用するべきだと思います。では早速一つずつ見て行きましょう。

今回は Qt のコンテナクラスの楽しさを実感してもらいたいため、QVector<T> を例に説明します。他のクラスについて詳しくは Container Classes を参照してください。

QVector<T> を例に

これは名称の通りベクトル型で値の組を保存するクラスですね。STL の vector<T> は多くの場合は動的配列として使われると思います。例えば STL の vector<T> はこのように使われます。

vector.cpp
vector<int> v;
v.push_back(1); // 1 を追加
cout << v[0] << endl; //表示

v.push_back(2);
v.push_back(3);
// すべて表示(添字)
for (int i=0; i < v.size(); ++i){
    cout << v[i] << endl;
}

// すべて表示(イテレーター)
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it){
    cout << *it << endl;
}

びみょ~ですね。QVector<T> を使うと

qvector.cpp
QVector<int> v;
v.append(1); // 1 を追加 append のほうが Qt っぽい push_back も勿論使える。
cout << v[0] << endl; //表示

v << 2 << 3; // 演算子のオーバーロードをしているため追加が楽に行える。2, 3 を追加している。

追加だけでも見栄えば全く違います。

for ループだけをとっても

for ループで全要素にアクセスするときも

qvector.cpp
// すべて表示(添字)
for (int i=0; i < v.size(); ++i){
    cout << v.at(i) << endl; // at() 使うとディープコピーが行われないため、高速。ただし Read-Only
}

// すべて表示(Java-Style イテレーター)
QVectorIterator<int> it(v);
while (it.hasNext())
    cout << it.next() << endl; // Java-Style なので注意!

// すべて表示(STL-Style イテレーター)
for (QVector<int>::iterator it = v.begin(); it != v.end(); ++it){
    cout << *it << endl; // 従来の方式でも
}

Java-Style のイテレーターの方が個人的には見やすいと思います。しかし QVectorIterator は const イテレーターで Read-Only なので値の変更可能なイテレーターを使用したい場合は QMutableVectorIterator<T> を使用してください。そして値を変更する場合は setValue(const T & value) 関数を使用してください。
実は Qt で foreach マクロが定義されているためもっと見やすい書き方ができます。

qvector.cpp
foreach (const int &i, v)
{
  cout << i << endl;
}

しかしマクロで定義されているので注意してください。(例えば QPair<int, int> のようにデータ型にカンマが入っている場合。詳しくは The foreach Keyword を参照してください。)
また、 C++11 で似たような機能が実装されているので

qvector.cpp
// C++11 Only!
for (const int &i : v){
    cout << i << endl;
}

これに限らず、 C++11 を使用する際、新たなテクニックが幾つか使えるで、興味がある方は C++0x in Qt を参照してください。
また、C++11 形式の for はディープコピーが行われないが、 Qt のマクロの foreach はディープコピーが行われるという相違点もあります。for ループでは要素が追加や削除が行われた時の挙動が予測できないわけです。これも上記のリンクで解説されています。

ユニークな関数たち

では、STL の vector には定義されていない関数を紹介します。QVector は vector であるにもかかわらず、先頭に追加する関数があります。

qvector.cpp
v.push_back(1); // 1 を先頭に追加 (STL っぽい書き方)
v.prepend(1); // 2 を先頭に追加 (Qt っぽい書き方)

連結リストで実装されてないため、先頭に追加する処理はデータ量が多ければ多いほど重くなります。(全データをずらしているため)。先頭に追加する場合は時間計算量は O(N) ですが、QList<T> は O(1) です。多くの機能が QList<T> にも実装されているため、差異がデータ構造以外にほぼなくなっているので、使用するときはそういったことを考慮するべきだと思います。

任意のインディックスの要素の削除も簡単にできます。

qvector.cpp
v.remove(1); // 2番目の要素を削除

これを STL で再現するとこうなります

vector.cpp
v.erase(v.begin() + 1); // イテレーターで削除

また任意の値の要素を全て削除できます。

qvector.cpp
v << 1 << 1 << 2 << 3;
v.removeAll(1); // 1 を全て削除

これを STL で再現するとこうなります

vector.cpp
v.push_back(1);
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.erase(remove(v.begin(), v.end(), 1), v.end()); // イテレーターで削除

これらの関数は全て QList<T> との互換性のために実装されたものであるため、こういった処理を沢山行う場合は QList<T> を使用したほうがいいと思います。

値の検索も容易に行えます。(戻り値はインディックス。また、単に存在の確認なら contains(const T & value) が便利です。)

qvector.cpp
v << 1 << 1 << 2 << 3;

cout << v.indexOf(1) << endl; // 出力: 0
cout << v.indexOf(2) << endl; // 出力: 2
cout << v.indexOf(3) << endl; // 出力: 3

cout << v.indexOf(4) << endl; // 出力: -1

これを STL で再現するとこうなります

vector.cpp
v.push_back(1);
v.push_back(1);
v.push_back(2);
v.push_back(3);

vector<int>::iterator it = find(v.begin(), v.end(), 1); // 1を検索

if ( it == v.end() ){
    cout << -1 << endl;
} else {
    cout << static_cast<int>(it - v.begin()) << endl;
}

とんでもないですね! Qt の有り難みを感じます。

また、 QVector は fromStdVector(const std::vector & vector) によて STL の vector から変換できますし、 toStdVector() で逆もできます。

おわりに

書いていて思ったのは、やはり Qt は日本語の情報が少ないということです。しかも情報が全体的に古いという二重苦に悩まされています。
一方で Qt の公式ドキュメントは非常によく整理されています。英語がわかる人、抵抗がない人はほとんど英語のドキュメントを読んで開発しなさっていると思います。

しかし、やはり初心者にとって日本語の情報が少ないというのは Qt の大きな障壁になると思いますし、私もそうでした。なので、こういった Qt Advent Calendar 2014 と言った情報を積極的に発信する機会が設けてもらって本当に良かったと思います。この Qt Advent Calendar 2014 を作成してくださった@task_jp さん、投稿してくださった皆さん、本当にありがとうございました。

この投稿は Qt Advent Calendar 201425日目の記事です。