Существует возможность переместить ошибку с этапа компоновки на этап компиляции (это всегда полезно – лучше обнаружить ошибку как можно раньше), если объявить конструктор копирования и оператор присваивания закрытыми не в самом классе HomeForSale, а в его базовом классе, специально созданном для предотвращения копирования. Такой базовый класс очень прост:
class Uncopyable {
protected:
Uncopyable() {} // разрешить конструирование
~Uncopyable() {} // и уничтожение
// объектов производных классов
private:
Uncopyable(const Uncopyable&); // но предотвратить копирование
Uncopyable& operator=(const Uncopyable&);
};
Чтобы предотвратить копирование объектов HomeForSale, нужно лишь унаследовать его от Uncopyable:
class HomeForSale : private Uncopyable { // в этом класс больше нет ни
... // конструктора копирования, ни
} // оператора присваивания
Такое решение работает, потому что компилятор пытается генерировать конструктор копирования и оператор присваивания, если где-то – пусть даже в функции-члене или дружественной функции – производится попытка скопировать объект HomeForSale. Как объясняется в правиле 12, сгенерированные компилятором версии будут вызывать соответствующие функции из базового класса. Но это не получится, так как в базовом классе они объявлены закрытыми.
Реализация и использование класса Uncopyable сопряжена с некоторыми тонкостями. Например, наследование от Uncopyable не должно быть открытым (см. правила 32 и 39), а деструктор Uncopyable не должен быть виртуальным (см. правило 7). Поскольку Uncopyable не имеет данных-членов, то компилятор может прибегнуть к оптимизации пустых базовых классов, описанной в правиле 39, но коль скоро этот класс базовый, то возможно возникновение множественного наследования (см. правило 40). А множественное наследование в некоторых случаях не дает возможности провести оптимизацию пустых базовых классов (см. правило 39). Вообще говоря, вы можете игнорировать эти тонкости и просто использовать Uncopyable, как показано выше. Можете также воспользоваться версией из билиотеки Boost (см. правило 55). В ней этот класс называется noncopyable. Это хороший класс, но мне просто показалось, что его название немного, скажем так, неестественное.
Что следует помнить
• Чтобы отключить функциональность, автоматически предоставляемую компилятором, объявите соответствующую функцию-член закрытой и не включайте ее реализацию. Наследование базовому классу типа Uncopyable – один из способов сделать это.
Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе
Существует много способов отслеживать время, поэтому имеет смысл создать базовый класс TimeKeeper и производные от него классы, которые реализуют разные подходы к хронометражу:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper {…};
class WaterClock: public TimeKeeper {….};
class WristWatch: public TimeKeeper {…};
Многие клиенты захотят иметь доступ к данным о времени, не заботясь о деталях того, как они получаются, поэтому мы можем воспользоваться фабричной функцией (factory function), которая возвращает указатель на базовый класс созданного ей объекта производного класса:
TimeKeeper *getTimeKeeper(); // возвращает указатель на динамически
// выделенный объект класса,
// производного от TimeKeeper
В соответствии с соглашением о фабричных функциях объекты, возвращаемые getTimeKeeper, выделяются из кучи, поэтому для того, чтобы избежать утечек памяти и других ресурсов, важно, чтобы каждый полученный объект был рано или поздно уничтожен:
TomeKeeper *ptk = getTimeKeeper(); // получить динамически выделенный
// объект из иерархии TimeKeeper
... // использовать его
delete ptk; // уничтожить, чтобы избежать утечки
// ресурсов
Как объясняется в правиле 13, полагаться на то, что объект уничтожит клиент, чревато ошибками, а в правиле 18 говорится, как можно модифицировать фабричную функцию для предотвращения наиболее частых ошибок в клиентской программе. Здесь же мы обсудим более серьезный недостаток приведенного выше кода: даже если клиент все делает правильно, мы не можем узнать, как будет вести себя программа.
Проблема в том, что getTimeKeeper возвращает указатель на объект производного класса (например, AtomicClock), а удалять этот объект нужно через указатель на базовый класс (то есть на TimeKeeper), при этом в базовом классе (TimeKeeper) объявлен невиртуальный деструктор. Это прямой путь к неприятностям, потому что в спецификации C++ постулируется, что когда объект производного класса уничтожается через указатель на базовый класс с невиртуальным деструктором, то результат не определен. Во время исполнения это обычно приводит к тому, что часть объекта, принадлежащая производному классу, никогда не будет уничтожена. Если getTimeKeeper() возвращает указатель на объект класс AtomicClock, то часть объекта, принадлежащая AtomicClock (то есть данные-члены, объявленные в этом классе), вероятно, не будут уничтожены, так как не будет вызван деструктор AtomicClock. Те же члены, что относятся к базовому классу (то есть TimeKeeper), будут уничтожены, что приведет к появлению так называемых «частично разрушенных» объектов. Это верный путь к утечке ресурсов, повреждению структур данных и проведению изрядного времени в обществе отладчика.
Читать дальше
Конец ознакомительного отрывка
Купить книгу