Программирование с 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 кб)

Ресурсы


Об авторе

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