Чтение онлайн

на главную - закладки

Жанры

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

Майерс Скотт

Шрифт:

public:

virtual void onTick const;

...

};

WidgetTimer timer;

...

};

Этот дизайн сложнее того, что использует только закрытое наследование, потому что здесь используются и открытое наследование, и композиция, а ко всему еще и новый класс (WidgetTimer). Честно говоря, я показал этот вариант в первую очередь для того, чтобы напомнить о существовании различных подходов к решению одной задачи. Стоит привыкать к тому, чтобы не ограничиваться единственным решением (см. также правило 35). Тем

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

Во-первых, вы можете спроектировать класс Widget так, чтобы ему можно было наследовать, но при этом запретить производным классам переопределять функцию onTick. Если Widget наследуется от Timer, то это невозможно, даже в случае закрытого наследования. (Напомню, что согласно правилу 35 производные классы могут переопределять виртуальные функции, даже если не могут вызывать их). Но если WidgetTimer – это закрытый класс внутри Widget, который наследует Timer, то производные от Widget классы не имеют доступа к WidgetTimer, а значит, не могут ни наследовать ему, ни переопределять его виртуальные функции. Если вам приходилось программировать на языках Java или C# и вы не обратили внимания на то, как можно запретить производным классам переопределять функции базового (с помощью ключевого слова final в Java или sealed в C#), то теперь вы знаете, как добиться примерно того же эффекта в C++.

Во-вторых, вы можете захотеть минимизировать зависимости Widget на этапе компиляции. Если Widget наследует классу Timer, то определение Timer должно быть доступно во время компиляции Widget, поэтому файл, определяющий Widget, вероятно, должен содержать директиву #include "Timer.h". С другой стороны, если WidgetTimer вынести из Widget, а в Widget оставить только указатель на WidgetTimer, тогда Widget сможет обойтись простым объявлением класса WidgetTimer; так что необходимость включать заголовочный файл для Timer будет устранена. Для больших систем такая развязка может оказаться важной. Подробнее о минимизации зависимостей на этапе компиляциии см. правило 31.

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

Граничный случай – действительно граничный: речь идет о классах, в которых вообще нет никаких данных. Такие классы не имеют ни нестатических членов-данных, ни виртуальных функций (поскольку наличие этих функций означает добавление указателя vptr в каждый объект – см. правило 7), ни виртуальных базовых классов (поскольку в этом случае тоже имеют место дополнительные расходы памяти – см. правило 40). Концептуально, объекты таких пустых классов вообще не занимают места, потому что в них не хранится никаких данных. Однако есть технические причины, по которым C++ требует, чтобы любой автономный объект должен иметь ненулевой размер, поэтому для следующих объявлений:

class Empty {}; // не имеет данных, поэтому объекты

// не должны занимать памяти

class HoldsAnInt { // память, по идее, нужна только для int

private:

int x;

Empty e; // не должен занимать память

};

оказывается, что sizeof(HoldsAnlnt) > sizeof(int); член данных Empty занимает какую-то память. Для большинства компиляторов sizeof(Empty) будет равно 1, потому что требование C++ о том, что не должно быть объектов нулевой длины, обычно удовлетворяется молчаливой вставкой одного байта (char) в такой «пустой» объект. Однако из-за необходимости выравнивания (см. правило 50) компилятор может оказаться вынужден дополнить классы, подобные HoldsAnInt, поэтому вполне вероятно, что размер объектов HoldsAnInt увеличится больше чем на char, скорее всего, речь может идти о росте на размер int. На всех компиляторах, где я тестировал, происходило именно так.

Возможно, вы обратили внимание, что, говоря о ненулевом размере, я упомянул «автономные» объекты. Это ограничение не относится к тем частям производного класса, которые унаследованы от базового, поскольку они уже не считаются «автономными». Если вы наследуете Empty вместо того, чтоб включать его,

class HoldsAnInt: private Empty {

private:

int x;

};

то почти наверняка обнаружите, что sizeof(HoldsAnlnt) = sizeof(int). Это явление известно как оптимизация пустого базового класса (empty base optimization – EBO), и оно реализовано во всех компиляторах, которые я тестировал. Если вы разрабатываете библиотеку, пользователям которой небезразлично потребление памяти, то знать о EBO будет полезно. Но имейте в виду, что в общем случае оптимизация EBO применяется только для одиночного наследования. Действующие в C++ правила размещения объектов в памяти обычно делают невозможной такую оптимизацию, если производный класс имеет более одного базового.

На практике «пустые» классы на самом деле не совсем пусты. Хотя они и не содержат нестатических данных-членов, но часто включают typedefbi, перечисления, статические члены-данные, или невиртуальные функции. В библиотеке STL есть много технически пустых классов, которые содержат полезные члены (обычно typedef). К их числу относятся, в частности, базовые классы unary_function и binary_function, которым обычно наследуют классы определяемых пользователями функциональных объектов. Благодаря широкому распространению реализаций EBO такое наследование редко увеличивает размеры производных классов.

Но вернемся к основам. Большинство классов не пусты, поэтому EBO редко может служить оправданием закрытому наследованию. Более того, в большинстве случаев наследование выражает отношение «является», а это признак открытого, а не закрытого наследования. Как композиция, так и закрытое наследование выражают отношение «реализован посредством», но композиция проще для понимания, поэтому использует ее всюду, где возможно.

Закрытое наследование чаще всего оказывается разумной стратегией проектирования, когда вы имеете дело с двумя классами, не связанными отношением «является», причем один из них либо нуждается в доступе к защищенным членам другого, либо должен переопределять одну или несколько виртуальных функций последнего. И даже в этом случае мы видели, что сочетание открытого наследования и композиции часто помогают реализовать желаемое поведение, хотя и ценой некоторого усложнения. Говоря о продумывании подхода к применению закрытого наследования, я имею в виду, что прибегать к нему стоит лишь тогда, когда рассмотрены все другие альтернативы и выяснилось, что это лучший способ выразить отношение между двумя классами в вашей программе.

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

• Закрытое наследование означает «реализован посредством». Обычно этот вариант хуже композиции, но все же приобретает смысл, когда производный класс нуждается в доступе к защищенным членам базового класса или должен переопределять унаследованные виртуальные функции.

• В отличие от композиции, закрытое наследование позволяет проводить оптимизацию пустого базового класса. Это может оказаться важным для разработчиков библиотек, которые стремятся минимизировать размеры объектов.

Поделиться:
Популярные книги

Вечная Война. Книга VIII

Винокуров Юрий
8. Вечная Война
Фантастика:
боевая фантастика
юмористическая фантастика
космическая фантастика
7.09
рейтинг книги
Вечная Война. Книга VIII

Идеальный мир для Лекаря 16

Сапфир Олег
16. Лекарь
Фантастика:
боевая фантастика
юмористическая фантастика
аниме
5.00
рейтинг книги
Идеальный мир для Лекаря 16

Отмороженный

Гарцевич Евгений Александрович
1. Отмороженный
Фантастика:
боевая фантастика
рпг
5.00
рейтинг книги
Отмороженный

Сфирот

Прокофьев Роман Юрьевич
8. Стеллар
Фантастика:
боевая фантастика
рпг
6.92
рейтинг книги
Сфирот

Снегурка для опера Морозова

Бигси Анна
4. Опасная работа
Любовные романы:
современные любовные романы
5.00
рейтинг книги
Снегурка для опера Морозова

Волк: лихие 90-е

Киров Никита
1. Волков
Фантастика:
попаданцы
альтернативная история
5.00
рейтинг книги
Волк: лихие 90-е

Газлайтер. Том 8

Володин Григорий
8. История Телепата
Фантастика:
попаданцы
альтернативная история
аниме
5.00
рейтинг книги
Газлайтер. Том 8

Враг из прошлого тысячелетия

Еслер Андрей
4. Соприкосновение миров
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Враг из прошлого тысячелетия

Кодекс Охотника. Книга XVII

Винокуров Юрий
17. Кодекс Охотника
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Кодекс Охотника. Книга XVII

Неудержимый. Книга XVIII

Боярский Андрей
18. Неудержимый
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Неудержимый. Книга XVIII

Диверсант

Вайс Александр
2. Фронтир
Фантастика:
боевая фантастика
космическая фантастика
5.00
рейтинг книги
Диверсант

Королевская Академия Магии. Неестественный Отбор

Самсонова Наталья
Любовные романы:
любовно-фантастические романы
8.22
рейтинг книги
Королевская Академия Магии. Неестественный Отбор

Неудержимый. Книга XI

Боярский Андрей
11. Неудержимый
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Неудержимый. Книга XI

Бестужев. Служба Государевой Безопасности. Книга вторая

Измайлов Сергей
2. Граф Бестужев
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Бестужев. Служба Государевой Безопасности. Книга вторая