Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Конечно, пользователи интерфейсных классов должны иметь способ создавать новые объекты. Обычно они делают это, вызывая функцию, играющую роль конструктора для производных классов, экземпляры которых необходимо создать. Такие функции часто называют функциями-фабриками (см. правило 13), или виртуальными конструкторами. Они возвращают указатели (и лучше бы интеллектуальные, см. правило 18) на динамически распределенные объекты, которые поддерживают интерфейс интерфейсного класса. Нередко подобные функции объявляют как статические внутри интерфейсного класса:
а используют так:
Разумеется, где-то должны быть определены конкретные классы, поддерживающие интерфейс такого интерфейсного класса, и вызваны реальные конструкторы. Все это происходит «за кулисами», внутри файлов, содержащих реализацию виртуальных конструкторов. Например, интерфейсный класс Person может иметь конкретный производный класс RealPerson, предоставляющий реализацию унаследованных виртуальных функций:
Имея класс RealPerson, очень легко написать Person::create:
Более реалистическая реализация Person::create должна создавать разные типы объектов классов-наследников, в зависимости, например, от дополнительных параметров функции, данных, прочитанных из файла или базы данных, переменных окружения и т. п.
RealPerson демонстрирует один из двух наиболее распространенных механизмов реализации интерфейсных классов: он наследует спецификации своего интерфейса от интерфейсного класса Person, а затем реализует функции этого интерфейса. Второй способ реализации интерфейсного класса предполагает использование множественного наследования (см. правило 40).
Итак, классы-дескрипторы и интерфейсные классы отделяют интерфейс от реализации, уменьшая тем самым зависимости между файлами на этапе компиляции. Теперь, я уверен, вы ждете примечания мелким шрифтом: «Во сколько обойдется этот хитрый фокус?» Цена вполне обычная в мире программирования: некоторое уменьшение скорости выполнения программы плюс дополнительный расход памяти на каждый объект.
Применительно к классам-дескрипторам функции-члены должны использовать указатель на реализацию (pImpl), чтобы добраться до данных самого объекта. Для каждого обращения это добавляет один уровень косвенной адресации. Кроме того, к объему памяти, необходимому для хранения каждого объекта, нужно добавить размер указателя. И наконец, указатель на реализацию должен быть инициализирован (в конструкторе класса-дескриптора), чтобы он указывал на динамически распределенный объект реализации; следовательно, вы навлекаете на себя еще и накладные расходы, сопровождающие динамическое выделение памяти и последующее ее освобождение, а также возможность возникновения исключений bad_alloc (из-за недостатка памяти).
Для интерфейсных классов каждый вызов функции будет виртуальным, поэтому всякий раз вы платите за косвенный переход (см. правило 7). Кроме того, классы, производные от интерфейсного класса, должны содержать указатель на таблицу виртуальных функций (и снова см. правило 7). Этот указатель может увеличить объем памяти, необходимый для хранения объекта, в зависимости от того, является ли интерфейсный класс единственным источником виртуальных функций для объекта.
И наконец, ни классы-дескрипторы, ни интерфейсные классы не могут извлечь выгоду из использования встроенных функций. В правиле 30 объяснено, почему тела потенциально встраиваемых функций должны быть в заголовочных файлах, но классы-дескрипторы и интерфейсные классы специально предназначены для того, чтобы скрыть такие детали реализации, как тело функций.
Однако было бы серьезной ошибкой отказываться от классов-дескрипторов и интерфейсных классов только потому, что их использование связано с дополнительными расходами. То же самое можно сказать и о виртуальных функциях, но вы ведь не отказываетесь от их применения. (В противном случае вы читаете не ту книгу.) Рассмотрите возможность использования предлагаемых приемов по мере эволюции ваших программ. Применяйте классы-дескрипторы и интерфейсные классы в процессе разработки, чтобы уменьшить влияние изменений в реализации на пользователей. Если вы можете показать, что различие в скорости и/или размере программы настолько существенно, что во имя повышения эффективности оно оправдывает увеличение зависимости между классами, то на конечной стадии реализации заменяйте их конкретными классами.