QStringLiteral


         原作者: Olivier Goffart 點擊打開鏈接http://woboq.com/blog/qstringliteral.html

         譯者: zzjin 點擊打開鏈接http://www.tuicool.com/articles/6nUrIr                   

QStringLieral是Qt5中新引入的一個用來從字符串常量創建QString對象的宏(字符串常量指在源碼中由雙引號包含的字符串)。在這篇博客我講解釋它的的內部實現和工作原理。

 

提要

讓我們從它的使用環境開始說起:假設你想要在Qt5中從字符串常量初始化一個QString對象,你應該這樣:

大多數情況:

(1)使用QStringLiteral(“某字符串”) --如果它最終轉會換成QString的話

(2)使用QLatin1String(“某字符串”) --如果使用的函數有支持QLatin1String的重載(比如operator==, operator+, startWith, replace等)的話

我把這段話放在最開始是爲了那些不怎麼想了解其具體技術細節的人着想。繼續閱讀你將瞭解QStringLiteral是如何工作的。

 

QString的工作方式

QStringQt中的其他類一樣,是一個隱式共享類。它唯一的數據成員就是一個指向其私有數據的指針。QStringDatamalloc函數分配空間,並且在其後(同一塊內存塊)分配了足夠的空間來存放實際的字符數據。

//爲了此博客的目標做了簡化

struct QStringData {

   QtPrivate::RefCount ref; // QAtomicInt進行封裝

   int size; // 字符串的大小

   uint alloc : 31 ; // 該字符串數據之後預留的內存數

   uint capacityReserved : 1 ; // reserve()使用到的內部細節

   qptrdiff offset; // 數據的偏移量 (通常是 sizeof(QStringData))

   inline ushort *data()

   { return reinterpret_cast < ushort *>( reinterpret_cast <char *>( this ) + offset); }

};

// ...

class QString {

   QStringData *d;

public :

   // ... 公共 API ...

};

 

offset是指向QStringData相對數據的指針。在Qt4中它是一個實際的指針。稍後我們會講到爲什麼這個指針發生了變化。在字符串中保存的實際數據是UTF-16編碼的,這意味着每一個字符都佔用了兩個字節。

 

文字與轉換

字符串常量是指直接在源碼中用引號包起來的字符串。 這有一些例子。(假設action,stringfilename都是QString類型)

o->setObjectName( "MyObject" );

if (action == "rename" )

string.replace( "%FileName%" , filename);

第一行我們調用了QObject::setObjectName(const QString&)函數。這裏有一個通過構造函數產生的從const char*QString的隱式轉換。一個新的QStringData獲取了足夠保存 "MyObject"字符串的空間,接着這個字符串 從 UTF-8轉碼爲UTF-16並拷貝到Data內 。 
在最後一行調用QString::replace(const QString &, const QString &)函數的時候也發生了相同的操作,一個新的QStringData獲取了保存 "%FileName%"的空間。


有辦法避免QStringData的內存分配和字符串的複製操作嗎?
當然有,創建臨時的QString對象耗費甚巨,解決這個問題的一個方法是重載一個const char*作爲參數的通用方法。 於是 我們有了下面的這幾個賦值運算符重載:

bool operator==( const QString &, const QString &);

bool operator==( const QString &, const char *);

bool operator==( const char *, const QString &)

這些重載運算可以直接操作原始char*,不必爲了我們的字符串常量去創建臨時QString對象。

 

編碼與 QLatin1String

Qt5中,我們把char* 字符串的默認編碼 改成了UTF-8。但是相對純ASCII或者latin1而言,很多算法處理UTF-8編碼數據的時候會慢很多。

因此你可以使用QLatin1String,它是在確定編碼的情況下對char*進行的輕量級封裝。一些接收QLatin1String爲參數的重載函數能夠直接對純latin1數據進行處理,不必進行編碼轉換。

所以我們的第一個例子現在看起來是這樣了:

o->setObjectName( QLatin1String ( "MyObject" ));

if (action == QLatin1String ( "rename" ))

     string.replace( QLatin1String ( "%FileName%" ), filename);

好消息是QString::replaceoperator==操作有了針對QLatin1String的重載函數,所以現在快很多。

在對s etObjectName的調用中,我們避免了從UTF-8的編碼轉換,但是我們仍然需要進行一次從QLatin1StringQString的(隱性)轉換, 所以不得不堆中分配QStringData的空間。

 

QStringLiteral

有沒有可能在調用setObjectName的時候同時阻止分配空間與複製字符串常量呢?當然,這就是 QStringLiteral所做的。

這個宏會在編譯時嘗試生成QStringData,並初始化其全部字段。它甚至是存放在.rodata內存段 中所以可以在不同的進程中共享。

爲了實現這個目標我們需要兩個C++語言的特性:

在編譯的時候生成UTF-16格式字符串的可能性 
Win環境下我們可以使用寬字符 L"String" Unix環境下我們使用新的C++11 Unicode字符串: u"String"。( GCC 4.4clang支持。)

從表達式中創建靜態數據的能力 
我們希望能把QStringLiteral放在代碼的任何地方。一種實現方法就是把一個靜態的QStringData放入一個C++11 lambda 表達式。(MSVC 2010GCC 4.5支持) (我們同樣用到了GCC  __extension__ ({ })   )

實現

我們需要一個同時包含了QStringData和實際字符串的POD結構。這個結構取決於我們生成的UTF-16時使用的實現方法。

定義QT_UNICODE_LITERAL_II並且聲明基於編譯器的qunicodechar  */

#if defined(Q_COMPILER_UNICODE_STRINGS)

    // C++11 unicode 字符串

    #define QT_UNICODE_LITERAL_II(str) u"" str

    typedef char16_t qunicodechar;

#elif __SIZEOF_WCHAR_T__ == 2

    // wchar_t是兩個字節  (這裏條件被適當簡化)

    #define QT_UNICODE_LITERAL_II(str) L##str

    typedef wchar_t qunicodechar;

#else

    typedef ushort qunicodechar; //fallback

#endif

// 會包含字符串的結構體

// N是字符串大小

template < int N>

struct QStaticStringData

{

     QStringData str;

     qunicodechar data[N + 1 ];

};

// 包裹了指針的輔助類使得我們可以將其傳遞給QString的構造函數

struct QStringDataPtr

{ QStringData *ptr; };

if defined(QT_UNICODE_LITERAL_II)

// QT_UNICODE_LITERAL needed because of macro expension rules

# define QT_UNICODE_LITERAL(str) QT_UNICODE_LITERAL_II(str)

# if defined(Q_COMPILER_LAMBDA)

#  define QStringLiteral(str) \

     ([]() ->  QString   { \

         enum   { Size =  sizeof ( QT_UNICODE_LITERAL (str))/ 2  -  1   }; \

         static   const   QStaticStringData<Size> qstring_literal = { \

             Q_STATIC_STRING_DATA_HEADER_INITIALIZER(Size), \

             QT_UNICODE_LITERAL (str) }; \

         QStringDataPtr   holder = { &qstring_literal.str }; \

         const   QString   s(holder); \

         return   s; \

     }()) \

# elif defined(Q_CC_GNU)

// 使用GCC的 __extension__ ({ }) 技巧代替lambda

// ... <skiped> ...

# endif

#endif

#ifndef QStringLiteral

// 不支持lambdas, 不是GCC,或者GCCC++98模式,使用4字節wchar_t

// fallback, 返回一個臨時的QString

// 默認認爲源碼爲utf-8編碼

# define QStringLiteral(str) QString::fromUtf8(str, sizeof(str) - 1)

#endif

 

讓我們稍微簡化一下這個宏,然後看看這個宏是如何展開的

o->setObjectName( QStringLiteral ( "MyObject" ));

// 將展開爲:

o->setObjectName(([]() {

         // 我們在一個返回QStaticStringlambda表達式中

         // 使用sizeof計算大小(去掉末尾的零結束符)

         enum { Size = sizeof (u "MyObject" )/ 2 - 1 };

         // 初始化(靜態數據在編譯時初始化)

         static const QStaticStringData <Size> qstring_literal =

         { { /* ref = */ - 1 ,

             /* size = */ Size,

             /* alloc = */ 0 ,

             /* capacityReserved = */ 0 ,

             /* offset = */ sizeof ( QStringData ) },

           u "MyObject" };

          QStringDataPtr holder = { &qstring_literal.str };

          QString s(holder);// 調用QString(QStringDataPtr&)構造函數

          return s;

     }()) // 調用lambda

   );

引用計數器初始化爲-1。由於這是隻讀數據所以這個負數永遠不會發生增減。

可以看到,我們使用一個偏移量(qptrdiff)而不是向Qt4中那樣使用一個指向字符串的指針是多麼重要。把一個指針放在一個只讀的部分裏面是完全不可能的,因爲指針很可能會在加載時 重新分配 。這意味着每次啓動或者調用程序、庫文件的時候操作系統都不得不用重分配表重寫全部的指針地址。

數據結果

爲了好玩,我們來看一段從一個非常簡單的對QStringLiteral的調用後生成的彙編代碼。 可以看到下面幾乎沒有什麼代碼,還有.rodata段的數據分佈。

QString returnAString() {

     return QStringLiteral ( "Hello" );

}

x84_64g++ -O2 -S -std=c++0x (GCC 4.7)編譯後

. text

     . globl   _Z13returnAStringv

     . type    _Z13returnAStringv, @function

_Z13returnAStringv:

     ; load the address of the QStringData into %rdx

     leaq    _ZZZ13returnAStringvENKUlvE_clEvE15qstring_literal(%rip), %rdx

     movq     %rdi, %rax

     ; copy the QStringData from %rdx to the QString return object

    ; allocated by the caller.  (the QString constructor has been inlined)

     movq     %rdx, (%rdi)

     ret

     . size    _Z13returnAStringv, .-_Z13returnAStringv

     . section     .rodata

     . align 32

     . type   _ZZZ13returnAStringvENKUlvE_clEvE15qstring_literal, @object

     . size    _ZZZ13returnAStringvENKUlvE_clEvE15qstring_literal, 40

_ZZZ13returnAStringvENKUlvE_clEvE15qstring_literal:

     . long    - 1    ; ref

     . long    5     ; size

     . long    0     ; alloc + capacityReserved

     . zero    4     ; padding

     . quad    24    ; offset

     . string "H"   ; the data. Each .string add a terminal ''

     . string "e"

     . string "l"

     . string "l"

     . string "o"

     . string ""

     . string ""

     . zero    4

 

結論

我希望讀完這篇博客的現在,你們能更好的理解什麼時候用和不用QStringLiteral。 
還有一個宏叫做QByteArrayLiteral,工作原理和QStringLiteral幾乎一模一樣但是創建的是QByteArray


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章