Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Сабуров С.В. - Язык программирования C и C++ - 2006

.pdf
Скачиваний:
314
Добавлен:
13.08.2013
Размер:
1.42 Mб
Скачать

Трюки программирования

Тонкости того, почему operator >() возвращает именно указатель A* (у которого есть свой селектор), а не, скажем, ссылку A& и все равно все компилируется таким образом, что выполнение доходит до метода A::method(), пропустим за ненадобностью — здесь мы не планируем рассказывать о том, как работает данный механизм и какие приемы применяются при его использовании.

Достоинства подобного подхода, в принципе, очевидны: возникает возможность контроля за доступом к объектам; малость тривиальных телодвижений и получается указатель, который сам считает количество используемых ссылок и при обнулении автоматически уничтожает свой объект, что позволяет не заботиться об этом самостоятельно... не важно? Почему же: самые трудно отлавливаемые ошибки — это ошибки в употреблении динамически выделенных объектов. Сплошь и рядом можно встретить попытку использования указателя на удаленный объект, двойное удаление объекта по одному и тому же адресу или неудаление объекта. При этом последняя ошибка, в принципе, самая невинная: программа, в которой не удаляются объекты (значит, теряется память, которая могла бы быть использована повторно) может вполне спокойно работать в течение некоторого периода (причем это время может спокойно колебаться от нескольких часов до нескольких дней), чего вполне хватает для решения некоторых задач. При этом заметить такую ошибку довольно просто: достаточно наблюдать динамику использования памяти программой; кроме того, имеются специальные средства для отслеживания подобных казусов, скажем, BoundsChecker.

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

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

377

Трюки программирования

удаляет что то совсем немыслимое. Вообще, что значит «удаляет»? Это значит, что помечает память как пустую (готовую к использованию). Как правило, менеджер кучи, для того чтобы знать, сколько памяти удалить, в блок выделяемой памяти вставляет его размер. Так вот, если память уже была занята чем то другим, то по «неверному» указателю находится неправильное значение размера блока, вследствие этого менеджер кучи удалит некоторый случайный размер используемой памяти. Это даст следующее: при следующих выделениях памяти (рано или поздно) менеджер кучи отдаст эту «неиспользуемую» память под другой запрос и... на одном клочке пространства будут ютиться два разных объекта. Крах программы произойдет почти обязательно, это лучшее что может произойти. Значительно хуже, если программа останется работать и будет выдавать правдоподобные результаты. Одна из самых оригинальных ошибок, с которой можно столкнуться и которая, скорее всего, будет вызвана именно повторным удалением одного и того же указателя, то, что программа, работающая несколько часов, рано или поздно «падет» в функции malloc(). Причем проработать она должна будет именно несколько часов, иначе эта ситуация не повторится.

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

Целесообразность использования «умных» указателей хорошо видно в примерах реального использования. Вот, к примеру, объявление «умного» указателя с подсчетом ссылок:

template<class T> class MPtr

{

public:

MPtr();

MPtr(const MPtr<T>& p); ~MPtr();

MPtr(T* p);

T* operator >() const; operator T*() const;

378

Трюки программирования

MPtr<T>& operator=(const MPtr<T>& p); protected:

struct RealPtr

{

T* pointer; unsigned int count;

RealPtr(T* p = 0); ~RealPtr();

};

RealPtr* pointer; private:

};

Особенно стоит оговорить здесь конструктор 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:

379

Трюки программирования

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 при описании методов.

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

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

380

Трюки программирования

«обертки» чрезвычайно полезны, им можно найти массу применений.

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

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

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

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

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

381

Трюки программирования

Рассуждения на тему «Умных» указателей

При изучении С++, не раз можно встретиться с «умными» указателями. Они встречаются везде и все время в разных вариантах. Без стандартизации.

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

Итак, что такое SmartPointer ы? По сути это такие классы, которые умеют чуть больше... — а в общем, смотрим пример:

class A

{

private:

int count; public:

A(){count = 0;}

void addref(){count++;}

void release(){if( count == 0) delete this;} protected:

~A();

public:

void do_something(){cout << "Hello";}

};

Сначала придумали внутри объекта считать ссылки на него из других объектов при помощи «механизма подсчета ссылок». Суть здесь в том, что когда вы сохраняете ссылку на объект, то должны вызвать для него addref, а когда избавляетесь от объекта, то вызвать release. Сложно? Совсем нет — это дело привычки. Таким образом, объект умеет сам себя удалять. Здорово? Так оно и есть.

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

Теперь переходим к собственно самим «умным» указателям и опять пример:

class PA

{

382

Трюки программирования

private: A* pa;

public:

PA(A* p){pa = p; p >addref();} ~PA(){pa >release();}

A* operator >(){return pa;}

};

Что мы видим? Есть класс PA, который умеет принимать нормальный указатель на объект класса A и выдавать его же по обращению к селектору членов класса. «Ну и что? — скажете вы, — Как это может помочь?». За помощью обратимся к двум примерам, которые иллюстрирует эффективность использования класса PA:

...

{

A* pa = new A(); pa >addref(); pa >do_something(); pa >release();

}

...

{

PA pa = new A(); pa >do_something();

}

Посмотрим внимательнее на эти два отрывка... Что видим? Видим экономию двух строчек кода. Здесь вы наверное скажете: «И что, ради этого мы столько старались?». Но это не так, потому что с введением класса PA мы переложили на него все неприятности со своевременными вызовами addref и release для класса A. Вот это уже что то стоит!

Дальше больше, можно добавить всякие нужные штучки, типа оператора присваивания, еще одного селектора (или как его некоторые называют «разименователь» указателя) и так далее. Таким образом получится удобная вещь (конечно, если все это дело завернуть в шаблон.

Теперь немного сменим направление рассуждений. Оказывается существуют такие вещи, как «Мудрые указатели»,

383

Трюки программирования

«Ведущие указатели», «Гениальные указатели», «Грани», «Кристаллы» — в общем, хватает всякого добра. Правда, в практичности этих поделок можно усомниться, несмотря на их «изящество». То есть, конечно, они полезны, но не являются, своего рода, панацеей (кроме «ведущих» указателей).

В общем особую роль играют только «ведущие» и «умные» указатели.

Начнем с такого класса как Countable, который будет отвечать за подсчет чего либо.

Итак он выглядит примерно так (в дальнейшем будем опускать реализации многих функций из за их тривиальности, оставляя, тем самым, только их объявления:

class Countable

{

private:

int count; public:

int increment (); int decrement ();

};

Здесь особо нечего говорить, кроме того, что, как всегда, этот класс можно сделать более «удобным», добавив такие вещи, как поддержку режима многопоточности и т.д.

Следующий простой класс прямо вытекает из многопоточности и осуществляет поддержку этого режима для своих детей:

class Lockable

{

public:

void lock(); void unlock(); bool islocked();

};

Этот класс не вносит никакой новизны, но стандартизует поддержку многопоточности при использовании, например, различных платформ.

384

Трюки программирования

Теперь займемся собственно указателями: class AutoDestroyable : public Countable

{

public:

 

 

 

virtual

int

addref

();

virtual

int

release

();

protected:

 

 

 

virtual

~AutoDestroyable();

...

 

 

 

};

Из кода видно, что этот класс занимается подсчетом ссылок

и«убивает» себя если «пришло время».

Асейчас процитируем код «умного» указателя, для того чтобы синхронизовать наши с вами понимание этого чуда.

template <class T> class SP

{

private:

T* m_pObject; public:

SP ( T* pObject){ m_pObject = pObject; }

~SP () { if( m_pObject ) m_pObject >release (); } T* operator > ();

T& operator * (); operator T* ();

...

};

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

Теперь остановимся на минутку и подумаем, когда мы можем использовать этот тип указателей, а когда нет. Главное требование со стороны SP, это умение основного объекта вести подсчет ссылок на себя и уничтожать себя в тот момент, когда он не становится нужен. Это серьезное ограничение, поскольку не во все используемые классы вы сможете добавить эти

385

Трюки программирования

возможности. Вот несколько причин, по которым вы не хотели бы этого (или не можете):

Вы используете закрытую библиотеку (уже скомпилированную) и физически не можете добавить кусок кода в нее.

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

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

Итак, причин много или по крайней мере достаточно для того, чтобы задуматься над более универсальным исполнением SP. Посмотрим на схематичный код «ведущих» указателей и «дескрипторов»:

template <class T>

clacc MP : public AutoDestroyable

{

private:

T* m_pObj; public:

MP(T* p);

T* operator >(); protected:

operator T*();

};

template <class T> class H

{

private:

MP<T>* m_pMP; public:

H(T*);

MP& operator T >(); bool operator ==(H<T>&); H operator =(H<T>&);

};

386

Соседние файлы в предмете Программирование на C++