QStringBuilder を使用した文字列の結合

この記事は Qt Blog の "String concatenation with QStringBuilder" を翻訳したものです。
執筆: Olivier Goffart, 2011年6月13日

[qt QString] と [qt QByteArray] の operator+ はとても使いやすく、以下のような記述が可能です。

QString directory = /*...*/, name = /*...*/;
QString dataFile = directory + QLatin1Char('/') + name + QLatin1String(".dat");

とても便利ですね。
ここで QLatin1Char と QLatin1String は正確を期すために使用しているだけなので、みなさんがアプリケーションを書く際には省略することも可能です。

というわけで、とても便利であることは分かりました。それではこの記法でのパフォーマンスはどうでしょうか?
それぞれの operator+ はすぐに破棄される一時的な文字列を生成します。つまり、メモリ確保とコピーが何度も発生します。
これは以下のようにすることで高速化することができます。

QString dataFile = directory;
dataFile.reserve(directory.size() + 1 + name.size() + 4);
dataFile += QLatin1Char('/');
dataFile += name;
datafile += QLatin1String(".dat");

メモリの確保もコピーも1度ずつです。しかし最適ではありますが、残念ながらこれでは見栄えがよくありません。
最初の記法で上記と同様の速度にできたらいいと思いませんか?素敵なことにこれは可能です。

Qt 4.6 で QStringBuilder という名前の隠しクラスを Qt に導入しました。
そして、このクラスは Qt 4.8 では QByteArray のサポートという改善がなされました。

この機能はソースコード非互換(以下を参照)のため、明示的に有効化する必要があります。
Qt 4.7 で有効化する方法は Qt 4.7 の QString のドキュメント に記載されています。
しかし、この方法ももはや非推奨で、Qt 4.8 ではこのマクロは QT_USE_QSTRINGBUILDER という新しいマクロ に置き換えられています。QByteArray に対する変更も有効にするにはこのマクロを使用する必要があります。

この高速化は Expression template と呼ばれる手法を用いて実現しています。
文字列を引数に取る operator+ を変更し、結果を最後にまとめて計算する特殊なテンプレートクラスを返すようにしました。

QT_USE_QSTRINGBUILDER が定義されていた場合には、
string1 + string2 は QString へ暗黙のうちに変換される QStringBuilder<QString, QString> 型になるでしょう。

もしかすると operator+ の結果が QString 型であると仮定したコードがあるかもしれないため、これはソースコード非互換です。

QVariant v = someString + someOtherString;
QString s = (someString + someOtherString).toUpper();

これに対する解決方法は QString に明示的にキャストすることです。

QVariant v = QString(someString + someOtherString);
QString s = QString(someString + someOtherString).toUpper();

QT_USE_QSTRINGBUILDER は Qt 自体や Creator をコンパイルする際には既に有効になっています。
以前のバージョンでは QByteArray をサポートしていなかったことに対する 5d3eb7a1 や、Qt 4.8 の QByteArray をサポートするために必要な 7101a3fa など、ソースコード互換の問題に対応するためにいくつかのコミットがなされています。

技術的な詳細

実装自体にもテンプレートを使用したとても良い機能が多数含まれているため、この記事の中でこのクラスの実装の詳細について説明するのは面白いと思いました。技術的に高度な内容なので、この機能を使用する際にはこれからのことを理解している必要は全くありません。

関連する全てのコードは qtringbuilder.h にあります。なお、この記事で紹介するコードは理解しやすいように若干省略しているところもあります。

それでは operator+ の実装を見てみましょう。

template <class A, class B>
QStringBuilder<typename QConcatenable<A>::type, typename QConcatenable<B>::type>
operator+(const A &a, const B &b)
{
return QStringBuilder<typename QConcatenable<A>::type,
typename QConcatenable<B>::type>(a, b);
}

この演算子は、文字列の結合をサポートする型のみで有効になるように SFINAE を使用しています。また、QConcatenable は Qt 内部のテンプレートクラスで、QString, QLatin1String, QChar, QStringRef, QCharRef に加え QByteArray と char* に対応しています。
QConcatenable<T>::type は T 型への typedef で、特殊化 された型にのみ存在します。
例えば QConcatenable<QVariant>::type というのは存在しないため、QVariant に対してはこの operator+ は有効になりません。

operator+(a,b) の場合は QStringBuilder<A, B>(a, b) を返します。
string1 + string2 + string3 の場合には QStringBuilder< QStringBuilder <QString, QString> , QString> 型になるでしょう。

それでは QStringBuilder クラスの中を見てみましょう。

template <typename A, typename B>
class QStringBuilder
{
public:
const A &a;
const B &b;

QStringBuilder(const A &a_, const B &b_) : a(a_), b(b_) {}

template <typename T> T convertTo() const;

typedef typename QConcatenable<QStringBuilder<A, B> >
::ConvertTo ConvertTo;
operator ConvertTo() const { return convertTo<ConvertTo>(); }
};

ConvertTo の typedef は A と B の型に応じて QByteArray か QString になります。詳細は後で見ることにします。ここで大事なのは QStringBuilder クラスは単に引数の参照を保持しているだけということです。
QStringBuilder が QString か QByteArray に暗黙の変換が行われる際に convertTo() 関数が呼ばれます。

template <typename A, typename B> template<typename T>
inline T QStringBuilder<A, B>::convertTo()
{
const uint len = QConcatenable< QStringBuilder<A, B> >::size(*this);
T s(len, Qt::Uninitialized);
typename T::iterator d = s.data();
QConcatenable< QStringBuilder<A, B> >::appendTo(*this, d);
return s;
}

この関数では QString か QByteArray のコンテナを適切なサイズで初期化せずに生成した後で、それぞれの文字をその中にコピーしています。
実際のコピーは QConcatenable< QStringBuilder<A, B> >::appendTo が行います。
QStringBuilder<A, B> に対する QConcatenable の部分特殊化により、個別の部分の結果を組み合わせることができます。1行の中に複数の operator+ がある場合、A は別の QStringBuilder 型になります。

template <class A, class B>
struct QConcatenable< QStringBuilder<A, B> >
{
typedef QStringBuilder<A, B> type;
typedef typename QtStringBuilder::ConvertToTypeHelper<
typename QConcatenable<A>::ConvertTo,
typename QConcatenable<B>::ConvertTo>::ConvertTo ConvertTo;
static int size(const type &p)
{
return QConcatenable<A>::size(p.a)
+ QConcatenable<B>::size(p.b);
}
template<typename T> static inline void appendTo(
const type &p, T *&out)
{
QConcatenable<A>::appendTo(p.a, out);
QConcatenable<B>::appendTo(p.b, out);
}
};

QConcatenable::appendTo 関数は文字列を結果のバッファにコピーする責任を果たします。

例えば、以下のコードが QString に対する QConcatenable になります。

template <> struct QConcatenable<QString>
{
typedef QString type;
typedef QString ConvertTo;
static int size(const QString &a) { return a.size(); }
static inline void appendTo(const QString &a, QChar *&out)
{
const int n = a.size();
memcpy(out, reinterpret_cast<const char*>(a.constData()),
sizeof(QChar) * n);
out += n;
}
};

QString への変換なのか QByteArray への変換なのかはどうやって知るのでしょうか?これを理解するために ConvertTo 型がどのように決定されるのかを見てみましょう。

namespace QtStringBuilder {
template <typename C, typename D> struct ConvertToTypeHelper
{ typedef C ConvertTo; };
template <typename T> struct ConvertToTypeHelper<T, QString>
{ typedef QString ConvertTo; };
}

QConcatenable< QStringBuilder<A, B> >::ConvertTo を計算するために ConvertToTypeHelper が使用されます。これはテンプレートの計算です。これは2つの型の引数(C と D)を取り、ConvertToTypeHelper::ConvertTo の typedef の型を返す関数のように見ることができます。
ConvertTo はデフォルトでは常に最初の型です。しかし、2番目の型が QString の場合にはテンプレートの部分特殊化が行われるため QString が "返される" ことになります。
簡単に言うと、どちらかの型が QString であれば QString が返されます。

Unicode を扱う型(QString、QLatin1String、QChar、...)での QConcatenable の特殊化では ConvertTo が QString になるのに対して、他の 8ビット文字用の方では QByteArray が ConvertTo の typedef になります。

次に QByteArray の特殊化を見てみましょう。

template <> struct QConcatenable<QByteArray> : private QAbstractConcatenable
{
typedef QByteArray type;
typedef QByteArray ConvertTo;
static int size(const QByteArray &ba) { return ba.size(); }
#ifndef QT_NO_CAST_FROM_ASCII
static inline void appendTo(const QByteArray &ba, QChar *&out)
{
QAbstractConcatenable::convertFromAscii(ba.constData(),
ba.size(), out);
}
#endif
static inline void appendTo(const QByteArray &ba, char *&out)
{
const char *a = ba.constData();
const char * const end = ba.end();
while (a != end)
*out++ = *a++;
}
};

QString と同じですが、Qt では QByteArray から QString への暗黙の変換が許されているため、ASCII から Unicode への変換のオーバーロードが存在しています。これは QT_NO_CAST_FROM_ASCII を定義することで無効化することができます。ライブラリを開発する際には、それを使用するアプリケーションの開発者がそのコードにどの文字コードを使用するかは分からないため、(QLatin1String を使用した)明示的な変換のみを使用するのが良いでしょう。

結論

UTF-8 などのいくつかの文字コードではサイズが異なる可能性があること(コードの ExactSize を見てください)に対応していることなど、いくつかの詳細はここでは省略しました。

今回の解説はいかがでしたでしょうか。
Qt の他の部分でも説明をしてもらいたいところがあれば(訳注:元記事 の)コメントでお知らせください。

(話は変わりますが、QLatin1Literal という名前を聞いたことがあってもそれは使ってはいけません。コンパイラの中には文字列リテラルの長さをコンパイル時に計算する strlen が組み込まれているものがあります。)


Blog Topics:

Comments