Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ — страница 11 из 74

Что следует помнить

• Компилятор может неявно генерировать для класса конструктор по умолчанию, конструктор копирования, оператор присваивания и деструктор.

Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны

Агенты по продаже недвижимости и программные системы, обслуживающие их деятельность, могут нуждаться в классе, представляющем дома, выставленные на продажу:


class HomeForSale {...};


Любой агент по продаже недвижимости скажет вам, что каждый объект уникален – не бывает двух, в точности одинаковых. Вот почему идея создания копии объекта HomeForSale бессмысленна. Как можно скопировать нечто, по определению, уникальное? Поэтому хотелось бы, чтобы попытки скопировать объекты HomeForSale не компилировались:


HomeForSale h1;

HomeForSale h2;

HomeForSale h3(h1); // попытка скопировать h1 –

// не должно компилироваться!

h1 = h2; // попытка скопировать h2 –

// не должно компилироваться!


Увы, предотвратить такую компиляцию не так-то просто. Обычно, если вы не хотите, чтобы класс поддерживал определенного рода функциональность, вы просто не объявляете функций, которые ее реализуют. Но с конструктором копирования и оператором присваивания эта стратегия не работает, поскольку, как следует из правила 5, если вы их не объявляете, а где-то в программе производится попытка их вызвать, то компилятор сгенерирует их автоматически.

Похоже на безвыходное положение. Если вы сами не объявите конструктор копирования или оператор присваивания, то их сгенерирует компилятор. И ваш класс будет поддерживать копирование. Но то же самое произойдет, если вы объявите эти функции самостоятельно. Однако наша цель – предотвратить копирование!

Ключ к решению в том, что все сгенерированные компилятором функции являются открытыми. Чтобы предотвратить автоматическое генерирование, вы должны объявить их самостоятельно, но никто не требует, чтобы они были открытыми. Ну так и объявите конструктор копирования и оператор присваивания закрытыми. Объявляя явно функцию-член, вы предотвращаете генерирование ее компилятором, а сделав ее закрытой, не позволяете кому-либо вызывать ее.

Схема не идеальна, потому что другие члены класса и функции-друзья по-прежнему могут вызывать закрытые функции. Если только вы не включите лишь объявление, опустив определение. Тогда если кто-то случайно вызовет такую функцию, то получит сообщение об ошибке на этапе компоновки. Этот трюк – объявление функций-членов закрытыми и сознательный отказ от их реализации – как раз и используется для предотвращения копирования в некоторых классах библиотеки iostreams. Взгляните, например, на объявления классов ios_base, basic_ios и sentry в вашей реализации стандартной библиотеки. Вы обнаружите, что в каждом случае как конструктор копирования, так и оператор присваивания объявлены закрытыми и нигде не определены.

Применить эту уловку в классе HomeForSale несложно:


class HomeForSale {

public:

...

private:

HomeForSale(const HomeForSale&); // только объявления

HomeForSale& oparetor=( const HomeForSale&);

};


Заметьте, что я не указал имена параметров функций. Это необязательно, просто таково общее соглашение. Ведь раз эти функции никогда не будут реализовываться и использоваться, то какой смысл задавать имена их параметров?

При таком определении компилятор будет блокировать любые попытки клиентов копировать объекты HomeForSale, а если вы случайно попытаетесь сделать это в функции-члене или функции-друге класса, то об ошибке сообщит компоновщик.

Существует возможность переместить ошибку с этапа компоновки на этап компиляции (это всегда полезно – лучше обнаружить ошибку как можно раньше), если объявить конструктор копирования и оператор присваивания закрытыми не в самом классе 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), будут уничтожены, что приведет к появлению так называемых «частично разрушенных» объектов. Это верный путь к утечке ресурсов, повреждению структур данных и проведению изрядного времени в обществе отладчика.