Qt QStringLiteral


zz 解釋QStringLiteral

 

原文發表於woboq網站  QStringLiteral explained

轉載 原作者: Olivier Goffart 譯者:zzjin

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

提要

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

  • 大多數情況:使用QStringLiteral(“某字符串”) --如果它最終轉會換成QString的話
  • 使用QLatin1String(“某字符串”) --如果使用的函數有支持QLatin1String的重載(比如operator==, operator+, startWith, replace等)的話

我把這段話放在最開始是為了那些不怎么想了解其具體技術細節的人着想。

繼續閱讀你將了解QStringLiteral是如何工作的。

回顧QString的工作方式

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

// 為了此博客的目標做了簡化
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,string和filename都是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::replace與operator==操作有了針對QLatin1String的重載函數,所以現在快很多。

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

介紹 QStringLiteral

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

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

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

  1. 在編譯的時候生成UTF-16格式字符串的可能性 
    Win環境下我們可以使用寬字符 L"String"。 Unix環境下我們使用新的C++11 Unicode字符串: u"String"。( GCC 4.4和clang支持。)
  2. 從表達式中創建靜態數據的能力 
    我們希望能把QStringLiteral放在代碼的任何地方。一種實現方法就是把一個靜態的QStringData放入一個C++11 lambda 表達式。(MSVC 2010和GCC 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,或者GCC為C++98模式,使用4字節wchar_t
// fallback, 返回一個臨時的QString
// 默認認為源碼為utf-8編碼
# define QStringLiteral(str) QString::fromUtf8(str, sizeof(str) - 1)
#endif

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

o->setObjectName(  QStringLiteral  (  "MyObject"  ));
// 將展開為:
o->setObjectName(([]() {
          // 我們在一個返回QStaticString的lambda表達式中
          // 使用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_64用g++ -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。

更新:  你也可以參看 Qt5提供的其他好功能 。

Tags: QStringLiteral , Qt5 
This entry was posted on Tuesday, August 21st, 2012 at 3:52 AM and is filed under Qt技術 . You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response , or trackback from your own site.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM