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.

Qt QStringLiteral的更多相关文章

  1. qt中ui的 使用介绍

    1.什么是ui?ui通常是用Qt 设计师设计出来的界面文件的后缀.通常情况下ui是一个指向这个界面类的指针.ui-> 一般就是用来访问这个界面类里面的控件.例如你的ui文件里有一个叫okButt ...

  2. QT笔记之解决QT5.2.0和VS2012中文乱码 以及在Qt Creator中文报错

    转载:http://bbs.csdn.net/topics/390750169 VS2012 中文乱码 1.方法一: 包含头文件 #include <QTextCodec> ....... ...

  3. Qt中文乱码解决思路

    最近项目中遇到不少的Qt中文乱码的问题,主要原因是客户的需求比较多,Qt版本有用4的版本的也有用5的版本,并且还有windows与linux跨平台的需求.经常出现个问题是windows的解决了,源代码 ...

  4. 用Qt写软件系列五:一个安全防护软件的制作(1)

    引言 又有许久没有更新了.Qt,我心爱的Qt,为了找工作不得不抛弃一段时间,业余时间来学一学了.本来计划要写一系列关于Qt组件美化的博文,但是写了几篇之后就没坚持下去了.技术上倒是问题不大,主要是时间 ...

  5. Qt之界面实现技巧

    一.主界面 1.窗口 最小化 最小化 关闭按钮 显示状态自定义 setWindowFlags(Qt::CustomireWindowHint); setWindowFlags(Qt::WindowCl ...

  6. Qt之QAbstractItemView视图项拖拽(二)

    一.需求说明 上一篇文章Qt之QAbstractItemView视图项拖拽(一)讲述了实现QAbstractItemView视图项拖拽的一种方式,是基于QDrag实现的,这个类是qt自己封装好了的,所 ...

  7. Qt Style Sheet实践(四):行文本编辑框QLineEdit及自动补全

    导读 行文本输入框在用于界面的文本输入,在WEB登录表单中应用广泛.一般行文本编辑框可定制性较高,既可以当作密码输入框,又可以作为文本过滤器.QLineEdit本身使用方法也很简单,无需过多的设置就能 ...

  8. Qt Style Sheet实践(二):组合框QComboBox的定制

    导读 组合框是一个重要且应用广泛的组件,一般由两个子组件组成:文本下拉单部分和按钮部分.在许多既需要用户选择.又需要用户手动输入的应用场景下,组合框能够很好的满足我们的需求.如我们经常使用的聊天软件Q ...

  9. Qt Style Sheet实践(一):按钮及关联菜单

    导读 正如web前端开发中CSS(Cascade Style Sheet)的作用一样,Qt开发中也可以使用修改版的QSS将逻辑业务和用户界面进行隔离.这样,美工设计人员和逻辑实现者可以各司其职而不受干 ...

随机推荐

  1. socket的双重属性

    1)api属性: 2)通信链路的端点属性.

  2. luogu P3381【模板】最小费用最大流

    嘟嘟嘟 没错,我开始学费用流了! 做法也是比较朴素的\(spfa\). 就是每一次以费用为权值跑一遍\(spfa\)找到一条最短路,然后把这条道全流满,并把这一次的流量和费用累加到答案上.因此我们需要 ...

  3. Odoo中的onchange

    转载请注明原文地址:https://www.cnblogs.com/cnodoo/p/9280723.html  [onchange=前端js函数,可以实现前端实时更新以及修改验证] onchange ...

  4. 贪心——HDU-5969 最大的位或

    HDU-5969:http://acm.hdu.edu.cn/showproblem.php?pid=5969 一开始也是分了类,觉得要两种情况,l 与 r 位数相同与不同的情况,仔细想一下,可以一起 ...

  5. sqoop导数据到hive报错

    [root@hadoop1 conf]# sqoop import --connect jdbc:mysql://192.168.122.15:3306/company --username sqoo ...

  6. 【SP2713 GSS4 - Can you answer these queries IV】 题解

    题目链接:https://www.luogu.org/problemnew/show/SP2713 真暴力啊. 开方你开就是了,开上6次就都没了. #include <cmath> #in ...

  7. JNI由浅入深_4_JNI基础知识详解

    Java Native Interface (JNI)标准是java平台的一部分,它允许Java代码和其他语言写的代码进行交互.JNI 是本地编程接口,它使得在 Java 虚拟机 (VM) 内部运行的 ...

  8. Linux 嵌入式 开发环境 交叉编译安装

    1.安装 Ubuntu 系统 安装完毕,系统 提示 重启,这个时候 请拔掉U盘,进行重启 OK. 2.安装 NFS 服务 3.安装 openssh服务 4.开启openSSH服务 5.就可以使用 Wi ...

  9. docker 容器不能访问宿主端口原因

    因为数据包到了eth0的 上的iptables 表,首先匹配PREROUTING 链,这个拒绝了来自docker0的流量,从而跳到input链,input没有放开服务端口,所以容器访问宿主端口失败;但 ...

  10. EF结合SqlBulkCopy实现高效的批量数据插入 |EF插件EntityFramework.Extended实现批量更新和删除

    原文链接:http://blog.csdn.net/fanbin168/article/details/51485969   批量插入 (17597条数据批量插入耗时1.7秒)   using Sys ...