使用QStringBuilder进行字符串连接
来源 https://www.qt.io/cn/blog/2011/08/22/string-concatenation-with-qstringbuilder
原文链接:Olivier Goffart - String concatenation with QStringBuilder
QString和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");
只需要一次内存分配和拷贝,这是最优的结果。但不幸的是,看起来不是那么好。
倘若第一个表达式和上面这个一样快会怎么样?好消息是——这是可能实现的。
在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
。
这是源码不兼容的,因为你可能写有假定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
QStringBuilder::type, typename QConcatenable::type>
operator+(const A &a, const B &b)
{
return QStringBuilder::type,
typename QConcatenable::type>(a, b);
}
该操作符使用SFINAE来做到仅对支持字符串连接的类型起作用。实际上,QContatenable是一个只对QString、QLatin1String、QChar、QStringRef、QCharRef以及QByteArray和char*进行了特化的内部模板类。
QConcatenable
比如,由于QConcatenableoperator+
用于QVariant时将不会被启用。
operator+(a,b)
简单地返回QStringBuilder(a, b);
。
像这样的一些东西string1 + string2 + string3
,其结果的类型将是 QStringBuilder< QStringBuilder
现在我们可以看一下QStringBuilder类
template
class QStringBuilder
{
public:
const A &a;
const B &b;
QStringBuilder(const A &a_, const B &b_) : a(a_), b(b_) {}
template
typedef typename QConcatenable
::ConvertTo ConvertTo;
operator ConvertTo() const { return convertTo
};
依赖于类型A和B,别名ConvertTo将代表QByteArray或QString,稍后我们会看到这是如何做到的。因此QStringBuilder只保存它的操作数的引用。
当QStringBuilder隐式地被转换成QString或QByteArray时,函数convertTo()
将被调用:
templatetemplate
inline T QStringBuilder::convertTo()
{
const uint len = QConcatenable< QStringBuilder >::size(*this);
T s(len, Qt::Uninitialized);
typename T::iterator d = s.data();
QConcatenable< QStringBuilder >::appendTo(*this, d);
return s;
}
该函数创建一个合适大小的未初始化的QString或QByteArray并把这些字符复制到里面。
实际的拷贝委托给了QConcatenable
。
将独立的片段进行合并的是用QStringBuilder偏特化后的模板QConcatenable。如果同一行中有许多operator+
,那么A将是另一个QStringBuilder类型。
template
struct QConcatenable< QStringBuilder >
{
typedef QStringBuilder type;
typedef typename QtStringBuilder::ConvertToTypeHelper<
typename QConcatenable::ConvertTo,
typename QConcatenable::ConvertTo>::ConvertTo ConvertTo;
static int size(const type &p)
{
return QConcatenable::size(p.a)
+ QConcatenable::size(p.b);
}
templatestatic inline void appendTo(
const type &p, T *&out)
{
QConcatenable::appendTo(p.a, out);
QConcatenable::appendTo(p.b, out);
}
};
函数QConcatenable::appendTo
负责将字符串拷贝到最终的缓冲区。
举例来说,对于QString,这是QConcatenable看起来的样子
template <> struct QConcatenable
{
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(a.constData()),
sizeof(QChar) * n);
out += n;
}
};
我们如何才能知道我们需要转换成QString还是QByteArray?让我们来尝试理解一下ConvertTo类型是如何确定的:
namespace QtStringBuilder {
templatestruct ConvertToTypeHelper
{ typedef C ConvertTo; };
templatestruct ConvertToTypeHelper
{ typedef QString ConvertTo; };
}
ConvertToTypeHelper被用来计算QConcatenable< QStringBuilder >::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: 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
将在编译时被计算)
================= End