MyTetra Share
Делитесь знаниями!
Размышления об умных указателях в Qt от Андрея Боровского. Отличия QPointer и QScopedPointer
Время создания: 13.07.2020 10:40
Автор: Андрей Боровский
Текстовые метки: язык, c++, qt, qt4, qt5, умные указатели, QPointer, QScopedPointer
Раздел: Компьютер - Программирование - Язык C++ - Библиотека Qt - Принципы написания кода

Интеллектуальные указатели


... Вы, возможно, обратили внимание, что в методе deleteModel() я совершенно не забочусь о содержимом указателей, для которых вызывается оператор delete. Я могу себе это позволить потому, что использую в качестве указателей на объекты моделей объекты классов на основе шаблона QPointer . QPointer — простейший из интеллектуальных указателей (smart pointers), используемых в Qt. Для получения из него класса ему необходимо сообщить тип данных, на который (простите за тавтологию) указывает указатель. В результате у нас появляется класс, который эмулирует поведение указателя на некоторый тип данных. Например, конструкция QPointer<Table> создает интеллектуальный указатель на объект класса Table .

 

Такой класс совместим по присваиванию с обычными указателями (Table *) и поддерживает ряд операций над указателями, например операцию разыменования указателя (оператор *). Но кроме этого, интеллектуальные указатели обладают различными функциями, которых нет у обычных указателей: счетчиком ссылок, или, например, возможностью выполнять атомарные операции. Библиотека Qt library активно использует интеллектуальные указатели в своем коде, но на уровне внешних интерфейсов предпочитает обычные. Все-таки интеллектуальные указатели сложнее в обслуживании, нежели обычные, а некоторые аспекты их использования могут сбить неподготовленного программиста с толку.

 

Самое интересное в интеллектуальных указателях — их дуализм. Когда мы обращаемся к переменной типа TablePtr , используя оператор разыменовывания (явно, или неявно, в виде оператора –> ), мы получаем доступ к объекту Table , как и должно быть с указателем. Но если мы обращаемся к переменной типа TablePtr , используя точку, то получаем доступ к методам объекта класса, реализующего указатель (в нашем примере — QPointer<Table> ). Когда же мы обращаемся к значению самой переменной типа TablePtr , например, в операциях сравнения или присваивания, мы, опять-таки, имеем дело с указателем на объект Table . Описанный дуализм достигается достаточно просто — путем перегрузки операторов, которые используются для работы с указателями.

 

Зачем нам нужен указатель QPointer? Согласно документации главное достоинство этого интеллектуального указателя в том, что мы всегда можем узнать, действительно ли он указывает на объект соответствующего типа (аргументом шаблона может быть только объект класса QObject или его потомка). Если где-то в программе объект был удален (или указатель так и не был инициализирован), и содержащийся в указателе адрес не соответствует никакому объекту, метод isNull() класса, основанного на QPointer , сообщит нам об этом (если указатель QPointer не содержит корректный адрес объекта, он возвращает значение 0). Документация рекомендует использовать указатели QPointer в том случае, когда ваш класс получает указатель на некий объект от другого класса. Если в какой-то момент другой класс удалит объект, на который ссылался указатель, ваш QPointer сможет сообщить вам об этом. По моему мнению, такое использование указателя QPointer провоцирует неряшливый стиль программирования. Если ваши классы обмениваются указателями, жизненный цикл которых не подчиняется определенным правилам, вам лучше пересмотреть структуру своей программы.

 

Однако указатель QPointer может быть полезен в другой ситуации. В программе musicdatabase объекты модели данных создаются тогда, когда пользователь создает или открывает базу данных. При выполнении операций с указателями на объекты модели данных необходимо знать, созданы ли уже эти объекты. Мы могли бы присвоить в конструкторе Dialog значения NULL всем этим указателям и затем проверять, сохранили ли они эти значения. Указатели QPointer делают это за нас и тем самым экономят нам несколько строк кода. Еще одно удобство, которое предоставляет нам указатель QPointer , — безопасная операция удаления объекта. Поскольку указатель становится равным нулю при удалении объекта, на который он указывает, а вызов оператора delete для нулевого указателя не имеет последствий, мы можем вызывать delete , не беспокоясь о значении указателя. Если значением указателя является адрес объекта, объект будет удален. Если нет — ничего не случится.

 

Еще один интересный интеллектуальный указатель — QScopedPointer , появившийся в Qt 4.6. Особенность этого указателя заключается в том, что он уничтожает (по умолчанию — с помощью оператора delete ) объект, на который указывает, при выходе указателя из области видимости, в том числе если это произошло в результате исключения. Ту же задачу можно решить с помощью класса-помощника, локальный объект которого создается внутри функции и в деструкторе которого можно выполнить все действия, необходимые при выходе локального объекта из области видимости, но шаблон QScopedPointer упрощает решение этой задачи. Поскольку по умолчанию QScopedPointer вызывает для удаления объекта оператор delete , по умолчанию же он должен указывать на объект, созданный с помощью оператора new . Однако это поведение можно изменить. Указатель QScopedPointer позволяет использовать при работе с Qt некоторые приемы и шаблоны программирования, которые обычно применяются в системах разработки с автоматической сборкой мусора. Иначе говоря, при использовании указателя QScopedPointer вам не придется заботиться о постановке операторов delete или функций, освобождающих память, везде, где указатель может стать недоступным.

 

Вторым параметром шаблона может быть класс-"удалитель", который и выполняет фактическое удаление объекта, на который ссылается указатель. Qt 4.7 предоставляет нам несколько таких классов. Помимо QScopedPointerDeleter, который используется по умолчанию, мы можем задействовать QScopedPointerArrayDeleter , предназначенный для удаления массивов, созданных с помощью оператора new (для той же цели существует указатель QScopedArrayPointer, который специально предназначен для работы с массивами, созданными с помощью new ). Еще один вспомогательный класс, QScopedPointerPodDeleter, вызывает для удаления объекта, на который ссылается указатель, функцию free(). Иными словами, с его помощью можно удалить область памяти, выделенную с помощью malloc(). Если возможностей классов, выполняющих удаление, которые предоставлены библиотекой Qt, вам не хватает, можете написать свой собственный класс.

 

Есть несколько ситуаций, в которых вам может понадобиться собственный вспомогательный объект-"удалитель". Во-первых, такая необходимость возникает, когда вы пользуетесь собственным менеджером памяти, и соответственно, собственной функцией для удаления объектов. Вторая ситуация — использование разделяемой библиотеки, которая создает и удаляет некие непрозрачные для вас объекты, а затем требует уничтожать их с помощью специальной функции.

 

Примером такой библиотеки может служить библиотека mpg123, предназначенная для работы с различными файлами формата MPEG, в том числе MP3. В начале работы с mpg123 вы должны получить указатель на структуру mpg123_handle . Сама структура непрозрачна для вас, иначе говоря, у вас в программе не должно быть ее определения.


 

ПРИМЕЧАНИЕ


Поскольку исходные тексты mpg123 доступны всем желающим, вы можете включить определение структуры mpg123_handle из исходных текстов mpg123 в текст своей программы. Но делать этого не следует. Разработчики библиотеки предполагают, что строение структуры mpg123_handle является "внутренним делом" библиотеки, и могут в любой момент изменить его. Если вы не хотите, чтобы ваша программа внезапно перестала работать при обновлении версий разделяемых библиотек, не пытайтесь напрямую использовать структуры, помеченные как внутренние.


 

Экземпляр mpg123_handle создается так:


 

mpg123_handle * mpg123_new (const char *decoder, int *error)


 

Указатель, возвращенный этой функцией, затем передается в качестве первого параметра другим функциям mpg123, так что вам совершенно не обязательно знать, на что именно он указывает. Поскольку внутреннее строение типа данных mpg123_handle вам неизвестно, вы, в принципе, можете рассматривать указатель, который возвращает функция mpg123_new(), как void *. После того как работа с экземпляром типа mpg123_handle закончена, его следует удалить так:


 

mpg123_delete (mpg123_handle *mh)


 

Поскольку вы не знаете, как создается экземпляр mpg123_handle , то и пытаться удалить его с помощью какой-либо другой функции просто опасно. Так что если вы хотите, чтобы жизненным циклом переменной типа mpg123_handle управлял интеллектуальный указатель QScopedPointer , вам придется создать собственный класс для удаления экземпляра (листинг 3.4).



Листинг 3.4. Использование указателя QScopedPointer


struct Mpg123HandleDeleter

{

static inline void cleanup(mpg123_handle * handle)

{

mpg123_delete(handle)

}

};


...


int error

QScopedPointer<mpg123_handle, Mpg123HandleDeleter>

mpg123Handle(mpg123_new(NULL, &error));


 

Переменная mpg123Handle будет содержать интеллектуальный указатель на структуру mpg123_handle. В первом параметре функции mpg123_new() можно указать имя кодека, а второй параметр вернет информацию об ошибке, если таковая произойдет, но все это относится к особенностям интерфейса mpg123, который мы здесь не рассматриваем. Для нас важно, что переменную mpg123Handle можно использовать везде, где можно было бы поставить переменную типа mpg123_handle *, и что при выходе переменной mpg123Handle из области видимости будет вызвана функция cleanup() структуры mpg123HandleDeleter.

 

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


 

Интеллектуальные указатели – "за и против"


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

 

Между прочим, подобный аргумент можно применить и против указателя QPointer . Этот указатель не скрывает от нас ошибку, возникающую при попытке разыменовывания неинициализированного указателя. Такая попытка будет равносильна разыменовыванию нулевого указателя, что приведет к генерации исключения. Однако указатель QPointer может скрыть от нас другую ошибку — ошибку, связанную с многократным удалением одного и того же объекта. Поскольку операция delete с этим указателем безопасна, мы никогда не узнаем, что удаляем уже удаленный объект. Можно возразить, что повторное удаление объекта — не такая страшная ошибка, как попытка разыменовывания неинициализированного указателя, но сам факт повторного удаления может свидетельствовать об изъянах в структуре программы, о которых программисту следует знать. Любопытно, однако, что интеллектуальный указатель не позволит инициализировать себя "мусором". Следующий фрагмент кода (листинг 3.5) приведет к генерации исключения, чего не случилось бы, если бы переменная obj была обычным указателем.


 

Листинг 3.5. Попытка инициализации интеллектуального указателя "мусором"


typedef QPointer<QObject> ObjPtr;

...

ObjPtr obj

QObject * f = (QObject *) 0xbadf00d;

obj = f;



В общем, по моему мнению, QPointer — не самый полезный из интеллектуальных указателей. Впрочем, нельзя сказать, что концепция интеллектуальных указателей вообще не нужна. Далее будут рассмотрены интеллектуальные указатели QSharedPointer и QAtomicPointer , весьма полезные в определенных ситуациях...

 

Так же в этом разделе:
 
MyTetra Share v.0.53
Яндекс индекс цитирования