Back to Blog home

使用QStringBuilder进行字符串连接

Published on Monday August 22, 2011 by Debao Zhang in C++ Performance Qt qt-labs-chinese | Comments

原文链接:Olivier GoffartString concatenation with QStringBuilder

QString和QByteArray提供了非常便利的operator+,以允许你写这样的代码:

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

非常方便。

QLatin1CharQLatin1String用在这儿只是出于正确性的考虑,你在编写自己的应用程序时可以省略它们。

虽然我们有了很方便的东西,但是这种表达式的性能如何呢?
每个operator+都会创建一个一个临时字符串,然后将其丢弃,这意味着,会有很多次的内存分配和拷贝。

如果(像下面)这样做的话,将会快很多。

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

只需要一次内存分配和拷贝,这是最优的结果。但不幸的是,看起来不是那么好。
倘若第一个表达式和上面这个一样快会怎么样?好消息是——这是可能实现的。

在Qt 4.6中我们引入一个隐藏的类:QStringBuilder。
在4.8中我们添加了对QByteArray的支持。

由于这是源码不兼容的(见下文),你需要显式地启用它。
在Qt 4.7中启用它的方法在4.7 QString文档中有介绍。
但是这种方法现在被废弃了,而且在Qt 4.8中,这个宏已被新的QT_USE_QSTRINGBUILDER宏所替代。要想受益于QByteArray的改变,你必须使用新的宏。

为了使其工作,我们使用了一个被称为表达式模板(Expression template)的技术。
我们修改了一些接受字符串的operator+,使其返回一个特殊的模板类,它的结果将会延迟(lazily)计算。

举例来说,当定义QT_USE_QSTRINGBUILDER后,string1 + string2的类型将是可以隐式转换成QString的QStringBuilder<QString, QString>

这是源码不兼容的,因为你可能写有假定operator+的返回值是QSting类型的代码。

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

解决方案是显式转换成QString:

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

编译Qt自身和Qt Creator时,QT_USE_QSTRINGBUILDER 已经被启用了。
一些修复源码兼容性问题的提交(commit)有:
5d3eb7a1对于尚未支持QByteArray的早期版本,和7101a3fa在Qt 4.8中添加对QByteArray支持。

技术细节

考虑到本实现展示了许多很好的模板特性,我认为在本文中解释一点这个类的细节将会非常有趣。它是高度技术性的,但使用它的话却完全不要求你理解这些。

一切均在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来做到仅对支持字符串连接的类型起作用。实际上,QContatenable是一个只对QString、QLatin1String、QChar、QStringRef、QCharRef以及QByteArray和char*进行了特化的内部模板类。
QConcatenable<T>::type是类型T的别名(typedef),且只对这些特殊的类型有效。
比如,由于QConcatenable<QVariant>::type不存在,operator+用于QVariant时将不会被启用。

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>(); }
};

依赖于类型A和B,别名ConvertTo将代表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。如果同一行中有许多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; };
}

ConvertToTypeHelper被用来计算QConcatenable< QStringBuilder<A, B> >::ConvertTo。它是一个模板计算(template computation)。它可以被看作是接收两个类型参数(C和D)并以别名ConvertToTypeHelper::ConvertTo的类型返回的函数。
默认情况下,ConvertTo总是第一个类型。但如果第二个类型是QString,模板偏特化将被使用,而QString将被“返回”。
在实际中,这意味着只要任何一个类型是QString,QString就将被返回。

为可感知unicode的类型(QString、QLatin1String、QChar、...)特化的QConcatenable将QString取为ConvertTo,而其他基于8位字符的类型将ConvertTo
作为QByteArray的别名。

现在让我们看一下关于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将在编译时被计算)

Subscribe to Our Blog

Stay up to date with the latest marketing, sales and service tips and news.