2015/08/25

Qtコンテナ――C++/Qtの深いところ#1


 例外安全性、暗黙的共有、emplaceと右辺値参照、スレッドセーフ・・・
※注意 Qt5.5時点の情報です。

目次

  • 目的
  •  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 件のコメント:

コメントを投稿