天天看點

解釋QStringLiteral

轉載 原作者: Olivier Goffart http://woboq.com/blog/qstringliteral.html 譯者:zzjin http://www.tuicool.com/articles/6nUrIr

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 ...

};

解釋QStringLiteral

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時使用的實作方法。

#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 =

{ {

-

1

,

Size,

,

,

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

; 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。

繼續閱讀