Про QStringLiteral

в 11:11, , рубрики: c++, c++11, QLatin1String, QStringLiteral, Qt Software, qt5

QStringLiteral — это новый макрос, введенный в Qt 5, для создания объектов QString из строковых литералов. (Строковые литералы — это строки внутри кавычек в исходном коде). В этой статье я объясню, что там внутри и как они реализованы.

Выводы

Разрешите начать с информации о том, когда нужно пользоваться макросом. Если есть необходимость инициализировать объект QString из строкового литерала в Qt5, то стоит делать так:

  • В большинстве случаев QStringLiteral(«foo») если он действительно будет преобразован в QString.
  • QLatin1String(«foo») если он будет использоваться в перегруженных 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(QSringData))
 
  inline ushort *data()
  { return reinterpret_cast<ushort *>(reinterpret_cast<char *>(this) + offset); }
};
 
// ...
 
class QString {
  QStringData *d;
public:
  // ... public API ...
};

Про QStringLiteral
offset — указатель на данные, относящиеся к QStringData. В Qt4 это был фактический указатель.

Данные строки хранятся в формате UTF-16, который использует по 2 байта на символ.

Литералы и преобразования

Строковые литералы — это строки, которые встречаются прямо в коде в кавычках. Приведем пример. (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.

То же самое происходит и в последней строке, где вызывается функция QString::replace(const QString &, const QString &). Для "%FileName%" создается новый объект QStringData.

Существует ли решение для избежания создания QStringData и копирования строки?

Да, одно из решений для избежания дорогостоящего создания временного объекта QString — это иметь перегруженные методы, которые принимают параметрами const char*.
У нас перегружен operator==

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

Перегруженным методам нет необходимости создавать новый объект QString для нашего литерала. Они могут использовать данные прямиком из char*.

Кодировка и QLatin1String

В Qt5, мы изменили кодировку строк по умолчанию с char* на UTF-8. Но многие алгоритмы намного медленнее с UTF-8 чем с обычным ASCII или latin1.

Таким образом можно использовать QLatin1String, который подобен тонкой обертке над char* и определяет кодировку. Перегруженные функции, принимающие QLatin1String и умеющие им оперировать, или сырые latin1 данные напрямую без преобразования.

Теперь наш первый пример будет выглядеть так:

o->setObjectName(QLatin1String("MyObject"));
if (action == QLatin1String("rename"))
    string.replace(QLatin1String("%FileName%"), filename);

Хорошая новость в том, что QString::replace и operator== перегружены для QLatin1String. Теперь они намного быстрее.

В случае с setObjectName мы избежали преобразования из UTF-8, но у нас все же осталось преобразование из QLatin1String в QString, которое повлекло создание QStringData в куче.

Встречайте QStringLiteral

Возможно ли избежать выделения памяти и копирования строкового литерала для случаев на подобии setObjectName? Да, это то, для чего создан QStringLiteral.

Макрос будет генерировать объект QStringData во время компиляции со всеми инициализированными полями, который так же будет размещаться в секции .rodata, поэтому его можно использовать между процессами.

Нам нужны две особенности языка, для этого:

  1. Возможность генерировать UTF-16 во время компиляции:
    В Windows можно использовать wide char L«String». В Unix мы можнм использовать новый C++11 Unicode литерал: u«String». (Поддерживается GCC 4.4 и clang.)
  2. Возможность создания статических данных из выражений.
    Мы хотим, что бы была возможность везде использовать QStringLiteral. Один из путей решения этой проблемы — вложить static QStringData внутрь лямбда выражений С++. (Поддерживается MSVC 2010 и GCC 4.5) (Мы так же используем GCC __extension__ ({ }) )

Реализация

Нам необходима POD структура, которая содержит как QStringData так и саму строку. Эта структура будет зависеть от того, как генерируется 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 2 байта  (условие немного упрощено)
   #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 необходим для правил расширения макроса
# 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__ ({ }) фокус вместо лямбды
// ... <пропущено> ...
# endif
#endif
 
#ifndef QStringLiteral
// нет лямбд, не GCC, или GCC в режиме C++98 с 4-байтным wchar_t
// возвращаем временный QString
// предполагается, что исходный код в кодировке UTF-8
# define QStringLiteral(str) QString::fromUtf8(str, sizeof(str) - 1)
#endif

Давайте немного упростим этот макрос и посмотрим, как он будет разворачиваться

o->setObjectName(QStringLiteral("MyObject"));
// будет разворачиваться в:
o->setObjectName(([]() {
        // Мы в лямбда выражении поэтому возвращаем QStaticString
 
        // Подсчитаем размера используя 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;
    }()) // Вызов лямбды
  );

Счетчик ссылок инициализирован в -1. Отрицательное значение никогда не увеличивается или уменьшается, потому что мы в области только для чтения.

Теперь ясно, почему так важно иметь смещение (qptrdiff) вместо указателя на строку (ushort*) как это было в Qt4. Совершенно невозможно вставить указатель в блок только для чтения, потому что указатель может быть перемещен во время загрузки. Это значит, что каждый раз для использования приложения или библиотеки, ОС необходимо перезаписать все адреса указателей используя таблицу перемещений.

Результаты

Ради забавы, давайте глянем на сгенерированный ассемблер очень простого вызова QStringLiteral. Мы видим, что кода практически нет, а данные находятся в .rodata секции.

Не следует забывать про накладные расходы в двоичном виде. Строки занимают в два раза больше памяти, так как они закодированы в UTF-16, а так же заголовок sizeof(QStringData) = 24. Эти накладные расходы и есть та причина, из-за которой есть смысл использовать QLatin1String, когда вызываемая функция перегружена.

QString returnAString() {
    return QStringLiteral("Hello");
}

Скомпилировано g++ -O2 -S -std=c++0x (GCC 4.7) на x86_64

    .text
    .globl  _Z13returnAStringv
    .type   _Z13returnAStringv, <hh user=function>
_Z13returnAStringv:
    ; загрузка адреса QStringData в %rdx
    leaq    _ZZZ13returnAStringvENKUlvE_clEvE15qstring_literal(%rip), %rdx
    movq    %rdi, %rax
    ; копирование QStringData из %rdx в возвращаемый объект QString
    ; выделеный вызывающим.  (конструктор QString был строеный)
    movq    %rdx, (%rdi)
    ret
    .size   _Z13returnAStringv, .-_Z13returnAStringv
    .section    .rodata
    .align 32
    .type   _ZZZ13returnAStringvENKUlvE_clEvE15qstring_literal, <hh user=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, который работает по тому же принципу, но создает QByteArray.

Автор: surik


  1. sergio:

    А почему нет ссылки на оригинальную статью (http://woboq.com/blog/qstringliteral.html)? Уже за перевод статьи себе и авторство присваивают, о как!

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js