Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Бьярн Страуструп - Язык программирования C++

.pdf
Скачиваний:
1042
Добавлен:
17.03.2018
Размер:
3.01 Mб
Скачать

Бьерн Страуструп.

Язык программирования С++

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

class compX_details { // специфика реализации compX public:

static void f(T2*, const char*); static T3 v;

// ...

};

Описание compX_details будет использовать только создатель класса, остальные не должны включать его в свои программы.

В компоненте конечно может быть много классов, не предназначенных для общего пользования. Если их имена тоже рассчитаны только на локальное использование, то их также можно "спрятать" внутри классов, содержащих специфику реализации:

class compX_details { // специфика реализации compX.

public:

// ...

class widget {

// ...

};

// ...

};

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

class Car { class Wheel {

// ...

};

Wheel flw, frw, rlw, rrw; // ...

};

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

class Wheel {

// ...

};

class Car {

Wheel flw, frw, rlw, rrw; // ...

};

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

331

Бьерн Страуструп.

Язык программирования С++

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

Другим средством построения компонента и представления его пользователю служит иерархия. Тогда базовый класс используется как хранилище общих данных и функций. Таким способом устраняется проблема, связанная с глобальными данными и функциями, предназначенными для реализации общих запросов классов данного компонента. С другой стороны, при таком решении классы компонента становятся слишком связанными друг с другом, а пользователь попадает в зависимость от всех базовых классов тех компонентов, которые ему действительно нужны. Здесь также проявляется тенденция к тому, что члены, представляющие "полезные" функции и данные "всплывают" к базовому классу, так что при слишком большой иерархии классов проблемы с глобальными данными и функциями проявятся уже в рамках этой иерархии. Вероятнее всего, это произойдет для иерархии с одним корнем, а для борьбы с этим явлением можно применять виртуальные базовые классы ($$6.5.4). Иногда лучше выбрать иерархию для представления компонента, а иногда нет. Как всегда сделать выбор предстоит разработчику.

12.4 Интерфейсы и реализации

Идеальный интерфейс должен

-представлять полное и согласованное множество понятий для пользователя,

-быть согласованным для всех частей компонента,

-скрывать специфику реализации от пользователя,

-допускать несколько реализаций,

-иметь статическую систему типов,

-определяться с помощью типов из области приложения,

-зависеть от других интерфейсов лишь частично и вполне определенным образом.

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

class X {

// пример плохого определения интерфейса

Ya;

Zb;

public:

void f(const char* ...); void g(int[],int);

void set_a(Y&); Y& get_a();

};

Вэтом интерфейсе содержится ряд потенциальных проблем:

--Типы Y и Z используются так, что определения Y и Z должны быть известны во время трансляции.

-У функции X::f может быть произвольное число параметров неизвестного типа (возможно, они каким-то образом контролируются "строкой формата", которая передается в качестве первого параметра).

-Функция X::g имеет параметр типа int[]. Возможно это нормально, но обычно это свидетельствует о том, что определение слишком низкого уровня абстракции. Массив целых не является достаточным определением, так как неизвестно из скольких он может состоять элементов.

-Функции set_a() и get_a(), по всей видимости, раскрывают представление объектов класса X, разрешая прямой доступ к X::a.

332

Бьерн Страуструп.

Язык программирования С++

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

Язык С++ раскрывает представление класса как часть интерфейса. Это представление может быть скрытым (с помощью private или protected), но обязательно доступным транслятору, чтобы он мог разместить автоматические (локальные) переменные, сделать подстановку тела функции и т.д. Отрицательным следствием этого является то, что использование типов классов в представлении класса может привести к возникновению нежелательных зависимостей. Приведет ли использование членов типа Y и Z к проблемам, зависит от того, каковы в действительности типы Y и Z. Если это достаточно простые типы, наподобие complex или String, то их использование будет вполне допустимым в большинстве случаев. Такие типы можно считать устойчивыми, и необходимость включать определения их классов будет вполне допустимой нагрузкой для транслятора. Если же Y и Z сами являются классами интерфейса большого компонента (например, типа графической системы или системы обеспечения банковских счетов), то прямую зависимость от них можно считать неразумной. В таких случаях предпочтительнее использовать член, являющийся указателем или ссылкой:

class X { Y* a;

Z& b; // ...

};

При этом способе определение X отделяется от определений Y и Z, т.е. теперь определение X зависит только от имен Y и Z. Реализация X, конечно, будет по-прежнему зависеть от определений Y и Z, но это уже не будет оказывать неблагоприятного влияния на пользователей X.

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

Отметим, что класс определяет три интерфейса:

class X {

private:

// доступно только для членов и друзей protected:

//доступно только для членов и друзей, а также

//для членов и друзей производных классов

public:

// общедоступно

};

Члены должны образовывать самый ограниченный из возможных интерфейсов. Иными словами, член должен быть описан как private, если нет причин для более широкого доступа к нему; если же таковые есть, то член должен быть описан как protected, если нет дополнительных причин задать его как public. В большинстве случаев плохо задавать все данные, представляемые членами, как public. Функции и классы, образующие общий интерфейс, должны быть спроектированы таким образом, чтобы представление класса совпадало с его ролью в проекте как средства представления понятий. Напомним, что друзья являются частью общего интерфейса.

Отметим, что абстрактные классы можно использовать для представления понятия упрятывания более высокого уровня ($$1.4.6, $$6.3, $$13.3).

333

Бьерн Страуструп.

Язык программирования С++

12.5 Свод правил

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

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

Нацеливайте пользователя на применение абстракции данных и объектно-ориентированного программирования.

-Постепенно переходите на новые методы, не спешите.

-Используйте возможности С++ и методы объектно-ориентированного программирования только по мере надобности.

Добейтесь соответствия стиля проекта и программы.

Концентрируйте внимание на проектировании компонента.

Используйте классы для представления понятий.

-Используйте общее наследование для представления отношений "есть".

-Используйте принадлежность для представления отношений "имеет".

-Убедитесь, что отношения использования понятны, не образуют циклов, и что число их минимально.

-Активно ищите общность среди понятий области приложения и реализации, и возникающие в результате более общие понятия представляйте как базовые классы.

Определяйте интерфейс так, чтобы открывать минимальное количество требуемой информации:

-Используйте, всюду где это можно, частные данные и функции-члены.

-Используйте описания public или protected, чтобы отличить запросы разработчика производных классов от запросов обычных пользователей.

-Сведите к минимуму зависимости одного интерфейса от других.

-Поддерживайте строгую типизацию интерфейсов.

-Задавайте интерфейсы в терминах типов из области приложения.

Дополнительные правила можно найти $$11.5.

334

Бьерн Страуструп.

Язык программирования С++

ГЛАВА 13. ПРОЕКТИРОВАНИЕ БИБЛИОТЕК

Проект библиотеки - это проект языка, (фольклор фирмы Bell Laboratories)

... и наоборот. - А. Кениг

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

13.1 Введение

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

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

Понятие класса С++ может использоваться самыми разными способами, поэтому разнообразие стилей программирования может привести к беспорядку. Хорошая библиотека для сведения такого беспорядка к минимуму обеспечивает согласованный стиль программирования, или, по крайней мере, несколько таких стилей. Этот подход делает библиотеку более "предсказуемой", а значит позволяет легче и быстрее изучить ее и правильно использовать. Далее описываются пять "архитипичных" классов, и обсуждаются присущие им сильные и слабые стороны: конкретные типы ($$13.2), абстрактные типы ($$13.3), узловые классы ($$13.4), интерфейсные классы ($$13.8), управляющие классы ($$13.9). Все эти виды классов относятся к области понятий, а не являются конструкциями языка. Каждое понятие воплощается с помощью основной конструкции - класса. В идеале надо иметь минимальный набор простых и ортогональных видов классов, исходя из которого можно построить любой полезный и разумно-определенный класс. Идеал нами не достигнут и, возможно, недостижим вообще. Важно понять, что любой из перечисленных видов классов играет свою роль при проектировании библиотеки и, если рассчитывать на общее применение, никакой из них не является по своей сути лучше других.

335

Бьерн Страуструп.

Язык программирования С++

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

($$13.7).

Здесь рассматриваются прежде всего классы, относящиеся строго к одному из перечисленных видов, хотя, конечно, используются классы и гибридного вида. Но использование класса гибридного вида должно быть результатом осознанного решения, возникшего при оценке плюсов и минусов различных видов, а не результатом пагубного стремления уклониться от выбора вида класса (слишком часто "отложим пока выбор" означает просто нежелание думать). Неискушенным разработчикам библиотеки лучше всего держаться подальше от классов гибридного вида. Им можно посоветовать следовать стилю программирования той из существующих библиотек, которая обладает возможностями, необходимыми для проектируемой библиотеки. Отважиться на создание библиотеки общего назначения может только искушенный программист, и каждый создатель библиотеки впоследствии будет "осужден" на долгие годы использования, документирования и сопровождения своего собственного создания.

В языке С++ используются статические типы. Однако, иногда возникает необходимость в дополнение к возможностям, непосредственно предоставляемым виртуальными функциями, получать динамическую информацию о типах. Как это сделать, описано в $$13.5. Наконец, перед всякой нетривиальной библиотекой встает задача управления памятью. Приемы ее решения рассматриваются в $$13.10. Естественно, в этой главе невозможно рассмотреть все методы, оказавшиеся полезными при создании библиотеки. Поэтому можно отослать к другим местам книги, где рассмотрены следующие вопросы: работа с ошибками и устойчивость к ошибкам ($$9.8), использование функциональных объектов и обратных вызовов ($$10.4.2 и $$9.4.3) , использование шаблонов типа для построения классов ($$8.4).

Многие темы этой главы связаны с классами, являющимися контейнерами, (например, массивы и списки). Конечно, такие контейнерные классы являются шаблонами типа (как было сказано в $$1.и 4.3 $$8). Но здесь для упрощения изложения в примерах используются классы, содержащие указатели на объекты типа класс. Чтобы получить настоящую программу, надо использовать шаблоны типа, как показано в главе 8.

13.2 Конкретные типы

Такие классы как vector ($$1.4), Slist ($$8.3), date ($$5.2.2) и complex ($$7.3) являются конкретными в том смысле, что каждый из них представляет довольно простое понятие и обладает необходимым набором операций. Имеется взаимнооднозначное соответствие между интерфейсом класса и его реализацией. Ни один из них (изначально) не предназначался в качестве базового для получения производных классов. Обычно в иерархии классов конкретные типы стоят особняком. Каждый конкретный тип можно понять изолированно, вне связи с другими классами. Если реализация конкретного типа удачна, то работающие с ним программы сравнимы по размеру и скорости со сделанными вручную программами, в которых используется некоторая специальная версия общего понятия. Далее, если произошло значительное изменение реализации, обычно модифицируется и интерфейс, чтобы отразить эти изменения. Интерфейс, по своей сути, обязан показать какие изменения оказались существенными в данном контексте. Интерфейс более высокого уровня оставляет больше свободы для изменения реализации, но может ухудшить характеристики программы. Более того, хорошая реализация зависит только от минимального числа действительно существенных классов. Любой из этих классов можно использовать без накладных расходов, возникающих на этапе трансляции или выполнения, и вызванных приспособлением к другим, "сходным" классам программы. Подводя итог, можно указать такие условия, которым должен удовлетворять конкретный тип:

[1] полностью отражать данное понятие и метод его реализации;

[2]с помощью подстановок и операций, полностью использующих полезные свойства понятия и его реализации, обеспечивать эффективность по скорости и памяти, сравнимую с "ручными программами";

[3]иметь минимальную зависимость от других классов;

[4]быть понятным и полезным даже изолированно.

Все это должно привести к тесной связи между пользователем и программой, реализующей конкретный тип. Если в реализации произошли изменения, программу пользователя придется перетранслировать,

336

Бьерн Страуструп. Язык программирования С++

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

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

Название "конкретный тип" (CDT - concrete data type, т.е. конкретный тип данных) , было выбрано по контрасту с термином "абстрактный тип" (ADT - abstract data type, т.е. абстрактный тип данных). Отношения между CDT и ADT обсуждаются в $$13.3.

Существенно, что конкретные типы не предназначены для явного выражения некоторой общности. Так, типы slist и vector можно использовать в качестве альтернативной реализации понятия множества, но в языке это явно не отражается. Поэтому, если программист хочет работать с множеством, использует конкретные типы и не имеет определения класса множество, то он должен выбирать между типами slist и vector. Тогда программа записывается в терминах выбранного класса, скажем, slist, и если потом предпочтут использовать другой класс, программу придется переписывать.

Это потенциальное неудобство компенсируется наличием всех "естественных" для данного класса операций, например таких, как индексация для массива и удаление элемента для списка. Эти операции представлены в оптимальном варианте, без "неестественных" операций типа индексации списка или удаления массива, что могло бы вызвать путаницу. Приведем пример:

void my(slist& sl)

{

for (T* p = sl.first(); p; p = sl.next())

{

// мой код

}

// ...

}

void your(vector& v)

{

for (int i = 0; i<v.size(); i++)

{

// ваш код

}

// ...

}

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

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

337

Бьерн Страуструп.

Язык программирования С++

void user()

 

{

 

slist sl;

 

vector v(100);

 

my(sl);

 

your(v);

// ошибка: несоответствие типа

my(v);

your(sl);

// ошибка: несоответствие типа

}

 

Чтобы компенсировать жесткость этого требования, разработчик некоторой полезной функции должен предоставить несколько ее версий, чтобы у пользователя был выбор:

void my(slist&);

 

void my(vector&);

 

void your(slist&);

 

void your(vector&);

 

void user()

 

{

 

slist sl;

 

vector v(100);

 

my(sl);

 

your(v);

// теперь нормально: вызов my(vector&)

my(v);

your(sl);

// теперь нормально: вызов your(slist&)

}

 

Поскольку тело функции существенно зависит от типа ее параметра, надо написать каждую версию функций my() и your() независимо друг от друга, что может быть хлопотно.

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

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

13.3 Абстрактные типы

Самый простой способ ослабить связь между пользователем класса и его создателем, а также между программами, в которых объекты создаются, и программами, в которых они используются, состоит в введении понятия абстрактных базовых классов. Эти классы представляют интерфейс со множеством реализаций одного понятия. Рассмотрим класс set, содержащий множество объектов типа T:

class set { public:

virtual void insert(T*) = 0; virtual void remove(T*) = 0;

338

Бьерн Страуструп.

Язык программирования С++

virtual int is_member(T*) = 0; virtual T* first() = 0; virtual T* next() = 0; virtual ~set() { }

};

Этот класс определяет интерфейс с произвольным множеством (set), опираясь на встроенное понятие итерации по элементам множества. Здесь типично отсутствие конструктора и наличие виртуального деструктора, см. также $$6.7. Рассмотрим пример:

class slist_set : public set, private slist { slink* current_elem;

public:

void insert(T*); void remove(T*); int is_member(T*); virtual T* first(); virtual T* next();

slist_set() : slist(), current_elem(0) { }

};

class vector_set : public set, private vector { int current_index;

public:

void insert(T*); void remove(T*); int is_member(T*);

T* first() { current_index = 0; return next(); } T* next();

vector_set(int initial_size)

: array(initial_size), current_index(0) { }

};

Реализация конкретного типа используется как частный базовый класс, а не член класса. Это сделано и для удобства записи, и потому, что некоторые конкретные типы могут иметь защищенный интерфейс с целью предоставить более прямой доступ к своим членам из производных классов. Кроме того, подобным образом в реализации могут использоваться некоторые классы, которые имеют виртуальные функции и не являются конкретными типами. Только с помощью образования производных классов можно в новом классе изящно переопределить (подавить) виртуальную функцию класса реализации. Интерфейс определяется абстрактным классом.

Теперь пользователь может записать свои функции из $$13.2 таким образом:

void my(set& s)

{

for (T* p = s.first(); p; p = s.next())

{

// мой код

}

// ...

}

void your(set& s)

{

for (T* p = s.first(); p; p = s.next())

{

// ваш код

}

// ...

}

Стало очевидным сходство между двумя функциями, и теперь достаточно иметь только одну версию

339

Бьерн Страуструп.

Язык программирования С++

для каждой из функций my() или your(), поскольку для общения с slist_set и vector_set обе версии используют интерфейс, определяемый классом set:

void user()

{

slist_set sl; vector_set v(100); my(sl);

your(v);

my(v);

your(sl);

}

Более того, создатели функций my() и your() не обязаны знать описаний классов slist_set и vector_set, и функции my() и your() никоим образом не зависят от этих описаний. Их не надо перетранслировать или как-то изменять, ни если изменились классы slist_set или vector_set ни даже, если предложена новая реализация этих классов. Изменения отражаются лишь на функциях, которые непосредственно используют эти классы, допустим vector_set. В частности, можно воспользоваться традиционным применением заголовочных файлов и включить в программы с функциями my() или your() файл определений set.h, а не файлы slist_set.h или vector_set.h.

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

Такое разделение интерфейса и его реализаций предполагает недоступность операций, являющихся "естественными" для какой-то одной реализации, но не достаточно общими, чтобы войти в интерфейс. Например, поскольку в произвольном множестве нет упорядоченности, в интерфейс set нельзя включать операцию индексирования, даже если для реализации конкретного множества используется массив. Это приводит к ухудшению характеристик программы из-за отсутствия ручной оптимизации. Далее, становится как правило невозможной реализация функций подстановкой (если не считать какихто конкретных ситуаций, когда настоящий тип известен транслятору), поэтому все полезные операции интерфейса, задаются как вызовы виртуальных функций. Как и для конкретных типов здесь плата за абстрактные типы иногда приемлема, иногда слишком высока.

Подводя итог, перечислим каким целям должен служить абстрактный тип:

[1] определять некоторое понятие таким образом, что в программе могут сосуществовать для него несколько реализаций;

[2]применяя виртуальные функции, обеспечивать достаточно высокую степень компактности и эффективности выполнения программы;

[3]сводить к минимуму зависимость любой реализации от других классов;

[4]представлять само по себе осмысленное понятие.

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

340

Соседние файлы в предмете Программирование