template ‹class T›
class SmartPointer {
private:
T* tObj;
public:
SmartPointer(T* _t=NULL): tObj(_t);
~SmartPointer() {if (tObj) delete tObj;}
operator T*(){return tObj;}
T* operator-›(){return tObj;}
};
1. Обработка обращения к NULL.
Заменяем реализацию оператора -› на:
T* operator-›() {
if (!tObj) {
cerr ‹‹ "NULL";
tObj = new T;
}
return tObj;
}
или
T* operator-›() {
if (!tObj) throw CError;
return tObj;
};
Здесь CError класс исключения. Или втыкаем статический экземпляр-шпион.
private:
T* tObj; // Это было;
static T* spy; // Это добавлено
Ну и сам перегруженный оператор.
T* operator-›() {
if (!tObj) return spy;
return tObj;
};
Здесь нужно пояснить: spy совсем не обязательно класса T. Можно воткнуть производный, и переопределить его функции. Тогда он будет Вам докладывать о попытках обращения к NULL. Не забудьте его создать, инициализировать, и прицепить к указателю. А то вся идея на помойку. Вы пытаетесь отловить обращение к NULL, а там… NULL!!! "Матрицу" видели?
2. Отладка и трассировка.
Ну это совсем банально. Выносим определение операторов за определение класса и ставим там точку останова. Чтобы не тормозило в релиз версии, окружаем слово inline ифдефами.
template ‹class T›
#ifndef DEBUG
inline
#endif
SmartPointer‹T›::operator T*() {
return tObj;
}
template ‹class T›
#ifndef DEBUG
inline
#endif
T* SmartPointer‹T›::operator T-›() {
return tObj;
}
3. Статистика классов и объектов.
Ну все, здесь уже совсем все просто. Ничего писать не буду, кроме напоминания о том, что всенепременнейше нужно определять статистические переменные класса, в том числе и для параметризированного (то бишь для шаблона), и ровно один раз.
4. Кэширование.
Здесь сложнее. Об этом мне самому нужно почитать и полапать руками. Идея, как можно догадаться, в том, что если при обращении к умному указателю объект отсутствует в памяти, он считывается с диска. Проблемы самые очевидные в том, когда его снова отгружать на диск, разрушать объект, и как гарантировать единичность копии объекта при наличии многих ссылок.
Так. Пока тормозим. Интересно, о чем я напишу следующий шаг?
Шаг 4 - О двойной диспетчеризации.
Предположим, у нас есть массив, в котором мы храним карту местности. Разумеется, что элементы массива разнообразные - дома, колодцы, казино… ничего общего. Кроме суперкласса - предка естественно.
CBuilding
¦
______¦_______
¦ ¦ ¦
CHouse CWell CCasino
А карту эту мы отражаем разными способами. И даже не то, что разными способами, а имеем для такой благой цели несколько видов карт. Ну я не знаю, не картограф. Черви и пики. Нет, ладно. Радиоактивность и карма.
CMap
|
____________
| |
CRadioMap CCarmaMap
И что получается? Кто будет себя отрисовывать? И кто кого? Для каждой комбинации наследников CBuilding и CMap свой уникальный алгоритм. Что делать то будем? Какие феерические решения приходят… нет… не вам! Вашему коллеге или начальнику или подчиненному в голову? Да они ни сном ни духом о двойной диспетчеризации! Они скорее всего предложат получить информацию о типе во время исполнения, и запузырить в Ваш прекрасный проект кривоногий switch (){}. Да еще и положить в каждый класс статический член с информацией о типе… Одно звучание предыдущей фразы наводит на подозрения. Но что делаем мы? вот что:
class CBuilding: {
public:
virtual void doDraw(CMap* map)=0;
}
class CHouse: public CBuilding {
public:
virtual void doDraw (CMap* map) {
// ВОТ ОНА САМАЯ КОРКА!
map-›doDraw(*this);
}
};
// Эти такие же.
class CWell: public CBuilding {
public:
virtual void doDraw (CMap* map) {map-›doDraw(*this);}
};
class CCasino: public CBuilding {
public:
virtual void doDraw (CMap* map) {map-›doDraw(*this);}
};
// Это абстрактный класс для карт.
class CMap {
public:
virtual void doDraw (CHouse& cb)=0;
virtual void doDraw (CWell& cb)=0;
virtual void doDraw (CCasino& cb)=0;
};
Это конечно не все. Теперь нужно наследовать CRadioMap и CcarmaMap от общего предка CMap и в каждом классе рисовать реализацию алгоритма. За отрисовку отвечает карта, но какая масть - решает виртуальная CBuilding::doDraw(), а какое строение - выбирается перегруженная CMap::doDraw().
Одинаковое имя для функций отрисовки в разных классах давать не обязательно, но это является хорошим тоном при двойной диспетчеризации, и плохим без нее.
Круто? Это - подвиг неизвестного программиста. У Элджера был разобран пример со сложением чисел, очень красивый, но не сразу понятный. Там числа происходят от одного предка, что левый, что правый операнд оператора +, и по моему, обе диспетчеризации происходят по механизму виртуальных функций. Увы, мне лень набирать код.
Код к данному шагу я не проверял, в отличие от предыдущих. К диспетчеризации мы еще вернемся. Или не вернемся. Но следующий шаг однозначно про указатели.
Шаг 5 - Ведущие указатели (Master Pointers). Важные конструкторы.
Если мы уж взялись заниматься умными указателями, то очень быстро придем к выводу, что неплохо ограничить их свободу так, чтобы два указателя не указывали на один объект. Далее я их называю ведущими указателями. Для этого нужно реализовать буквально три-четыре правила:
1. Порождение ведущего указателя порождает объект, уничтожение ведущего указателя уничтожает объект;
2. Копирование ведущего указателя создает точную копию объекта;
3. Присваивание ведущего указателя уничтожает предыдущий объект и ставит на его место копию нового объекта.
Если же мы хотим получить однозначное соответствие объекта и его ведущего указателя, то нужно запретить создание объекта, кроме как при помощи ведущего указателя, и запретить создание ведущего указателя, кроме как специальной функцией. Последнее в общем не обязательно, а первое весьма важно.
Такие простые, но замечательно полезные механизмы просто сами набираются на клавиатуре сначала в виде класса, а потом в виде шаблона класса (мы же не последний день на свете живем, пригодится еще).
class CThat {
private:
int i;
public:
CThat (int _i=0):i(_i) {}
CThat (const CThat& _that):i(_that.i) {}
CThat& operator=(const CThat& _that) {
if (this == &_that) return *this;
i = _that.i;
return *this;
}
};
class MasterPointer {
private:
CThat* t;
public:
// MasterPointer():t(new CThat){}
MasterPointer(CThat _that=0):t(new CThat(_that)) {}
MasterPointer(const MasterPointer& mp): t(new CThat((*mp.t))) {}
~MasterPointer() { delete t; }
MasterPointer& operator=(const MasterPointer& mp) {
if (this != &mp) {
delete t;
t = new CThat(*(mp.t));
}
return *this;
}
};
Напоминать не надо, что this - это указатель на самого себя? Кстати и оказалось, что для реализации ведущего указателя класс указываемого объекта должен и сам иметь:
1. Конструктор без аргументов или с аргуметами, имеющими значения по умолчанию (default constructor). Компилятор вам нарисует такой один, если вы не определите конструкторов вообще никаких. На вид это будет просто ежик чернобыльский. Попытка создать внутри функции (я имею в виду в стеке) массив таких объектов наплодит вам массив дегенератов (это вне семантики ведущих указателей, мы же договариваемся, что без ведущих указателей такие объекты вообще не существуют). Так что не рискуйте.
2. Конструктор копии. Если вы не определите его, то компилятор нарисует свой. ТАКОЕ компилятору можно позволять только в крайнем случае, или перед пенсией, ибо по сравнению с этим чудовищем упомянутый ранее ежик просто Киану Ривз.
3. Оператор присваивания. То же самое. Подробности можно узнать в любой книге по C++, или в киоске с видеокассетами в разделе "Ужасы".
4. Виртуальный деструктор. Ну это еще ничего. Если Вы его не определите, то компилятор не задушит Вас ночью. Но вообще… должны быть очень серьезные причины для того, чтобы деструктор не был виртуальным, если вы наследуете свой класс или собираетесь от него наследовать.