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

Правило 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!= будет корректным.

(Кстати говоря: мы не принимаем во внимание возможность перегрузки operator&&, в результате которой семантика приведенного выражения может стать уже не конъюнкцией, а чем-то совершенно иным.)

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

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

Мало что можно сказать об ограничениях, налагаемых функциями size, operator>, operator&& или operator!=, но идентифицировать ограничения всего выражения в целом легко. Условная часть предложения if должна быть булевским выражением, поэтому независимо от конкретных типов результат вычисления (w.size() > 10 && w!= someNastyWidget) должен быть совместим с bool. Это та часть неявного интерфейса, которую шаблон doProcessing налагает на свой параметр типа T. Кроме того, для работы doProcessing необходимо, чтобы интерфейс типа T допускал обращения к конструктору копирования, а также функциям normalize, size и swap.

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

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

• И классы, и шаблоны поддерживают интерфейсы и полиморфизм.

• Для классов интерфейсы определены явно и включают главным образом сигнатуры функций. Полиморфизм проявляется во время исполнения – через виртуальные функции.

• Для параметров шаблонов интерфейсы неявны и основаны на корректных выражениях. Полиморфизм проявляется во время компиляции – через конкретизацию и разрешение перегрузки функций.

Данный текст является ознакомительным фрагментом.



Поделитесь на страничке

Следующая глава >

Похожие главы из других книг:

Работа с данными, связанными с процессорами, на этапе компиляции

Из книги автора

Работа с данными, связанными с процессорами, на этапе компиляции Описать переменную, которая связана с определенным процессором, на этапе компиляции можно достаточно просто следующим образом.DEFINE_PER_CPU(type, name);Это описание создает переменную типа type с именем name, которая


6.4.2. Неявные критерии

Из книги автора

6.4.2. Неявные критерии В этом разделе мы рассмотрим неявные критерии, точнее, те критерии, которые подгружаются неявно и становятся доступны, например при указании критерия –protocol tcp. На сегодняшний день существует три автоматически подгружаемых расширения, это TCP


Полиморфизм на этапе выполнения

Из книги автора

Полиморфизм на этапе выполнения Управление реализациями классов с использованием абстрактных базовых классов как интерфейсов открывает целый мир новых возможностей в терминах того, что может случиться на этапе выполнения. Напомним, что DLL FastString экспортирует только


1.6.4. Правило разделения: следует отделять политику от механизма и интерфейсы от основных модулей

Из книги автора

1.6.4. Правило разделения: следует отделять политику от механизма и интерфейсы от основных модулей В разделе "Что в Unix делается неверно" отмечалось, что разработчики системы X Window приняли основное решение о реализации "механизма, а не политики". Такой подход был направлен на


Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно

Из книги автора

Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно C++ изобилует интерфейсами. Интерфейсы функций. Интерфейсы классов. Интерфейсы шаблонов. Каждый интерфейс – это средство, посредством которого пользователь


Правило 31: Уменьшайте зависимости файлов при компиляции

Из книги автора

Правило 31: Уменьшайте зависимости файлов при компиляции Рассмотрим самую обыкновенную ситуацию. Вы открываете свою программу на C++ и вносите незначительные изменения в реализацию класса. Заметьте, не в интерфейс класса, а просто в реализацию – только в закрытые члены.


Правило 49: Разберитесь в поведении обработчика new

Из книги автора

Правило 49: Разберитесь в поведении обработчика new Когда оператор new не может удовлетворить запрос на выделение памяти, он возбуждает исключение. Когда-то он возвращал нулевой указатель, и некоторые старые компиляторы все еще так и поступают. Вы можете столкнуться с таким


1.6.4. Правило разделения: следует отделять политику от механизма и интерфейсы от основных модулей

Из книги автора

1.6.4. Правило разделения: следует отделять политику от механизма и интерфейсы от основных модулей В разделе "Что в Unix делается неверно" отмечалось, что разработчики системы X Window приняли основное решение о реализации "механизма, а не политики". Такой подход был направлен на


1.1.3. Полиморфизм

Из книги автора

1.1.3. Полиморфизм Термин «полиморфизм», наверное, вызывает самые жаркие семантические споры. Каждый знает, что это такое, но все понимают его по-разному. (Не так давно вопрос «Что такое полиморфизм?» стал популярным во время собеседования при поступлении на работу. Если его


5.15. Явные и неявные преобразования чисел

Из книги автора

5.15. Явные и неявные преобразования чисел Программисты, только начинающие изучать Ruby, часто удивляются, зачем нужны два метода to_i и to_int (и аналогичные им to_f и to_flt). В общем случае метод с коротким именем применяется для явных преобразований, а метод с длинным именем — для


Шаг 20 - Временные объекты. Неявные вызовы конструкторов и их подавление.

Из книги автора

Шаг 20 - Временные объекты. Неявные вызовы конструкторов и их подавление. Не удается углубиться в какую-либо тему. Приходится касаться по верхам, потом переключаться на что-то другое. С другой стороны, может это и правильно, часто достаточно только знать, что есть ТАКОЕ


Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref

Из книги автора

Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref Загадочные функции ptr_fun/mem_fun/mem_fun_ref часто вызывают недоумение. В одних случаях их присутствие обязательно, в других они не нужны... но что же они все-таки делают? На первый взгляд кажется, что они бессмысленно


Разберитесь с долгами

Из книги автора

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


Полиморфизм

Из книги автора

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


Полиморфизм

Из книги автора

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