一杯清酒邀明月
天下本无事,庸人扰之而烦耳。

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。

posted on 2020-11-09 15:43  一杯清酒邀明月  阅读(1812)  评论(0编辑  收藏  举报