使用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<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
將在編譯時被計算)
================= End