Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

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

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

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 49 50 51 52 53 54 55 56 57 ... 73
Перейти на страницу:

// строки-разделители

В нотации UML это решение выглядит так:

Рассмотренный пример показывает, что множественное наследование может быть и удобным, и понятным.

Замечу, что множественное наследование – просто еще один инструмент в объектно-ориентированном инструментарии. По сравнению с одиночным наследованием оно несколько труднее для понимания и применения, поэтому если вы можете спроектировать программу с одним лишь одиночным наследованием, который более или менее эквивалентен варианту с множественным наследованием, то, скорее всего, предпочтение следует отдать первому подходу. Если вам кажется, что единственно возможный вариант дизайна требует применения множественного наследования, то рекомендую как следует подумать – почти наверняка найдется способ обойтись одиночным. В то же время иногда множественное наследование – это самый ясный, простой для сопровождения и разумный способ достижения цели. В таких случаях не бойтесь применять его. Просто делайте это, тщательно обдумав все последствия.

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

• Множественное наследование сложнее одиночного. Оно может привести к неоднозначности и необходимости применять виртуальное наследование.

• Цена виртуального наследования – дополнительные затраты памяти, снижение быстродействия и усложнение операций инициализации и присваивания. На практике его разумно применять, когда виртуальные базовые классы не содержат данных.

• Множественное наследование вполне законно. Один из сценариев включает комбинацию открытого наследования интерфейсного класса и закрытого наследования класса, помогающего в реализации.

Глава 7

Шаблоны и обобщенное программирование

Изначально шаблоны в C++ появились для того, чтобы можно было реализовать безопасные относительно типов контейнеры: vector, list, map и им подобные. Однако по мере обретения опыта работы с шаблонами стали обнаруживаться все новые и новые способы их применения. Контейнеры были хороши сами по себе, но обобщенное программирование – возможность писать код, не зависящий от типа объектов, которыми он манипулирует, – оказалось еще лучше. Примерами такого программирования являются алгоритмы STL, такие как for_each, find и merge. В конечном итоге выяснилось, что механизм шаблонов C++ сам по себе является машиной Тьюринга: он может быть использован для вычисления любых вычисляемых значений. Это привело к метапрограммированию шаблонов: созданию программ, которые исполняются внутри компилятора C++ и завершают свою работу вместе с окончанием компиляции. В наши дни контейнеры – это лишь малая толика того, на что способны шаблоны C++. Но, несмотря на огромное разнообразие применений, в основе программирования шаблонов лежит небольшое число базовых идей. Именно им и посвящена настоящая глава.

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

Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции

В мире объектно-ориентированного программирования преобладают явные интерфейсы и полиморфизм на этапе исполнения. Например, рассмотрим следующий (бессмысленный) класс:

class Widget {

public:

Widget();

virtual ~Widget();

virtual std::size_t size() const;

virtual void normalize();

void swap(Widget& other); // см. правило 25

...

};

и столь же бессмысленную функцию:

void doProcessing(Widget& w)

{

if(w.size() > 10 && w != someNastyWidget) {

Widget temp(w);

temp.normalize();

temp.swap(w);

}

}

Вот что мы можем сказать о переменной w в функции doProcessing:

• Поскольку объявлено, что переменная w имеет тип Widget, то w должна поддерживать интерфейс Widget. Мы можем найти точное описание этого интерфейса в исходном коде (например, в заголовочном файле для Widget), поэтому я называю его явным интерфейсом – явно присутствующим в исходном коде программы.

• Поскольку некоторые из функций-членов Widget являются виртуальными, то вызовы этих функций посредством w являются примером полиморфизма времени исполнения: конкретная функция, которую нужно вызвать, определяется во время исполнения на основании динамического типа w (см. правило 37).

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

template<typename T>

void doProcessing(T& w)

{

if(w.size() > 10 && w != someNastyWidget) {

T temp(w);

temp.normalize();

temp.swap(w);

}

}

Что теперь можно сказать о переменной w в шаблоне doProcessing?

• Теперь интерфейс, который должна поддерживать переменная w, определяется операциями, выполняемыми над w в шаблоне. В данном случае видно, что тип переменной w (а именно T) должен поддерживать функции-члены size, normalize и swap; конструктор копирования (для создания temp), а также операцию сравнения на равенство (для сравнения с someNastyWidget). Скоро мы увидим, что это не совсем точно, но на данный момент достаточно. Важно, что набор выражений, которые должны быть корректны для того, чтобы шаблон компилировался, представляет собой неявный интерфейс, который тип T должен поддерживать.

• Для успешного вызова функций, в которых участвует w, таких как operator> и operator!=, может потребоваться конкретизировать шаблон. Такая конкретизация происходит во время компиляции. Поскольку конкретизация шаблонов функций с разными шаблонными параметрами приводит к вызову разных функций, мы называем это полиморфизмом времени компиляции.

Даже если вы никогда не пользовались шаблонами, разница между полиморфизмом времени исполнения и полиморфизмом времени компиляции должна быть вам знакома, поскольку она напоминает разницу между процедурой определения того, какую из перегруженных функций вызывать (это происходит во время компиляции) и динамическим связыванием при вызове виртуальных функций (которое происходит во время исполнения). Однако разница между явными и неявными интерфейсами – понятие, характерное только для шаблонов, поэтому остановимся на нем более подробно.

Явные интерфейсы обычно состоят из сигнатур функций, то есть имен функций, типов параметров, возвращаемого значения и т. д. Так, открытый интерфейс класса Widget

class Widget {

public:

Widget();

virtual ~Widget();

virtual std::size_t size() const;

virtual void normalize();

void swap(Widget& other);

};

состоит из конструктора, деструктора и функций size, normalize и swap вместе с типами их параметров, возвращаемых значений и признаков константности (интерфейс также включает генерируемые компилятором конструктор копирования и оператор присваивания – см. правило 5). В состав интерфейса могут входить также typedefbi.

Неявный интерфейс несколько отличается. Он не базируется на сигнатурах функций. Вместо этого он состоит из корректных выражений. Посмотрим еще раз на условия в начале шаблона doProcessing:

template<typename T>

void doProcessing(T& w)

{

if(w.size() > 10 && w != someNastyWidget) {

...

Неявному интерфейсу T (типа переменной w) присущи следующие ограничения:

• Он должен предоставлять функцию-член по имени size, которая возвращает целое значение.

• Он должен поддерживать функцию operator!=, которая сравнивает два объекта типа T. (Здесь мы предполагаем, что someNastyWidget имеет тип T.)

Благодаря возможности перегрузки операторов ни одно из этих требований не должно удовлетворяться в обязательном порядке. Да, T должен поддерживать функцию-член size, хотя стоит упомянуть, что эта функция может быть унаследована от базового класса. Но эта функция не обязана возвращать целочисленный тип. Она даже может вообще не возвращать числовой тип. Вообще-то она даже не обязана возвращать тип, для которого определен operator>! Нужно лишь, чтобы она возвращала объект такого типа X, что может быть вызван operator>, которому передаются параметры типа X и int (потому что 10 имеет тип int). При этом функция operator> может и не принимать параметра, тип которого в точности совпадает с X; достаточно, если тип ее параметра Y может быть неявно преобразован к типу X!

Аналогично не требуется, чтобы тип T поддерживал operator!=, достаточно будет и того, чтобы функция operator!= принимала один объект типа X и один объект типа Y. Если T можно преобразовать в X, а someNastyWidget в Y, то вызов operator!= будет корректным.

1 ... 49 50 51 52 53 54 55 56 57 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии