Программирование с Qt: Часть 2. Типы, варианты, ссылки и разделение данных

 

Программирование с Qt: Часть 2. Типы, варианты, ссылки и разделение данных

 

 

Источник: IBM developerWorks Россия

Автор: Алексей Бешенов, технический писатель, независимый специалист

Уровень сложности: средний

08.10.2009

В этой статье:

Qt – кроссплатформенный инструментарий для разработки прикладного программного обеспечения, широко используемый при создании графических пользовательских интерфейсов. В первом материале цикла мы рассмотрели основные инструменты разработчика, а также используемую в Qt объектную модель. Теперь перейдем к вопросам, касающиеся типов, вариантов, подсчета ссылок и разделения данных, поскольку без этих базовых сведений работать с инструментарием невозможно.

1. typedef в Qt

В Qt имеются директивы typedef, облегчающие кроссплатформенную разработку и написание имен типов (таблица 1).

Таблица 1. typedef в Qt

Для переносимости кода
qint8 8-битное знаковое целое
quint8 8-битное беззнаковое целое
qint16 16-битное знаковое целое
quint16 16-битное беззнаковое целое
qint32 32-битное знаковое целое
quint32 32-битное беззнаковое целое
qint64 64-битное знаковое целое
quint64 64-битное беззнаковое целое
quintptr Беззнаковый указатель (32 или 64 бита)
qptrdiff Знаковый указатель (32 или 64 бита)
qreal Везде double; float только для архитектуры ARM
Для краткой записи типов
uchar unsigned char
uint unsigned int
ulong unsigned long
ushort unsigned short
qlonglong long long int или __int64
qulonglong unsigned long long int или unsigned __int64

В большинстве систем qint8, qint16, qint8 соответствуют signed char, signed short и signed int.

Тип qint64 обычно соответствует long long int. В компиляторах, не использующих расширения C99 (например, msvc), этот тип может обозначаться как __int64 (также используются __int8, __int16, __int32).

Для повышения переносимости кода 64-битные литералы можно записывать через макросы Q_INT64_C и

Q_UINT64_C:
qint64  m =  Q_INT64_C(9223372036854775807);
quint64 n = Q_UINT64_C(18446744073709551615);

Это то же самое, что 9223372036854775807LL и 18446744073709551615ULL.

Для указателей гарантируется

sizeof(void*) == sizeof(quintptr) 
	&& sizeof(void*) == sizeof(qptrdiff)

2. Регистрация типов

Для эффективной работы с данными (в частности, в контейнерах) определенному коду требуется информация о типах. В Qt ее можно указать при помощи Q_DECLARE_TYPEINFO:

Q_DECLARE_TYPEINFO (Type, Flags)

где Type – тип, Flags – флаг:

  • Q_PRIMITIVE_TYPE – примитив без конструктора и деструктора;
  • Q_MOVABLE_TYPE – тип с конструктором и/или деструктором, который можно перемещать в памяти при помощи memcpy();
  • Q_COMPLEX_TYPE – сложный тип с конструктором и/или деструктором, который нельзя перемещать в памяти.

Примеры примитивных типов: bool, double, qint64.

Многие классы Qt относятся к Q_MOVABLE_TYPE: QBitArray, QChar, QDate, и т.д. Например, QPoint:

class QPoint
{
public:
  QPoint();
  QPoint(int xpos, int ypos);

  // ...

private:
  int xp;
  int yp;
};

По умолчанию (если в коде нет Q_DECLARE_TYPEINFO) подразумевается Q_COMPLEX_TYPE.

Например, в контейнере Qvector<T> при увеличении вектора используется realloc(), если тип T может перемещаться в памяти.

При вставке в середину вектора элементы за точкой вставки нужно сдвинуть на одну позицию вперед. Если T можно перемещать в памяти, применяется memmove(). Иначе требуется присваивание (с оператором =).

Также, если указано Q_PRIMITIVE_TYPE, токонтейнер не будет вызывать конструктор и деструктор.

Контейнеры мы подробно рассмотрим в отдельной статье.

В начало

3. Варианты

Объединение (union) в C и C++ – это структура, все члены которой располагаются по одному и тому же адресу. Таким образом, под объединение отводится столько места в памяти, сколько занимает его наибольший член. Никакого контроля за тем, что находится в объединении не ведется, и его членами не могут быть объекты класса со специальным конструктором, деструктором, либо операцией копирования.

Варианты, реализованные через класс QVariant, могут хранить одновременно одно значение определенного типа, но являются более удобными и безопасными, а также не предполагают жестких ограничений, как объединения.

Все возможные типы определяет перечисление QVariant::Type:

class QVariant
{
  enum Type {
  // Неправильный тип:
    Invalid     = 0,

  // Классы из модуля QtCore и примитивные типы
    BitArray    = 13,  // QBitArray
    Bool        = 1,   // bool
    ByteArray   = 12,  // QByteArray
    Char        = 7,   // QChar
    Date        = 14,  // QDate
    DateTime    = 16,  // QDateTime
    Double      = 6,   // double
    Hash        = 28,  // QHash<QString, QVariant>
    Int         = 2,   // int
    Line        = 23,  // QLine
    LineF       = 24,  // QLineF
    List        = 9,   // QList<QVariant>
    Locale      = 18,  // QLocale
    LongLong    = 4,   // qlonglong
    Map         = 8,   // QMap<QString, QVariant>
    Point       = 25,  // QPoint
    PointF      = 26,  // QPointF
    Rect        = 19,  // QRect
    RectF       = 20,  // QRectF
    RegExp      = 27,  // QRegExp
    Size        = 21,  // QSize
    SizeF       = 22,  // QSizeF
    String      = 10,  // QString
    StringList  = 11,  // QStringList
    Time        = 15,  // QTime
    UInt        = 3,   // uint
    ULongLong   = 5,   // qulonglong
    Url         = 17,  // QUrl

  // Классы из модуля QtGui
    Bitmap      = 73,  // QBitmap
    Brush       = 66,  // QBrush
    Color       = 67,  // QColor
    Cursor      = 74,  // QCursor
    Font        = 64,  // QFont
    Icon        = 69,  // QIcon
    Image       = 70,  // QImage
    KeySequence = 76,  // QKeySequence
    Matrix      = 80,  // QMatrix
    Transform   = 81,  // QTransform
    Palette     = 68,  // QPalette
    Pen         = 77,  // QPen
    Pixmap      = 65,  // QPixmap
    Polygon     = 71,  // QPolygon
    Region      = 72,  // QRegion
    SizePolicy  = 75,  // QSizePolicy
    TextFormat  = 79,  // QTextFormat
    TextLength  = 78,  // QTextLength

  // Базовое значение для пользовательских типов:
    UserType = 127
  };
  // ...
};

Для контейнеров имеются удобные короткие синонимы:

typedef QList<QVariant>           QVariantList;
typedef QMap<QString, QVariant>   QVariantMap;
typedef QHash<QString, QVariant>  QVariantHash;

3.1. Создание вариантов

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

Конструктор по умолчанию QVariant создает неправильный вариант. Если указать в конструкторе QVariant::Type, то будет создан нулевой вариант данного типа.

Нулевой и неправильный варианты можно определить при помощи методов isNull() и isValid().

bool QVariant::isNull() const;
bool QVariant::isValid() const;

Пример:

QVariant().isValid();              // false
QVariant(QVariant::Int).isNull();  // true
QVariant(0).isNull();              // false

Вторым аргументом передается указатель на объект, которым инициализируется вариант.

QVariant::QVariant (Type type);
QVariant::QVariant (int typeOrUserType, const void* copy);
QVariant::QVariant (const QVariant& p);

Пример:

QVariant var0(QVariant::Point);  // нулевой вариант

QPoint p(23,23);
QVariant var1(QVariant::Point, &p);

В конструкторе QVariant можно передать ссылку на объект любого из указанных выше классов из модуля QtCore, либо примитивный тип (const QBitArray&, bool, const QByteArray&, ...):

QVariant var2(3.1415926);

QVariant var3(true);

QList<QVariant> list;
list << QVariant(1) << QVariant(2) << QVariant(3);
// (О контейнерах мы расскажем отдельно)
QVariant var4(list);

QtGui – это уже другой модуль, и для аргументов его классов (QBitmap, QBrush, QColor, ...) схожих конструкторов нет.

3.2. Пользовательские типы в вариантах

Для хранения пользовательских типов используется класс QMetaType. Размещенные в QVariant типы должны регистрироваться через макрос Q_DECLARE_METATYPE:

namespace Foo
{
  struct Bar
  {
    int  baz;
    char quux;
  };
}
Q_DECLARE_METATYPE(Foo::Bar)

Обратите внимание, что макрос размещен вне пространства имен, а тип указан полностью (Foo::Bar). Теперь Foo::Bar можно использовать в вариантах:

Foo::Bar bar;
bar.baz  = 23;
bar.quux = 'a';

QVariant var;
var.setValue<Foo::Bar>(bar);

// ...

Foo::Bar bar2 = var.value<Foo::Bar>();
var.typeName();  // Foo::Bar

Желательно, чтобы такая регистрация типа происходила сразу после его объявления. Тогда не нужно следить, чтобы он использовался в вариантах только после Q_DECLARE_METATYPE.

Тип может быть любым классом или структурой с конструктором по умолчанию, конструктором копирования и деструктором, имеющими модификатор доступа public.

Зарегистрированным типам присваиваются целочисленные идентификаторы, начиная с 256. Идентификатор типа T можно получить при помощи функций

template<typename T> int qMetaTypeId();
static int QMetaType::type (const char* typeName);

qMetaTypeId() работает на стадии компиляции, а QMetaType::type() – на стадии выполнения, поэтому допускает несуществующие типы, для которых просто возвращает 0.

В перечислении QMetaType::Type определены идентификаторы для встроенных типов:

class QMetaType
{
  enum Type {
    Bool         = 1,    // bool
    Char         = 131,  // char
    Double       = 6,    // double
    Float        = 135,  // float
    Int          = 2,    // int
    Long         = 129,  // long
    LongLong     = 4,    // LongLong
    QBitArray    = 13,   // QBitArray
    QBitmap      = 73,   // QBitmap
    QBrush       = 66,   // QBrush
    QByteArray   = 12,   // QByteArray
    QChar        = 7,    // QChar
    QColor       = 67,   // QColor
    QColorGroup  = 63,   // QColorGroup
    QCursor      = 74,   // QCursor
    QDate        = 14,   // QDate
    QDateTime    = 16,   // QDateTime
    QFont        = 64,   // QFont
    QIcon        = 69,   // QIcon
    QImage       = 70,   // QImage
    QKeySequence = 76,   // QKeySequence
    QLine        = 23,   // QLine
    QLineF       = 24,   // QLineF
    QLocale      = 18,   // QLocale
    QMatrix      = 80,   // QMatrix
    QObjectStar  = 136,  // QObject*
    QPalette     = 68,   // QPalette
    QPen         = 77,   // QPen
    QPixmap      = 65,   // QPixmap
    QPoint       = 25,   // QPoint
    QPointF      = 26,   // QPointF
    QPolygon     = 71,   // QPolygon
    QRect        = 19,   // QRect
    QRectF       = 20,   // QRectF
    QRegExp      = 27,   // QRegExp
    QRegion      = 72,   // QRegion
    QSize        = 21,   // QSize
    QSizeF       = 22,   // QSizeF
    QSizePolicy  = 75,   // QSizePolicy
    QString      = 10,   // QString
    QStringList  = 11,   // QStringList
    QTextFormat  = 79,   // QTextFormat
    QTextLength  = 78,   // QTextLength
    QTime        = 15,   // QTime
    QTransform   = 81,   // QTransform
    QUrl         = 17,   // QUrl
    QVariantHash = 28,   // QVariantHash
    QVariantList = 9,    // QVariantList
    QVariantMap  = 8,    // QVariantMap
    QWidgetStar  = 137,  // QWidget*
    Short        = 130,  // short
    UChar        = 134,  // unsigned char
    UInt         = 3,    // unsigned int
    ULong        = 132,  // unsigned long
    ULongLong    = 5,    // ULongLong
    UShort       = 133,  // unsigned short
    Void         = 0,    // void
    VoidStar     = 128,  // void*

  // Базовое значение для пользовательских типов:
    User         = 256
  };
  // ...
};

Для использования своих типов в вариантах достаточно макроса Q_DECLARE_METATYPE.

В дополнение к регистрации типа через макрос, для его использования с шаблонами функций (такими как QVariant::setValue<T>() и QVariant::value<T>()), имеется регистрация на стадии исполнения через

template<typename T> int qRegisterMetaType();

qRegisterMetaType возвращает идентификатор, используемый QMetaType. Эта функция должна вызываться после того, как тип T зарегистрирован при помощи Q_DECLARE_METATYPE.

После этого становятся доступными функции, действующие на стадии исполнения.

При помощи QMetaType можно динамически вызывать конструкторы и деструкторы:

int id = qRegisterMetaType<Foo::Bar>();

Foo::Bar* p_bar = (Foo::Bar*)QMetaType::construct(id, "Foo::Bar");

if (p_bar != (void*)(-1))
{
  p_bar->baz = 23;
  p_bar->quux = 'a';
  // ...
  qDebug() << p_bar->baz;
  qDebug() << p_bar->quux;
}
else
{
  qFatal("Can't construct Foo::Bar");
}

QMetaType::destroy(id, p_bar);

3.3. Операции над вариантами

Чтобы узнать тип, можно использовать следующие методы:

Type QVariant::type() const;
int QVariant::userType() const;
const char* QVariant::typeName() const;
static const char* QVariant::typeToName (Type type);

Для пользовательских типов userType() возвращает зарегистрированный идентификатор, а type() – значение QVariant::UserType. Для типов из QVariant разницы между userType() и type() нет.

Прежде чем извлекать из варианта значение, стоит проверить, допустима ли данная операция:

bool QVariant::canConvert (Type type) const;
template<typename T> bool QVariant::canConvert() const;

Для вариантов из предыдущего примера:

var1.canConvert(QVariant::String); // false
var2.canConvert(QVariant::Int);    // true 
var3.canConvert(QVariant::Int);    // true
var3.canConvert(QVariant::Date);   // false

То же самое через шаблон метода:

var1.canConvert<QString>(); // false
var2.canConvert<int>();     // true
var3.canConvert<int>();     // true
var3.canConvert<QDate>();   // false

В таблице 2 перечислены возможные преобразования.

Таблица 2. Автоматическое преобразование типов в QVariant

Тип Автоматически преобразуется в
Bool Char, Double, Int, LongLong, String, UInt, ULongLong
ByteArray Double, Int, LongLong, String, UInt, ULongLong
Char Bool, Int, UInt, LongLong, ULongLong
Color String
Date DateTime, String
DateTime Date, String, Time
Double Bool, Int, LongLong, String, UInt, ULongLong
Font String
Int Bool, Char, Double, LongLong, String, UInt, ULongLong
KeySequence Int, String
List StringList
LongLong Bool, ByteArray, Char, Double, Int, String, UInt, ULongLong
Point PointF
Rect RectF
String Bool, ByteArray, Char, Color, Date, DateTime, Double, Font, Int, KeySequence, LongLong, StringList, Time, UInt, ULongLong
StringList List, String
Time String
UInt Bool, Char, Double, Int, LongLong, String, ULongLong
ULongLong Bool, Char, Double, Int, LongLong, String, UInt

Стоит отметить, что проверяется лишь принципиальная возможность преобразования типов. Например, строка может быть преобразована в целое число, но не каждая строка описывает целое число.

Значения получают следующими методами:

QBitArray   QVariant::toBitArray()   const;
bool        QVariant::toBool()       const;
QByteArray  QVariant::toByteArray()  const;
QChar       QVariant::toChar()       const;
QDate       QVariant::toDate()       const;
QDateTime   QVariant::toDateTime()   const;
QLine       QVariant::toLine()       const;
QLineF      QVariant::toLineF()      const;
QLocale     QVariant::toLocale()     const;
QPoint      QVariant::toPoint()      const;
QPointF     QVariant::toPointF()     const;
QRect       QVariant::toRect()       const;
QRectF      QVariant::toRectF()      const;
QRegExp     QVariant::toRegExp()     const;
QSize       QVariant::toSize()       const;
QSizeF      QVariant::toSizeF()      const;
QString     QVariant::toString()     const;
QStringList QVariant::toStringList() const;
QTime       QVariant::toTime()       const;
QUrl        QVariant::toUrl()        const;

QList<QVariant>          QVariant::toList() const;
QHash<QString, QVariant> QVariant::toHash() const;
QMap<QString, QVariant>  QVariant::toMap()  const;

double     QVariant::toDouble    (bool* ok = 0) const;
int        QVariant::toInt       (bool* ok = 0) const;
qlonglong  QVariant::toLongLong  (bool* ok = 0) const;
uint       QVariant::toUInt      (bool* ok = 0) const;
qulonglong QVariant::toULongLong (bool* ok = 0) const;

Методам преобразования в числа можно передать указатель на булеву переменную чтобы проверить, удачно ли преобразовано значение.

var1.toPoint();  // QPoint(23,23)
    
var2.toDouble(); // 3.1415926
var2.toString(); // "3.1415926"
var2.toInt();    // 3
    
var3.toBool();   // true

bool test;
QVariant("foo").toInt(&test); // 0
test;                         // false

Для типов из QtGui используется метод value():

template<typename T> T QVariant::value() const;

Перепишем приведенный выше пример:

var1.value<QPoint>();  // QPoint(23,23)

var2.value<double>();  // 3.1415926
var2.value<QString>(); // "3.1415926"
var2.value<int>();     // 3

var3.value<bool>();    // true

В случае неудачи возвращается значение по умолчанию (для чисел это 0, для классов вызывается конструктор по умолчанию).

Аналогично работает qvariant_cast:

template<typename T> T qvariant_cast (const QVariant& value);

Для преобразования без создания нового варианта существует метод convert():

bool QVariant::convert (Type type);

Если преобразование завершено успешно, тип меняется и возвращается true. Иначе возвращается false, а вариант становится неправильным:

var2.convert(QVariant::Int);  // true
var2.typeName();              // int
var2.value<int>();            // 3

var2.convert(QVariant::Date);  // false
var2.isValid();                // false

Для присвоения значения используйте setValue():

template<typename T> void QVariant::setValue (const T& value);
template<typename T> static void QVariant::fromValue (const T& value);

В setValue() значение копируется и сохраняется внутри варианта. Метод fromValue() делает то же самое с той лишь разницей, что он статический, и возвращает новый вариант.

Как и в STL, в Qt имеются шаблоны контейнеров. При помощи вариантов можно использовать рекурсивные структуры данных (листинг 1.1).

Листинг 1.1. представление вложенных списков через QVariant

void dumpList (const QVariantList& list, QTextStream& out)
{
  out << "(";
  // (На вводе/выводе мы еще остановимся в другой статье)

  QVariantList::const_iterator i = list.constBegin();
  // (На итераторах и контейнерах — тоже)

  while (i != list.constEnd())
  {
    if ((*i).type() == QVariant::List)
      dumpList ((*i).toList(), out);
    else
      out << (*i).toString();

    if (++i != list.constEnd())
      out << " ";
  }

  out << ")";
}

int main()
{
  QVariantList list;
  list << QVariant("foo");
  QVariantList sublist;
  sublist << QVariant("bar") << QVariant("baz") << QVariant(23) << QVariant(true);
  list << QVariant(sublist) << QVariant("quux") << QVariant(QVariantList());

  QTextStream cout(stdout);
  dumpList (list, cout);  // (foo (bar baz 23 true) quux ())

  return 0;
В начало

4. Защищенные указатели

Когда объект, на который ссылается указатель, разрушается, указатель становится «повисшим», т.е. содержащим адрес, по которому уже нет объекта. Это часто служит источником ошибок.

В Qt имеется защищенный указатель QPointer<T>, который автоматически принимает значение 0 при разрушении связанного с ним объекта. Объект должен наследовать от QObject.

В остальном QPointer<T> работает как T*. Он автоматически приводится к T*, перегружаются операторы * и -> для разыменования, а также присваивание =.

Конструкторы QPointer<T>:

QPointer();  // нулевой
QPointer (T *p);
QPointer (const QPointer<T> &p); // копия

Методы:

T* data() const;      // указатель
bool isNull() const;  // true, если 0

Операторы (+, -, ++ и --), используемые для арифметики указателей, не перегружаются.

Пример:

#include <QPointer>

QPointer<Foo> x = new Foo;

// ...

if (x)
{
  x->bar();  // не выполняется, если x был удален
}

Разумеется, защищенные указатели предполагают дополнительные накладные расходы по сравнению с обычными. Оповещение об уничтожении объекта реализовано через подключение сигнала к слоту. Но обычно этим можно пренебречь.

В начало

5. Подсчет ссылок

Классы QSharedPointer и QWeakPointer реализуют подсчет ссылок.

5.1. Жесткие ссылки

Обычный указатель типа T* можно «обернуть» в объект QSharedPointer<T>, который послужит жесткой ссылкой. Указатель будет удален в тот момент, когда последняя ссылка выйдет за область действия, и для нее будет вызван деструктор QsharedPointer<T>. Такая ссылка конструируется из обычного указателя:

QSharedPointer<Foo> sp0(new Foo);

Если указатель на объект передан конструктору QSharedPointer<T>, то объект нельзя самостоятельно уничтожать либо создавать из него еще один QSharedPointer<T>. Новые жесткие ссылки создаются через копирование и присваивание:

Foo* foo = new Foo;
QSharedPointer<Foo> sp0(foo);

QSharedPointer<Foo> sp1(sp0);  // правильно

QSharedPointer<Foo> sp2;
sp2 = sp0;                     // правильно

QSharedPointer<Foo> sp3(foo);  // ошибка!

В конструкторе можно указать произвольную функцию (функтор) для удаления:

QSharedPointer::QSharedPointer (T *ptr, Deleter deleter);

Как и для других умных указателей, здесь перегружены операторы разыменования (*), доступа к членам (->), а также сравнения (!= и ==). QSharedPointer<T> работает как обычный указатель T*.

QSharedPointer<Foo> sp0(new Foo);
QSharedPointer<Foo> sp1(sp0);

(*sp0).bar = 23;

sp0->baz();

if (sp0 == sp1)
{
  // ...
}

if (sp0.isNull())  // выполняется, если указатель нулевой
{
  // ...
}

if (sp0 || !sp1)  // используется приведение к bool
{                 // и оператор !
  // ...
}

Сам указатель можно получить при помощи метода

T* QSharedPointer::data() const;

5.2. Слабые ссылки

Для слабых ссылок QWeakPointer<T> не задано разыменование. Они применяются только для того чтобы проверить, не был ли указатель удален. Аналогично, имеется метод isNull(), приведение к bool и оператор !. Слабые ссылки можно копировать, присваивать и сравнивать.

Допускается преобразование жесткой ссылки в слабую и наоборот:

QWeakPointer<T>   QSharedPointer::toWeakRef() const;
QSharedPointer<T> QWeakPointer::toStrongRef() const;

При этом, слабые ссылки создаются только из жестких, как в случае

QWeakPointer<Foo> wp(sp0);

5.3. Приведение

Для приведения типов предусмотрены вспомогательные функции. Если указатель на T0 нужно привести к указателю на T1 при помощи static_cast, const_cast, dynamic_cast, то используйте соответственно:

QSharedPointer<T1> qSharedPointerCast (const QSharedPointer<T0>& other);
QSharedPointer<T1> qSharedPointerCast (const QWeakPointer<T0>& other);

QSharedPointer<T1> qSharedPointerConstCast (const QSharedPointer<T0>& other);
QSharedPointer<T1> qSharedPointerConstCast (const QWeakPointer<T0>& other);

QSharedPointer<T1> qSharedPointerDynamicCast (const QSharedPointer<T0>& other);
QSharedPointer<T1> qSharedPointerDynamicCast (const QWeakPointer<T0>& other);

Эти функции принимают жесткую или слабую ссылку и возвращают жесткую. Преобразование из слабой ссылки в слабую при помощи static_cast осуществляет

QWeakPointer<T1> qWeakPointerCast (const QWeakPointer<T0>& other);

Если преобразование dynamic_cast не удается, то возвращается ссылка, соответствующая нулевому указателю.

В начало

6. Разделение данных

Объекты классов с разделяемыми данными без дополнительных накладок могут быть переданы по значению. При этом передаются только указатели на данные. Ведется подсчет ссылок, и данные удаляются, как только счетчик принимает значение 0.

При неявном разделении данных передаваемые указатели служат неглубокими копиями, а сами данные дублируются только при записи изменений (создаются глубокие копии). Присваивание создает неглубокую копию.

Явное разделение данных означает, что данные не копируются, а изменения затрагивают все объекты.

Иногда это называют семантикой значений и семантикой указателей.

6.1. Неявное разделение данных в Qt

В Qt данные неявно разделяются:

  • вариантами QVariant;
  • массивами битов QBitArray;
  • массивами байтов QByteArray и строками QString;
  • регулярными выражениями QRegExp;
  • контейнерами: QHash, QLinkedList, QList, QMap, QMultiHash, QMultiMap, QQueue, QSet, QStack, QVector;
  • классами, наследующими от контейнеров: QPolygon, QPolygonF, QStringList;
  • кэшами QCache;
  • URL QUrl;
  • QLocale;
  • классами для работы с файловой системой: QDir, QFileInfo;
  • классами для работы со шрифтами: QFont, QFontInfo, QFontMetrics, QFontMetricsF;
  • классами для работы с SQL: QSqlField, QSqlQuery, QSqlRecord;
  • различными классами QtGui: QBitmap, QBrush, QCursor, QGLColormap, QGradient, QIcon, QImage, QKeySequence, QPainterPath, QPalette, QPen, QPicture, QPixmap, QRegion, QTextBoundaryFinder, QTextCursor, QTextDocumentFragment, QTextFormat, QX11Info.

Неявно разделяемые данные могут копироваться между потоками.

6.2. Создание собственных классов с разделением данных

Для создания собственных классов с разделением данных используется QSharedData. Пусть нам нужно создать класс Book, представляющий информацию о книге, чтобы разделялись данные об авторе, заглавии, годе издания и номере ISBN. Создадим вспомогательный класс BookData с соответствующими полями, наследующий от QSharedData (листинг 2.1).

Листинг 2.1. Класс BookData, хранящий разделяемые данные для Book

#include <QSharedData>
#include <QString>

class BookData : public QSharedData
{
public:
  BookData() : year(0)
  {
    author.clear();
    title.clear();
    isbn.clear();
  }

  BookData (const BookData& other) :
    QSharedData(other),
    author(other.author),
    title(other.title),
    year(other.year),
    isbn(other.isbn) {}

  ~BookData() {}

  QString author;
  QString title;
  ushort year;
  QString isbn;
};

Далее мы можем использовать указатель на данные QSharedDataPointer<BookData> (листинг 2.2).

Листинг 2.2. Класс Book с разделяемыми данными внутри BookData

#include <QString>
#include <QSharedDataPointer>

#include "bookdata.h"

class Book
{
public:
  Book() { d = new BookData; }

  Book (QString author, QString title, ushort year, QString isbn)
  {
    d = new BookData;
    setAuthor (author);
    setTitle (title);
    setYear (year);
    setIsbn (isbn);
  }

  Book (const Book& other) : d (other.d) {}

  QString author() const { return d->author; }
  void setAuthor(QString author) { d->author = author; }

  QString title() const { return d->title; }
  void setTitle(QString title) { d->title = title; }

  ushort year() const { return d->year; }
  void setYear(ushort year) { d->year = year; }

  QString isbn() const { return d->isbn; }
  void setIsbn(QString isbn) { d->isbn = isbn; }

private:
  QSharedDataPointer<BookData> d;
};

BookData скрывается от пользователя, и в программный интерфейс входит только класс Book.

Разделение данных будет работать следующим образом (листинг 2.3).

Листинг 2.3. Пример работы с Book при неявном разделении данных

Book cpppl97 ("Bjarne Stroustrup", "The C++ Programming Language", 1997, "0201889544");

Book cpppl00;

cpppl00 = cpppl97;  // неглубокая копия

cpppl00.setYear(2000);  // теперь требуется глубокая копия
cpppl00.setIsbn("0201700735");

qDebug() << cpppl97.isbn();  // "0201889544"
qDebug() << cpppl00.isbn();  // "0201700735"

Если требуется явное разделение памяти, то QSharedDataPointer<BookData> просто заменяется на QExplicitlySharedDataPointer<BookData>. Получаем другую логику работы (листинг 2.4).

Листинг 2.4. Пример работы с Book при явном разделении данных

cpppl00 = cpppl97;  // изменения затронут и данные cpppl97

cpppl00.setYear(2000);
cpppl00.setIsbn("0201700735");

qDebug() << cpppl97.isbn();  // "0201700735"
qDebug() << cpppl00.isbn();  // "0201700735"

QSharedData добавляет к данным счетчик ссылок (с атомарными операциями, чтобы обеспечить безопасную многопоточность).

QSharedDataPointer<T> предоставляет метод detach(), который создает глубокую копию данных, если значение счетчика ссылок больше 1.

Оператор -> перегружен таким образом, что возвращает указатель на данные. Если требуется T*, то вызывается detach(); если требуется const T*, то detach() не вызывается:

T* QSharedDataPointer::operator-> ();
const T* QSharedDataPointer::operator-> () const;

То же самое делает метод data(), возвращающий T* или const T* (с detach() или без); constData() возвращает const T*.

Аналогично перегружен оператор разыменования (унарная *) и приведение к указателю на данные. Есть const-версия без вызова detach() и не-const-версия с вызовом detach(). QSharedDataPointer<T> работает как обычный указатель T*, и помимо разыменования позволяет сравнение через == и !=.

Конструктор копирования и оператор присваивания увеличивают счетчик ссылок для новых данных. В ходе присваивания и уничтожения объекта счетчик ссылок у старых данных уменьшается.

QExplicitlySharedDataPointer<T> тоже предоставляет метод detach(), но не вызывает его при передаче не-const данных (в отличие от QSharedDataPointer<T>). При необходимости detach() вызывается вручную.

Если в QExplicitlySharedDataPointer<T> часто возникает необходимость вызова detach(), то лучше использовать вместо него QsharedDataPointer<T>.

Таким образом, класс работает как обычный указатель с подсчетом ссылок, удаляющий данные только в том случае, если на счетчике остается 0.

Заключение

При написании переносимого кода удобно использовать специальные typedef, такие как quint32 или quint64. Иногда typedef вводится просто для краткой записи, например ulong как синоним unsigned long.

Для эффективной работы с пользовательскими типами необходимо знать, являются ли они примитивами без конструктора и деструктора либо сложными типами. Не стоит также забывать, можно ли перемещать объекты в памяти. В Qt это указывается через макрос Q_DECLARE_TYPEINFO.

В качестве объектов, которые могут хранить значения разных типов, используются варианты QVariant. Они похожи на объединения, но работают удобнее и безопаснее.

Для удобства разработчика предусмотрены «умные» указатели, которые работают как обычные, но предоставляют дополнительные возможности. Защищенные указатели QPointer<T> обнуляются при удалении объекта, QSharedPointer<T> работает как жесткая ссылка, а QWeakPointer<T> – как слабая.

Многие классы Qt неявно разделяют данные. Новые классы с неявным или явным разделением данных легко реализуются через QSharedData и QSharedDataPointer<T> или QExplicitlySharedDataPointer<T>.

Подсчет ссылок везде осуществляется с атомарными операциями над счетчиком, поэтому безопасен для многопоточных приложений.

В следующей статье мы продолжим разговор о базовых сведениях по программированию с использованием Qt и рассмотрим контейнеры, а также алгоритмы.

В начало

Загрузка

Скачать исходный код с примерами (zip, 3 кб)

 

Ресурсы

Об авторе

 

Алексей Бешенов --- независимый разработчик и технический писатель, работающий со свободным программным обеспечением и свободными технологиями. Интересуется функциональным и логическим программированием, занимается математикой и теоретической информатикой.