目次
- 目的
- Qtコンテナ
- 概要(例外安全性について)
- 暗黙的共有
- emplaceと右辺値参照
- スレッドセーフ
- foreachマクロ
- Qtコンテナファミリー
- ?(疑問点)
- まとめ
- 次回予告
目的
C++/Qtを利用する際にハマりがちな、Qtコンテナについて調査します。わかりやすく言えば、筆者がハマってしまったところを、後々の方々が見返しやすいようにまとめています。英語ドキュメントが多く、誤解を生みがちなので、筆者と同じ轍を踏まないようにできればと考えています。このシリーズでは、あまり詳しいコードを書くつもりはありません。C++はわかるけど、Qtには慣れていないという方に向けて、噛み砕いた説明をします。Qtコンテナ
概要
Qtコンテナは、STLコンテナやBoostコンテナとは設計思想が異なります。明確に区別できるようにしましょう。QtコンテナとQtの汎用的なコンポーネントクラスは重要な関連付けがされているので、Qtコンテナを知る前に、まずはQtクラスがどのように作られているのかを知る必要があります。まず、Qtクラスはコピー代入演算子やコピーコンストラクタで例外を投げないこと(型安全性)が暗黙的に保証されています。したがって、例えば次のようなコードが実現できます。QtType obj = list.takeAt(2);// 型安全性が保証されていない場合は
// 要素が消えた後に例外が投げられる可能性がある
Qtにおいて、takeとは、「要素を削除し、そのオブジェクトを返す」ことを意味します。要素の型安全性が無いと、この関数を使うことは危険です。自作クラスをQtコンテナに入れるなら、次のように書く(もしくは、コピーコンストラクタとコピー代入演算子にQ_DECL_NOEXCEPTキーワードを宣言する)必要があります。
MyType obj = list.at(2);// 例外を含んでも問題ない
list.removeAt(2);
Qtにおいて、atとは、「そのオブジェクトをconst参照として返す」ことを意味します。removeは見ての通りです。これらの例から分かるように。QtコンテナはQtクラスを使う限り例外安全性は強いのですが、自作クラスを使う場合は完全にユーザー任せになります。ここで重要なことがあります。Qtコンテナはいかなる場合も、要素から投げられた例外を拾わないことを保証しています。つまり、自作データクラスを入れたQtコンテナは基本的に例外中立です。
暗黙的共有
Qtコンテナを選択するもうひとつの利点は、Qtのコンテナ系クラスに暗黙的共有(Implicitly Sharing)という設計が施されていることにあります。話が逸れますが、これはPHPのArrayのコピーオンライトにも共通しています。コピーオンライトと言い換えた方が分かりやすいかもしれませんが、ここではQtのネーミングに従って暗黙的共有と呼びます。暗黙的共有が適用されたQtコンテナ系クラスは最大限のリソースと、最小限のコピーを活用する機能を持ちます。これらのクラスの内部では、オブジェクトをコピーする時点(正確には、コピーするための引数として渡された時点)では、実際にはコピーせず、ポインタをデータとして保持します。そして、データを書き換える時点で初めて、コピーが行われます。厳密な仕組みは、Qtの公式ドキュメントを読んでください。この暗黙的共有はブラックボックスとなっているので、ユーザー側からほとんど気にすることがありませんが、QtコンテナのSTLスタイルのイテレータを使う際に、暗黙的共有絡みの、ある問題が発生します。この詳細は、Qt5のドキュメント”Container Classes”を参照してください。QtコンテナはJavaスタイルのイテレータをサポートしているので、この問題を避けたい場合は、Javaスタイルのイテレータを使います。また、Javaイテレータなら、STLイテレータ特有のremove問題も解消されます。ただし、パフォーマンスの点から考えると、よりlow-levelのSTLスタイルのイテレータを使うべきです。emplaceと右辺値参照
次は、Qtコンテナの欠点を紹介しましょう。QtコンテナはSTLコンテナで既に標準実装されている、emplace関数とrvalue referenceオーバーロードを持っていません。理由は分かりませんが、これらを使う場合は、他のコンテナを利用します。しかしながら、実際は、QtではQObjectのポインタをコンテナに入れることが殆どなので、気にすることではありません。そもそも論になりますが、QObjectはそもそもコピーがdeleted宣言されているので以下略。それに、Qtコンテナは前述の通り、暗黙的共有による最適化がありますので、rvalue referenceオーバーロードに関しては、それと似たパフォーマンスを得られます。それどころか、lvalue referenceにでさえ効果があるのですから、これで十分なのではないでしょうか。また、ムーブコンストラクタを書かないことは、Qtの掲げるCode lessにも適います。スレッドセーフ
Qtはクロスプラットフォームのマルチスレッドプログラミングをサポートしています。QThreadというクラスから利用可能です。詳細はQtのドキュメントを参照してください。ここで気になることが、スレッドセーフの是非です。STLコンテナでは、constメンバによるRead-Onlyの場合はいかなる場合もスレッドセーフであり、異なる要素にアクセスしたときのみ、書き込みに関してもスレッドセーフであると規定されています。そして、Qtコンテナも同様のスレッドセーフを保証しています。マルチスレッドによる暗黙的共有に対する影響はありません。foreachマクロ
Qtは全てのコンテナクラスに対してforeachマクロを持っています。foreachマクロでは、前述の暗黙的共有による擬似コピーを行い、それを代入した変数を次々と返します。参照にすることもできるので、元データの書き換えも可能です。C++11で標準搭載されたranged forと、本質的には異なるものの、扱いは似ています。Qtコンテナファミリー
Qtコンテナは他のコンテナ同様、様々なタイプを持ちます。赤黒木のQMap、そのサブクラスであるQMultiMap、ハッシュテーブルのQHash、お馴染みのQListや、インソートの速いQLinkedList。その他、多種多様なコンテナを持ちます。一つ重要なのは、基本的にQListはQVectorの上位互換である、ということです。QVectorは強いシーケンスコンテナとしての性質を持っているので、連続したメモリ領域の使用が強いられる場合のみ、用いられます。一方で、このQVectorを応用したコンテナに、QVarLengthArrayという可変長な固定長コンテナがあります。QVarLengthArrayはコンパイル時に組み込み型定数をテンプレート引数に取ることにより、予めreserveされた状態になっているので、要素を追加する度にメモリを初期化する必要はありません。また、QVarLengthArrayはQVectorよりlow-levelな実装がされています。ただし、QVarLengthArrayは暗黙的共有を行わない点に注意してください。このコンテナの優れているところは、必要に応じて容量を拡張できる点です。これにより、例外安全性が強くなります。ただし、その場合のパフォーマンスは落ちることを覚悟してください。?
ちなみに、よくわかりませんが、Qtコンテナにコピー代入演算子をdeleted宣言したクラスを入れるとコンパイル時エラーになります。暗黙的共有ができなくなるのでしょうか。暗黙的共有をしないQVarLengthArrayにそれを入れて試してみましょうか。まとめ
ザックリとした記事でしたが、ここまで読んでくださいまして、ありがとうございました。テストコードが少なかったでしょうか。Qtドキュメントに多くのサンプルが載せられているので、なるべく紙面を簡略化するためにも、この記事では省略してみました。Qtをあまり知らなかったという方は、Qtに少しでも興味を持っていただければ幸いです。また、Qtを今利用しているという方は、こういうこともできる、とか、これは知らなかった、ここはこうだ、などの感想を頂けると幸いです。ご意見、ご質問等も可能な範囲で受け付けております。
よりQtを知るためにも、今後はQtコンテナの内部実装について調べていきたいと考えております。
※この記事はQt公式ドキュメント(doc.qt.io)を参考に書かれました。
次回予告
次回は、Qtのメタオブジェクトシステムを紹介する予定です。 Qtのシグナル/スロットは、メタオブジェクトシステムによって構成されています。
0 件のコメント:
コメントを投稿