Совместный доступ к данным класса

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 2 | Документация

Jasmin Blanchette (перевод Andi Peredri)

Совместный доступ к данным, или копирование при записи (copy on write), широко используется в Qt, сочетая эффективность указателей с простотой и удобством обычных переменных. В этой статье рассказывается, как создавать свои классы с общими данными, используя технику d-указателей и счетчики экземпляров (reference counting). Этот материал может быть полезен широкому кругу читателей, так как эти технические приемы являются достаточно общими и могут найти широкое применение.

Содержание

[править] D-указатели

Перед тем, как закрытые данные объекта предоставить в совместное использование, вы должны отделить интерфейсные методы объекта от его закрытых данных, используя технику "d-указателей" (data pointer). Термин "d-указатель" впервые ввел Arnt Gulbrandsen. Позже эта техника была позаимствована разработчиками Qt и KDE. Она упоминается под названием "Pimpl", Pointer to implementation (указатель на реализацию) в книге Exceptional C++ и "Cheshire Cat" (чеширский кот) в Design Patterns.

Допустим, объявление исходного класса Catalog выглядит так:

#include <qmap.h>
#include <qstring.h>
 
class Catalog
{
public:
	/* ... */
private:
	QString name;
	QMap<QString, QString> itemMap;
};

Соответствующая версия с d-указателем содержит два класса: Catalog предоставляет интерфейсные методы, а CatalogData содержит данные:

#include <qmap.h>
#include <qstring.h>
 
struct CatalogData
{
	QString name;
	QMap<QString, QString> itemMap;
};
 
class Catalog
{
public:
	/* ... */
private:
	CatalogData *d;
};

Новый класс Catalog содержит только одно поле данных: d-указатель. Вот пример реализации двух конструкторов, деструктора, функции operator= и еще одной функции:

Catalog::Catalog()
{
	d = new CatalogData;
}
 
Catalog::Catalog( const Catalog&amp; other )
{
	d = new CatalogData( *other.d );
}
 
Catalog::~Catalog()
{
	delete d;
}
 
Catalog&amp; Catalog::operator=( const Catalog&amp; other )
{
	*d = *other.d;
	return *this;
}
 
void Catalog::addItem( const QString&amp; id, const QString&amp; desc )
{
	d->itemMap[id] = desc;
}

Заметьте, данные не являются общими. Если создать 243 объекта Catalog, будет создано ровно столько же объектов CatalogData.

Несмотря на то, что в заголовочном файле можно определить оба класса, Catalog и CatalogData, в файле catalog.h рекомендуется определять только класс Catalog, а его реализацию и определение класса CatalogData помещать в файл catalog.cpp. Чтобы компилятор распознавал такие типы, как CatalogData * или const QString &, в заголовочный файл нужно дополнительно поместить декларации используемых классов:

    class CatalogData;
    class QString;

А директивы #include переместить в класс catalog.cpp. В этом случае заголовочный файл будет содержать лишь интерфейс класса Catalog, а его внутреннее представление и реализация будут скрыты в cpp-файле.

Такое структурирование программы с использованием техники d-указателей обеспечивает бинарную совместимость между различными версиями библиотеки и ускоряет ее компиляцию за счет уменьшения числа обрабатываемых директив #include. Основными недостатками такого подхода являются невозможность встраивания функций компилятором и слегка замедленный доступ к данным. Однако это не всегда критично, поэтому d-указатели широко используются в классах Qt.

[править] Счетчики экземпляров

Теперь мы создадим версию класса Catalog с совместно используемыми данными. В результате одновременно несколько объектов Catalog смогут содержать указатели на один и тот же объект CatalogData. Чтобы это работало корректно, должны выполняться два следующих условия:

  1. Закрытые данные не должны изменяться, когда используются несколькими объектами.
  2. При уничтожении последнего объекта закрытые данные также должны быть уничтожены.

Это значит, что в любой момент времени объект, имеющий общие данные, должен уметь ответить на вопрос: "Я один?". Вот для этого необходимы счетчики экземпляров.

Мы начнем с расширения класса CatalogData полем refCount для подсчета количества экземпляров Catalog, содержащих указатели на определенный объект CatalogData.

struct CatalogData
{
	int refCount;
	QString name;
	QMap<QString, QString> itemMap;
};

Конструктор по умолчанию класса Catalog устанавливает счетчик экземпляров равным единице:

Catalog::Catalog()
{
	d = new CatalogData;
	d->refCount = 1;
}

Конструктор копирования инициализирует d-указатель адресом уже существующего объекта CatalogData и инкрементирует счетчик экземпляров:

Catalog::Catalog( const Catalog&amp; other )
{
	d = other.d;
	d->refCount++;
}

Деструктор уменьшает на 1 счетчик экземпляров и удаляет объект CatalogData, если он больше никем не используется:

Catalog::~Catalog()
{
	if ( --d->refCount == 0 )
		delete d;
}

Функция-операция присваивания более сложна. Самой распространенной ошибкой при ее реализации является обработка случая a = a. Даже в Qt 2.0 была ошибка в функции-операции присваивания класса QMap. Вот, как она должна быть реализована:

Catalog&amp; Catalog::operator=( const Catalog&amp; other )
{
	other.d->refCount++;
	if ( --d->refCount == 0 )
		delete d;
	d = other.d;
	return *this;
}

А вот ее типичная неверная реализация:

Catalog&amp; Catalog::operator=( const Catalog&amp; other )
{
	if ( --d->refCount == 0 )
		delete d;
	d = other.d;
	d->refCount++;
	return *this;
}

Функция addItem() и все остальные не-const функции перед изменением объекта CatalogData должны удостовериться в том, что данные используются монопольно. Обычно это обеспечивается вызовом функции detach():

void Catalog::addItem( const QString&amp; id,
            	   const QString&amp; desc )
{
	detach();
	d->itemMap[id] = desc;
}
 
void Catalog::detach()
{
	if ( d->refCount > 1 ) {
		d->refCount--;
		d = new CatalogData( *d );
		d->refCount = 1;
	}
}

Const-функции не потребуют дополнительных изменений.

Const-ссылки и строки типа QString
Более 750 функций в Qt принимают один или более параметров типа const QString &. При преобразовании из 8-разрядных строк в 16-разрядные в Qt 2.0, мы пытались использовать обычный QString в качестве параметров, но это оказалось немного медленнее, чем const QString &. Т.к. параметры типа QString очень часто используются, мы выбрали более быстрое решение. Для совместимости с предопределенными в Qt сигналами и слотами, используйте параметры const QString &, даже если вам безразлична скорость.

А теперь зададимся вопросом: был ли смысл реализовывать совместный доступ к данным класса Catalog? На самом деле, нет, так как основной объем данных хранится в классе QMap, и совместный доступ к ним уже реализован. Однако это пришлось бы сделать, если вместо QMap мы бы использовали его STL-версию map или нашу собственную структуру данных.

Сам по себе счетчик экземпляров может использоваться для избежания утечек памяти. Вот типичный случай:

static QMap<QString, QString> *globalMap = 0;
static int refCount = 0;
 
Image::Image()
{
	if ( refCount++ == 0 )
		globalMap = new QMap<QString, QString>;
}
 
Image::~Image()
{
	if ( --refCount == 0 ) {
		delete globalMap;
		globalMap = 0;
	}
}

[править] Неявный доступ безопаснее явного

Так же, как и холестерин, совместный доступ бывает опасным и безопасным. Безопасный тип доступа, который мы только что рассмотрели, в действительности называется неявным совместным доступом. Противоположный ему явный (опасный) доступ используется в некоторых Qt-классах, например: QMemArray<T>, QImage и QMovie.

При явном совместном доступе за вызов функции detach() перед изменением объекта отвечает сам пользователь. Если он забудет это сделать, то произойдет опасный побочный эффект, заключающийся в изменении состояния всех объектов, использующих общие данные.

Семантически классы с явным совместным доступом подобны указателям. Сравните код слева, в котором используются указатели int * и код справа, в котором используется воображаемый класс Int с явным совместным доступом:

int *a = new int( 111 );    Int a( 111 );
int *b = a;                 Int b = a;
*b = 222;                   b = 222;
qDebug( "%d", *a );         qDebug( "%d", (int) a );

В обоих случаях будет выведено 222. Мы ожидали это от левостороннего кода (этому способствует синтаксис указателей), но уж никак не от правостороннего, что стало для нас неприятным сюрпризом. Явный совместный доступ позволяет решить проблему смены владельца, однако обманчивый синтаксис дискредитирует его в качестве альтернативы указателям.

Своим явным совместным доступом классы QMemArray<T>, QImage и QMovie обязаны истории. Чтобы избавить себя от головной боли при работе с подобными классами, придерживайтесь одного из следующих правил:

  1. Избегайте классов с явным совместным доступом к данным.
  2. Вызывайте detach() всегда, когда собираетесь модифицировать объект, за исключением тех случаев, когда вы твердо уверены в отсутствии копии этого объекта. Нарушение этого правила - наиболее распространенная ошибка.
  3. Вызывайте detach() всегда, когда создаете копию объекта:
b = a;
b.detach();

Это предотвращает совместный доступ к данным и избавляет от необходимости вызывать detach() при модификации объекта. По возможности, используйте для этой цели функцию copy():

b = a.copy();

[править] Дополнительные возможности

Если два экземпляра класса с совместным доступом к данным содержат идентичные данные, значит ли это, что данные являются общими? Не всегда. Если один из экземпляров является копией второго, то данные используются совместно; если их идентичность случайна, то - нет. Например, двум следующим объектам Tune в памяти будут соответствовать два идентичных объекта TuneData:

Tune a( "8a1 4a1 8f1 8f1 4f1 8g1 8a1 4a1 8g1 8f1 4e1" );
Tune b( "8a1 4a1 8f1 8f1 4f1 8g1 8a1 4a1 8g1 8f1 4e1" );

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

Для примера рассмотрим класс QRegExp. Большинство программ использует небольшое количество регулярных выражений. В процессе работы на основе идентичных объектов QRegExp могут многократно создаваться сложные внутренние структуры данных. Реализация QRegExp в библиотеке Qt способствовала увеличению кэша для хранения наиболее часто используемых объектов QRegExpData (в действительности, QRegExpEngine).

Для более сложных классов может иметь смысл хранить список экземпляров объектов, чтобы при создании нового объекта можно было просмотреть существующие в поисках экземпляра с аналогичными данными. На первый взгляд, такое решение может показаться медленным, однако при использовании соответствующих структур данных (например, хеш-таблиц) поиск уже существующего объекта может занять меньше времени, чем создание нового. К тому же, такой подход гарантирует минимальный расход памяти.

Работа с классами, которые приходится часто создавать, присваивать и удалять, может быть ускорена за счет отложенной инициализации закрытых данных. Основным недостатком такого решения является то, что разработчик должен всегда проверять d-указатель перед его использованием. Эта техника используется в классах Qt QIconSet и QPainter.

Для счетчиков экземпляров в Qt используется небольшой внутренний класс QShared. Для получения большей информации о создании классов с совместным доступом к данным смотрите Implicitly and Explicitly Shared Classes.