Использование "умных" указателей
Страница 2.


 

Особенно стоит оговорить здесь конструктор MPtr::MPtr(T* p), который несколько выбивается из общей концепции. Все дело в том, что гарантировать отсутствие указателей на реальный объект может лишь создание такого объекта где-то внутри, это сделано в MPtr::MPtr(), где вызов new происходит самостоятельно. В итоге некоторая уверенность в том, что значение указателя никто нигде не сохранил без использования умного указателя, все-таки есть. Тем не менее, очень часто встречается такое, что у типа T может и не быть конструктора по умолчанию и объекту такого класса обязательно при создании требуются какие-то аргументы для правильной инициализации. Совсем правильным будет для подобного случая породить из MPtr новый класс, у которого будут такие же конструкторы, как и у требуемого класса, но реально я на такое геройство ни разу не сподобился. Поэтому, сделав себе торжественную клятву, что подобный конструктор MPtr::MPtr(T* p) будет использоваться только как MPtr<T> ptr(new T(a,b,c)) и никак иначе, я ввел этот конструктор в шаблон.

Еще один спорный момент: наличие оператора преобразования к T*. Честно говоря, я ни разу им не пользовался, но его наличие так же дает потенциальную возможность где-нибудь сохранить значение реального указателя.

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

Кроме MPtr я активно использую еще одну разновидность "умных" указателей, которая логично вытекает из описанной выше и отличается лишь одной деталью:


template<class T>

class MCPtr

{

public:

MCPtr(const MPtr<T>& p);

MCPtr(const MCPtr<T>& p);

~MCPtr();

const T* operator->() const;

operator const T*() const;

MCPtr<T>& operator=(const MPtr<T>& p)

MCPtr<T>& operator=(const MCPtr<T>& p);

protected:

MPtr<T> ptr;

private:

MCPtr();

};


Во-первых, это надстройка (адаптер) над обычным указателем. А во-вторых, его главное отличие, это то, что operator-> возвращает константный указатель, а не обычный. Это очень просто и, на самом деле, очень полезно: все дело в том, что это дает использовать объект в двух контекстах --- там, где его можно изменять (например, внтури другого объекта, где он был создан) и там, где можно пользоваться лишь константным интерфейсом (т.е., где изменять нельзя; например, снаружи объекта-фабрики). Это логично вытекает из простых константых указателей. Для того, что бы пользоваться MCPtr требуется единственное (хотя и достаточно строгое) условие: во всех классах должны быть корректно расставленна константность методов. Вообще, это, на мой взгляд, признак профессионального программиста: использование модификатора const при описании методов.

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

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

Конечно же, использовать "умные" указатели надо с осторожностью. Все дело в том, что, как у всякой простой идеи, у нее есть один очень большой недостаток: нетрудно придумать пример, в котором два объекта ссылаются друг на друга через "умные" указатели и... никогда не будут удалены. Почему? Потому что счетчики ссылок у них всегда будут как минимум 1, при том, что снаружи на них никто не ссылается. Есть рекомендации по поводу того, как определять такие ситуации во время выполнения программы, но, на мой взгляд, они очень громоздки и, поэтому не годятся к использованию. Ведь что привлекает в "умных" указателях? Простота. Фактически, ничего лишнего, а сколько можно при желании извлечь пользы из их применения.

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

"Умные" указатели активно используются в отображении COM-объектов и CORBA-объектов на C++: они позволяют прозрачно для программиста организовать работу с объектами, которые реально написаны на другом языке программирования и выполняются на другой стороне земного шара.

Техника подсчета ссылок в явном виде (через вызов методов интерфейса AddRef() и Release()) используется в технологии COM.

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

Резюме

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

Несмотря на присутствие в STL класса auto_ptr, "умные" указатели каждый программист должен закодировать для себя сам, потому что должен досконально точно понимать механизм их работы

 
« Предыдущая статья   Следующая статья »