116
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ УРАЛЬСКИЙ ФЕДЕРАЛЬНЫЙ УНИВЕРСИТЕТ ИМЕНИ ПЕРВОГО ПРЕЗИДЕНТА РОССИИ Б. Н. ЕЛЬЦИНА Д. Р. Кувшинов, С. И. Осипов ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ Стандартная библиотека шаблонов Рекомендовано методическим советом УрФУ в качестве учебного пособия для студентов, обучающихся по программе бакалавриата по направлению подготовки 010800 «Механика и математическое моделирование» Екатеринбург Издательство Уральского университета 2013

ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

  • Upload
    others

  • View
    0

  • Download
    0

Embed Size (px)

Citation preview

Page 1: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИУРАЛЬСКИЙ ФЕДЕРАЛЬНЫЙ УНИВЕРСИТЕТ

ИМЕНИ ПЕРВОГО ПРЕЗИДЕНТА РОССИИ Б. Н. ЕЛЬЦИНА

Д. Р. Кувшинов, С. И. Осипов

ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО-ОРИЕНТИРОВАННОГО

ПРОГРАММИРОВАНИЯ

Стандартная библиотека шаблонов

Рекомендовано методическим советом УрФУ в качестве учебного пособия для студентов, обучающихся по программе бакалавриата по направлению подготовки

010800 «Механика и математическое моделирование»

Екатеринбург Издательство Уральского университета

2013

Page 2: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

УДК 004.42(075.8) К885

Р е ц е н з е н т ы : кафедра высшей математики Российского государственного

профессионально-педагогического университета (заведующий кафедрой кандидат физико-математических наук,

доцент Е. А. П ерм ин ов);С. Г. Ф р о л о в , кандидат технических наук, доцент

(Уральский государственный горный университет)

Кувшинов, Д. Р.К885 Основы обобщенного и объектно-ориентированного

программирования : Стандартная библиотека шаблонов : [учеб. пособие] / Д. Р. Кувшинов, С. И. Осипов ; М-во образования и науки Рос. Федерации, Урал, федер. ун-т. — Екатеринбург : Изд-во Урал, ун-та, 2013. — 116 с.

ISBN 978-5-7996-1014-2В издании рассматриваются основные понятия и некоторые

приемы объектно-ориентированного и обобщенного программи­рования с примерами на языке C++. Отдельная глава посвя­щена Стандартной библиотеке шаблонов и смежным компонен­там Стандартной библиотеки C++. Для закрепления материа­ла предлагаются упражнения. Пособие рассчитано на студентов, освоивших основы структурного программирования.

УДК 004.42(075.8)

© Уральский федеральный университет, 2013 ISBN 978-5-7996-1014-2 © Кувшинов Д. Р., Осипов С. И., 2013

Page 3: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Оглавление

Предисловие 5

Введение 7

Глава 1. От процедур к шаблонам 111.1. Структурное программирование............................. 111.2. Калькулятор на основе постфиксной записи . . . 121.3. Модульное программирование................................ 151.4. От модулей к классам ................................................ 161.5. Конструкторы и д еструкторы ................................ 191.6. Обобщенная р е а л и за ц и я .......................................... 271.7. Расширение классов................................................... 321.8. Стратегии...................................................................... 36L9. Метапрограммирование............................................. 38

Глава 2. Объектно-ориентированное программиро­вание 42

2.1. Значения и объекты ................................................... 422.2. АТД «Множество» и класс «Множество» 472.3. Основные понятия и п р и н ц и п ы ............................ 49

2.3.1. И нкапсуляция................................................ 502.3.2. Наследование 512.3.3. П олим орф изм ................................................ 512.3.4. Оформление классов и интерфейсов в UML 552.3.5. Виды отношений между классами 56

Page 4: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

2.3.6. Геттеры и сеттеры ........................................ 572.3.7. SO LID ............................................................... 59

2.4. Паттерны ООП ........................................................ 632.4.1. Паттерны со зд ан и я ..................................... 642.4.2. Паттерны стр у кту р ы .................................. 652.4.3. Паттерны поведения 662.4.4. М Ѵ С и М Ѵ Р .................................................. 71

2.5. Множественное наследование.............................. 72

Глава 3. Элементы Стандартной библиотеки 763.1. Контейнеры и итераторы ........................................ 78

3.1.1. И тер ато р ы ..................................................... 783.1.2. Стандартные контейнеры 803.1.3. Линейные кон тей н ер ы ............................... 823.1.4. Ассоциативные контей неры ..................... 853.1.5. И тераторы-адаптеры.................................. 89

3.2. Алгоритмы и ф у н к т о р ы ........................................ 913.2.1. Идиома удаления элементов из контейнера 983.2.2. Средства конструирования функторов . . 99

3.3. Ѵ аіаггау........................................................................ 1023.4. Статическая диспетчеризация................................ 1093.5. Умные у к а з а т е л и ....................................................... 110

Список рекомендуемой литературы 114

Page 5: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Предисловие

Цель данного пособия — формирование у студентов базо­вых знаний и навыков в области современного программиро­вания на языке C++. Для того чтобы освоить представленный материал, требуется знание основных конструкций общего под­множества языков С и C++, а также наличие навыков разра­ботки программ, опирающихся на принципы структурного про­граммирования.

Пособие содержит ряд сравнительно крупных по объему ра­боты упражнений, которые помимо закрепления пройденного материала предполагают самостоятельное изучение студента­ми дополнительных источников. Упражнения отмечены знач­ком [у].

Материал пособия разбит на три главы. В первой главе «От процедур к шаблонам» в качестве точки отсчета взято структурное программирование и постепенно вводятся элемен­ты обобщенного программирования, опирающегося в языке C++ на механизм применяемых во время компиляции шаблонов. Во второй главе «Объектно-ориентированное программирова­ние» дано описание объектно-ориентированного программиро­вания с нуля, при этом основная часть представленных по­нятий, принципов и паттернов (моделей программных компо­нент) применима в большинстве актуальных современных язы­ков программирования. Третья глава «Элементы Стандарт­ной библиотеки» в основном посвящена Стандартной библио­

Page 6: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

теке шаблонов, но освещает и некоторые другие важные части Стандартной библиотеки языка программирования C++.

Во введении представлены исторический аспект развития C++ в последние годы и авторский взгляд на место, занимаемое этим языком среди прочих языков программирования, а также его ценность в качестве изучаемого языка.

Пособие ориентировано на студентов, изучающих компью­терные науки в рамках учебного плана направления 010800 «Механика и математическое моделирование».

Page 7: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Введение

С принятием в 2011 г. нового Международного стандар­та языка программирования C++1 (ISO С++11), добавившего в язык и Стандартную библиотеку много новых элементов, произошло оживление интереса к C++ как к основному язы­ку разработки программного обеспечения. В отличие от таких языков, как Java, G-jf и JavaScript, которые задействуют вир­туальные машины и компиляцию в машинный код на компью­тере пользователя2, использование C++ подразумевает компи­ляцию в машинный код до распространения продукта, поэтому наметившийся рост популярности C++ в англоязычном Интер­нете окрестили native renaissance, что можно примерно переве­сти как «возрождение распространения программного обеспе­чения, заранее скомпилированного в машинный код»3.

Существует множество языков программирования, предпо­лагающих компиляцию в машинный код, не опирающийся на виртуальную машину, однако языки С и C++ занимают среди них особое положение, связанное с их широким применением в инфраструктурном программном обеспечении (ПО): опера­ционные системы, драйверы устройств и системы управления

*См.: International Standard ISO/IEC-14882: Programming Langua­ges - C++/ ISO/IEC. N. Y., 2011.

2JIT-компиляция, от англ. just in time «точно в срок» или «только в тот момент, когда потребуется». ЛТ-комгіиляция явля­ется оптимизацией и может выполняться не для всего кода или не выполняться вовсе.

3Здесь от англ. native code — машинный код.

Page 8: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

оборудованием, браузеры, виртуальные машины и транслято­ры, «движки» и библиотеки средств управления базами дан­ных, серверного ПО, игровых приложений, систем трехмерного моделирования, систем для выполнения инженерных и науч­ных вычислений. Кроме того, они оказали значительное влия­ние на ряд более поздних языков, в первую очередь на широко применяемые в настоящее время Java и С # , а также языки, предназначенные для программирования графических процес­соров (например, GLSL и OpenCL).

На волне native renaissance получили известность три дру­гих сравнительно новых языка, отчасти претендующих на ме­сто C++: D, Go и Rust. Среди них D наиболее развитый и наи­более похож на C++. Важно отметить, что во многом данные языки в своем развитии отталкиваются от решений, принятых в C++, поэтому их изучение удобно строить на сопоставлении с C++, и люди, хорошо его освоившие, сразу видят идеологию большинства конструкций и причины их появления в данных языках.

Другая тенденция последних лет состоит в росте популяр­ности функционального программирования (в том числе ком­пилируемых в машинный код языков Haskell и Common Lisp), сопровождающемся широким внедрением конструкций, поза­имствованных из функциональных языков4, в традиционные («императивные») языки. C++ один из ранних примеров такого проникновения: механизм шаблонов основан на А-исчислении и предоставляет возможность описывать вычисляемые компи­лятором чистые функции («шаблоны»), пользуясь сопоставле­нием по образцу, встроенным в систему типов C++. ISO С++11 пополнился некоторыми другими элементами, свойственными функциональным языкам.

C++ продолжает активно развиваться. Планируется приня­тие новых стандартов в 2014 и 2017 гг. Первый из них будет включать необходимые уточнения и дополнения ISO С++11.

4Англ. fimperative programming -- от соединения слов «функцио­нальное» и «императивное».

Page 9: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Следующая версия стандарта предполагает существенное об­новление языка. Таким образом, несмотря на почтенный по компьютерным меркам возраст, язык C++ продолжает сохра­нять актуальность и даже становится источником нововведе­ний для других языков программирования.

Язык С тоже развивался, и в 2011 г. был принят новый Международный стандарт языка программирования С5. Тем не менее наиболее распространена поддержка старого стандар­та 1990 г. (можно считать, что компиляторы есть для всех ак­туальных программно-аппаратных платформ). Кроме того, ос­новная его часть входит в качестве подмножества в язык C++ (в дальнейшем планируется сближение стандартов и улучше­ние совместимости C++ с С более новых версий, хотя некоторые их элементы уже включены в ISO C++11). Поэтому изучение C++ неизменно влечет изучение С, и в данном пособии пред­полагается, что читатель знаком с основными элементами ISO С90 и структурного программирования.

Поддержка стандартного C++ на различных платформах уступает таковой ISO С90, однако все равно находится на вы­соком уровне относительно прочих языков программирования, что делает C++ подходящим средством для создания кросс- платформенных приложений. Особенно важно наличие богато­го выбора готовых качественных библиотек программных ком­понент, в том числе кросс-платформенных (от смартфонов до суперкомпьютеров).

В мире открытого ПО существуют два основных компи­лятора, поддерживающих широкий круг систем: GNU g-f-h и Clang. Их последние версии поддерживают ISO C++11 пол­ностью. В мире Windows основным компилятором является Microsoft Visual C++ (сокращенно называемый MSVC). Основ­ным приоритетом в развитии MSVC является поддержка гря­дущего стандарта 2014 г. К сожалению, на данный момент MSVC не может похвастать полной поддержкой ISO С++11.

5См.: International Standard ISO/IEC-9899: Programming Langur ges - С / ISO/IEC. N. Y., 2011.

Page 10: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Содержание данного пособия ориентируется на возможности Visual C++ 2012.

С точки зрения создания программных продуктов следу­ет отметить следующее. Язык С задает стандарт связывания программных компонент в машинном коде (называемый АВІЬ), благодаря поддержке которого прочими языками программи­рования можно обеспечить минимальный уровень совместимо­сти между компонентами, созданными на разных языках. Что же касается С-+ +, то, несмотря на полную обратную совмести­мость с С АВІ, C++ ABI не является переносимым и зависит от конкретного компилятора. Сюда относятся конструкции, отве­чающие объектно-ориентированному и обобщенному програм­мированию.

Ценность C++ в качестве языка программирования, изуча­емого в учреждениях профессионального образования, заклю­чается в двух основных (взаимосвязанных) аспектах: богат­стве поддерживаемых методологий программирования и прак­тической применимости для широкого спектра задач. Первое опирается на «поддержку» структурного, объектно-ориентиро­ванного и обобщенного программирования, включая некоторые элементы, характерные для функционального программирова­ния. Второе, помимо того что перечислено выше, также опира­ется на широкий охват «уровней» программирования — от ас­семблерных вставок и прямого управления памятью до высоко­уровневых классов и метапрограммирования. Именно прямое управление выделением памяти и временем жизни объектов нередко считается основной чертой, отличающей C++ от язы­ков, использующих виртуальную машину и скрытый от про­граммиста механизм освобождения ресурсов («сборщик мусо­ра»). Средства, предоставляемые C++ и Стандартной библио­текой, позволяют во многом преодолеть недостатки такого под­хода, сохранив его преимущества.

6От англ. application binary interface — двоичный интерфейс при­ложений.

Page 11: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Глава 1

От процедур к шаблонам

1.1. Структурное программирование

Процесс решения некоторой задачи можно представить со­стоящим из двух основных действий - анализа и синтеза.

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

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

*В языках С и C++ эти термины являются синонимами.

Page 12: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

Структурное2 программирование и проектирование ввело набор стандартных базовых конструкций построения алгорит­мов и принцип разработки «сверху-вниз»3: сначала ставится общая задача, алгоритм решения которой записывается с ис­пользованием еще не реализованных подпрограмм, после чего реализация алгоритма проверяется. Чтобы иметь возможность выполнить тестирование, вместо еще не готовых рабочих реа­лизаций подпрограмм подставляются «заглушки», которые вы­водят текстовое сообщение. После того как разработчик убе­дится в корректности реализации алгоритма, он переходит к реализации подпрограмм.

Другим элементом, свойственным структурному проекти­рованию, является выделение кортежей характеристик предме­тов, с описаниями которых нужно работать программе, объяв­ление их самостоятельными типами данных и снабжение этих типов данных наборами подпрограмм, выполняющих над их значениями базовые действия. Иными словами, это определе­ние алгебры («абстрактного типа данных») — пары (множество значений, множество операций над этими значениями).

1.2. Калькулятор на основе постфиксной записи

В качестве примера простой программы в структурном сти­ле рассмотрим «Стековый калькулятор». Программа позволя­

2Англ. structured programming можно также перевести как «упо­рядоченное программирование».

3См.: Дал У., Дейкстра Э., Хоор К. Структурное программиро­вание. М., 1975.

Page 13: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

45 3 2 1 + * / = 45 3 (2 + 1) * / = 45 (3 * (2 + 1)) / = = (45/(3 *(2 + 1))).

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

45 3 2 1 + * / => 45 3 3 * / =» 45 9 / =► 5.Опишем алгоритм более формально.

• Создать стек чисел.

• Считывать лексемы из потока ввода, пока это возможно,

если следующая лексема:

число — поместить число в стек; знак «равно» — вывести вершину стека; знак операции —

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

Иначе сообщить об ошибке.

• Удалить стек.

Опишем стек чисел на См-, следуя идеологии структурного программирования.

/ / представление стека const size t STACK SIZE = 500; s tru c t Stack {

Page 14: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

double elem [STACK_SIZE]: s iz e _ t top :

};/ / операции над стекомvoid in i t ( Stack&); / / разметить пустой стек bool isEnipt}'( const S tack&); / / пуст ли стек? bool isF u Il (const Stack&); / / полон ли стек? void push ( Stack&, doub le ); / / затолкнуть значение double pop(Stack& ); / / извлечь значение из стека double top (const S tack&): / / посмотреть вершину

Операцию считывания лексем удобно реализовать в виде отдельной функции, работающей со значениями типа «Лексе­ма»./ / представление лексемыs t r u c t Token { / / число или знак операции

union { double number; char o p e ra tio n ; }; enum { Number, O peration } type;

};/ / считывание лексемы, возвращает успех bool readToken ( std :: is tream &from , Token&);

Теперь не составит труда записать алгоритм на языке С-н-Характерными чертами этой реализации являются:

• непосредственная доступность представлений используе­мых типов. Каждый может видеть, что Stack построен поверх статического массива, a Token использует объеди­нение с тегом. Из любого места программы с этим пред­ставлением можно работать непосредственно;

• функции, реализующие базис операций над типами, су­ществуют отдельно и могут быть объявлены и опреде­лены где угодно. Изучение структуры Stack не дает нам знание о том, какие действия с ней можно выполнять;

• процедурная декомпозиция, построенная на основе явно выписанного алгоритма. Программа предстает в виде на­бора вызывающих друг друга функций, обрабатывающих данные.

Page 15: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

1.3. Модульное программирование

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

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

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

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

Отдельные отлаженные модули удобно задействовать по­вторно в других проектах. Таким образом, модульное програм­мирование повышает производительность работы программи­стов относительно структурного программирования без исполь­зования модулей.

В языке С (и, по наследству, в C++) модулем может считать­ся связка заголовочного файла (.h), содержащего интерфейс, с единицей трансляции (.с/'.срр файл), содержащей реализацию.

Page 16: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Язык позволяет скрыть представление типов, дав в .h файле только объявление типа (определение — в .с файле). К значе­ниям таких типов извне определившего их модуля можно будет обращаться только через ссылку или указатель. Вынесем Stack в пару файлов stack.h и stack.cpp.// s t a c k . h ^pragm a once / / объявление типа s t r u c t S tack;/ / операции над стекомStack* newStack ( void ); / / создать пустой стек void d e le teS tack ( Stack *); / / удалить стек bool isE m pty(const S tack * ); / / пуст ли стек? bool is F u ll (co n st S tack* ); / / полон ли стек? void push (S tack * , d o u b le ); / / втолкнуть значение double pop (S tack * ); / / извлечь значение из стека double top (co n st S tack * ); / / посмотреть вершину

// s t a c k . cpp # in c lu d e " stack . h ”/ / представление стека const size t STACK_S1ZE = 500; s t r u c t Stack {

double elem [STACK_SIZE]; s iz e _ t top :

};

Stack* newStack () {Stack *S = new S tack ; / / выделить память S->top — 0; / / стек пустой r e tu rn S;

}/ / далее — определения остальных функций

1.4. От модулей к классамРазумно размещать объявления типов и операций над ни­

ми в одном заголовочном файле. Следующий т а г — связать

Page 17: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

их синтаксически. В C++ в качестве членов структуры можно объявлять не только поля данных, но и функции (функции- члены).

/ / s ta c k .h ^pragma once s t r u c t Stack {

const size t STACKSIZE = 500; double elem [STACK SIZE]; s iz e _ t top_;/ / операции над стеком void i n i t ( ) ; / / разметить пустой стек bool isEm ptyO c o n s t; / / пуст ли стек? bool isF u ll () co n s t; / / полон ли стек? void push (d o u b le ); / / втолкнуть значение double рор(): / / извлечь значение из стека double top () c o n s t: / / посмотреть вершину

};

/ / s tack, срр ^ in c lu d e " stack . h" void S ta c k :: i n i t (){ t°P_ — 0; /* понимается как th i s —>top_ * / }/ / далее реализации других функций

Заметим, что функции, став членами структуры, потеря­ли первый аргумент — указатель на Stack, так как компи­лятор передает указатель на значение, для которого вызва­на функция-член, в виде неявного дополнительного параметра. Теперь вместо, например, push (festack, х + у) следует писать s tack .p u sh (х + у).

Внутри функции-члена этот неявно переданный указатель на значение, для которого она вызвана (стоящее слева от точ­ки), можно получить, используя ключевое слово th i s . Далее будем называть такие значения объектами (подробнее в гла­ве 2). При обращении к членам структуры th is -> можно не писать, если нет конфликта имен. Запись вида

bool isEm ptyO c o n s t;

Page 18: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

говорит о том, что th i s для этой функции является указате­лем на константу, так что компилятор будет препятствовать явному изменению полей структуры внутри этой функции и вызову из нее функций-членов без const (так как они, в свою очередь, могут изменять поля).

Впрочем, с точки зрения модульного программирования но­вый код — это шаг назад. Реализация видна и доступна. Все, что мы на данный момент получили, это более явное связы­вание данных с обрабатывающими их процедурами и новый синтаксис вызова функции для объекта «через точку». Поэто­му немного изменим код Stack.

c la s s Stack { p u b l i c :

Stack () ; / / разметить пустой стек bool isEm ptyO c o n s t; / / пуст ли стек? bool is F u ll () c o n s t; / / полон ли стек? void push (double ); / / втолкнуть значение double pop () ; / / извлечь значение double top () c o n s t; / / посмотреть вершину

p r iv a te :s t a t i c co n st s iz e _ t STACK SIZE = 500; double elem [STACK_SIZE] ; s iz e _ t top_ ;

};

Рассмотрим внесенные изменения.

• Ключевое слово pub lic открывает секцию, содержащую общедоступные зависимые имена.

• Ключевое слово p r iv a te открывает секцию, содержащую зависимые имена, доступ к которым открыт только функ­циям-членам самого класса.

• Слово s t r u c t поменяли на c lass . В языке C++ как струк­туры традиционно оформляются простые типы с откры­той реализацией. В случае же если реализация скрыта,

Page 19: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

тип описывается как класс. Отличие класса C++ от струк­туры C++ только в том, что в классе доступ по умолчанию private, а в структуре — public.

• Вместо функции i n i t объявлено что-то, имеющее вид функции с именем класса, не возвращающей значения. В C++ так объявляется конструктор — специальная функ­ция, вызываемая в момент создания объекта (после вы­деления памяти на его представление). Конструктор вы­полняет инициализацию объекта. Конструктор, не име­ющий параметров (как Stack О ), называется конструк­тором по умолчанию и вызывается автоматически при создании объекта, как показано ниже:

Stack s tack ; / / неявный вызов S t a c k :: S t a c k () Stack *ps = new S tack ; / / аналогично

• К определению константы STACK_ SIZE добавлено сло­во s ta t ic . Предыдущий вариант предполагал, что каж­дая копия стека содержит свое поле STACK_SIZE (пусть и константное). Слово s t a t i c приводит к тому, что член класса считается «общим» для всех его объектов и не раз­мещается внутри какого-либо из них. Другими словами, теперь Stack — эго пространство имен для STACK_SIZE (но с контролем доступа — p r iv a te остается в силе).

[у] (1.1) Допишите Stack и реализуйте функцию readToken. Создайте консольное приложение «Постфиксный калькулятор» и проверьте его работоспособность.

1.5. Конструкторы и деструкторы

Наш стек был построен поверх массива строго заданного размера STACK SIZE. При этом пользователь стека не имеет никакого способа выбрать этот размер и даже (в случае private- константы) не может получить его из программы. С другой

19

Page 20: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

стороны, 500 элементов кому-то может быть мало, а кому-то и много — пустой расход памяти. Поэтому разумно позволить при создании объекта стека указывать его размер.

Будем хранить в объекте Stack указатель на динамический массив. У пользователя будет возможность указать требуемый размер стека в момент создания объекта.

c la s s Stack { p u b l i c :

/ / создать новый стек размера size S t a c k ( s i z e _ t size = 500);"Stack () ; / / удалить стек / / . . . старый набор функций

p r i v a t e :double *elem ; s ize_ t size_ . top_ ;

};

Мы заменили конструктор, не принимающий параметров, конструктором, принимающим размер массива. Этот конструк­тор может быть использован и как конструктор по умолчанию, так как мы предоставили значения по умолчанию для всех его параметров (в данном случае — одного параметра size, те са­мые 500 элементов). Возможная реализация приведена ниже.

Stack :: S ta c k ( s i z e _ t sz) { elem = n u l l p t r ; elem — new double [ sz ];s ize_ — sz ; top — 0;

}

При удалении объекта Stack (это равным образом относится к автоматической, статической и динамической памяти) проис­ходит вызов деструктора — специальной функции (имя кото­рой "ИмяКласса, например, "Stack), отвечающей за корректное освобождение ресурсов, управляемых удаляемым объектом. В нашем случае деструктор должен удалить массив, выделенный в куче конструктором.

Page 21: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Stack :: ~ Stack (){ de l e te [J elem; }

Теперь посмотрим на такой код.Stack S = 10; / / вызов конструктора?Stack Р — S; / / вызов конструктора?S = 20; / / ???

Этот код успешно скомпилируется. Так как при создании объекта обязательно вызывается конструктор, то для S будет вызван тот самый конструктор, принимающий размер масси­ва, т. е. под стек будет выделен массив из 10 элементов. Запись Р = S подразумевает копирование объекта S в новый объект Р. Когда копирование выполняется при инициализации, компиля­тор вызывает конструктор копирования, принимающий ссыл­ку на объект своего класса. Если конструктор копирования не определен, то компилятор создаст его автоматически, и этот конструктор будет тривиальным: он будет копировать поля класса в порядке перечисления в определении класса, вызывая конструкторы копирования полей. Конструкторы копирования примитивных типов выполняют копирование двоичного пред­ставления.

Конструктор по умолчанию также может быть тривиаль­ным и создается компилятором, если класс не объявляет пи од­ного конструктора. Поэтому, если, например, определить кон­структор копирования и только его, то объекты класса нельзя будет создать «просто так», для этого потребуется определить конструктор по умолчанию явно. Тривиальный конструктор по умолчанию вызывает конструкторы по умолчанию (где они определены) для полей в порядке их перечисления. C++ га­рантирует вызов конструктора при создании объекта, поэтому любой наш конструктор также будет неявно вызывать для по­лей (не POD) их конструкторы по умолчанию, если не описаны явные их вызовы с помощью списка инициализации (см. ниже).

Упрощая, можно сказать, что в C++ под POD (англ. plain old data) понимают типы, которые определены в рамках под­множества языка С. Поля, имеющие такой тип, не инициали­

Page 22: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

зируются, поэтому elem нужно явно присваивать нулевой ука­затель, иначе там может оказаться произвольное значение. Де­структор должен быть способен корректно отработать, даже если во время работы конструктора возникла ошибка, из-за ко­торой объект не был полностью создан. В нашем случае ошиб­ка возможна при вызове new, если блок переданного размера невозможно выделить. Вызов d e le te для нулевого указателя не является ошибкой.

Деструктор автоматически вызывает деструкторы для но­лей объекта в порядке, обратном порядку их перечисления в определении класса. Деструкторы POD-типов не выполняют никаких действий, поэтому ресурсы, вроде блоков динамиче­ской памяти, управляемых через указатель, необходимо зуда­лять явно.

Итак, к чему же приведет Р = S? Содержимое S будет ско­пировано в Р «как есть». В частности, P.elem станет равно S . elem, что может привести к двойному удалению этого блока памяти вызовом деструктора и для S, и для Р. Это серьезная ошибка. Следовательно, нужно определить конструктор копи­рования явно.

А что же делает присваивание S = 20? Компилятор нахо­дит конструктор S tack(s ize_ t ) , которому передает значение 20, и выполняет оператор присваивания для S и нового объекта стека (временного объекта, который после выполнения присва­ивания будет автоматически удален!). Автоматически опреде­ляемый оператор присваивания поступает аналогично триви­альному конструктор}' копирования: присваивает полям объ­екта слева значения соответствующих полей объекта справа в порядке их перечисления. В нашем случае это просто копирова­ние значений «как есть», что приведет к ошибке (блок памяти, исходно привязанный к полю elem объекта справа, будет сразу удален).

Итак, во-первых, следует запретить неявно вызывать кон­структор, для чего к его объявлению надо добавить ключевое слово e x p lic i t . Во-вторых, следует явно определить оператор

Page 23: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

присваивания, который будет выполнять корректное копиро­вание. Правило трех: если требуется определить конструктор копирования, деструктор или копирующий оператор присваи­вания, то, скорее всего, надо определить все три.

c lass Stack { p u b l i c :

e x p l i c i t S t a c k ( s i z e _ t sz = 500);Stack (const Stack&); / / 1) скопировать “ Stack (); / / 2) удалить стекStack& op e ra to r= (co n s t Stack&); / / 3) присвоить / / . . . остальное как раньше

};

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

Stack :: Stack ( s ize_ t sz) / / от и до ’{ ’ — список: s i z e _ ( s z ) , t o p _ (0 ) . elem ( n u l l p t r ) / / инициализации { elem — new double [ s z ] ; }

Список инициализации «отрабатывает» строго до старта тела конструктора и не влияет на порядок вызова конструкто­ров полей (важно помнить об этом: в каком бы порядке вы не указывали конструкторы в списке инициализации, вызываться они все равно будут в порядке определения нолей класса).

Обеспечив корректность кода, можно позволить себе пора­ботать над оптимизацией. Предположим, где-то понадобилось обменять значения двух стеков. Простая реализация через вре­менную переменную (объект стека) весьма неэффективна: это три копирования возможно больших массивов с выделением временного массива в куче. Очевидно, эффективно выполняе­мый обмен легко реализовать как функцию-член S tack : :swap (Stack &other), обменивающий значения нолей elem, size_ и top_.

Page 24: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Чтобы компилятор мог использовать наш обмен вместо std:: swap, мы можем определить собственную свободную функцию swap, делегирующую вызов функции-члену swap (ее необходи­мо разместить в том же пространстве имен, что и Stack).void swap (Stack , Stack &b) { a. swap (b); }

Функция foo обратится к нашей «быстрой» функции swap для обмена стеков вместо «медленной» стандартной:void foo () {

using s t d : : swap; / / no умолчанию s td: :swapStack a (10) , b (20);in t с — 10, d = 20;swap(c, d) ; / / ѳызоѳ s t d :: swapswap (a , b ) ; / / вызов нашей функции swap

}Рассмотрим еще одну ситуацию. Возможно, есть функция,

которая заполняет стек определенным образом, например по­мещает в него числа от 1 до п. Каким образом эта функция должна возвращать заполненный стек? Например, она может сформировать локальный объект и вернуть его по значению.Stack f ro in l to n ( in t n) {

Stack S(n) ;for ( i n t i — 1; i <— n; -H-i) S .p u s h ( i ) ; r e t u r n S :

}Однако в данный момент это неэффективно, так как вызов

Stack ns = fromlton(100); потенциально приведет к двум ко­пированиям: вызовам конструктора копирования локального объекта S инструкцией re tu rn во временный безымянный объ­ект на стеке вызовов и конструктора копирования этого вре­менного объекта в ns. Иногда компилятор способен удалить лишние копирования, выполнив неявную передачу ссылки на переменную, которой присваивается возвращаемое значение. Не полагаясь на оптимизацию, выполняемую компилятором, можно передавать объект по ссылке явно и вызывать функ­цию для уже готового объекта.

Page 25: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

bool from lton (Stack &S, in t n) { for ( in t i = 1; i <= n: -f+ i) {

i f ( S . i s F u l lQ ) r e tu rn f a ls e ;S . push( i );

}re tu rn t ru e ;

}

Stack ns(100); from lton (ns , 100);

Иногда этот способ действительно удобнее, особенно если один и тот же объект используется неоднократно. Однако неред­ко желательно возвращать объект непосредственно и при этом не выполнять ненужные дорогостоящие копирования. В ISO C++11 для этого предусмотрен механизм rvalue reference — ссы­лок на временные объекты, позволяющих явно определить се­мантику перемещения объектив. Тип «ссылка на временный объект типа Т» обозначается как Тkk, стандартная функция std: :move(T&) преобразует «обычную» ссылку (lvalue reference) в ссылку на временный объект (в обратную сторону приводит­ся автоматически).

Чтобы при инициализации или присваивании действитель­но происходило перемещение, следует определить перемещаю­щий конструктор (аналог конструктора копирования, но при­нимающий ссылку на временный объект) и перемещающий опе­ратор присваивание (аналог копирующего оператора присва­ивания). Действие этих функций сводится к передаче управ­ляемых ресурсов из временного объекта в новый объект. Итак, теперь класс Stack выглядит следующим образом:

c la ss Stack { p u b l ic :

e x p l ic i t S ta c k (s iz e _ t sz - 500);Stack (co n st Stack& ); / / скопировать Stack ( Stack&&); / / переместить ~ S tack (); / / удалить стекStack& o p e ra to r= (c o n s t S tack &); / / присвоитьStack& o p e ra to r = (Stack&:&:); / / переместить

Page 26: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

void swap( S tack&); / / обменять / / . . . остальное как раньше

};Покажем определение перемещающего конструктора и при­

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

Stack :: Stack ( Stack Met ): elem ( t . e lem) , s ize_ ( t . size_ ) , t o p _ ( t . t o p _ )

{ t . elem = n u l l p t r ;/* теперь t может быть удален * / }

Stack& Stack :: ope ra to r= (S tack kkX ) {swap ( t ); / / обменяем, а старое содержимоеr e t u r n * t h i s : / / будет удалено автоматически

}Теперь первый вариант from 1 ton не будет требовать копиро­

вания для возвращения значения из функции: упрощая, мож­но сказать, что локальная переменная, возвращаемая re tu rn , считается временным значением.

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

s t r u c t Person { s t r i n g name;

/ / Person(const s t r ing &n) : name(n) {}Person ( s t r i n g n) : name(move(n )) {}

};

Внимание! Если используется параметр Т&&, где Т — тип, выводимый по параметрам шаблона функции (о шаблонах рас­сказывается далее), то он может разрешаться и как rvalue refe­rence, и как lvalue reference (универсальная ссылка).

te m p la te e c l a s s Т> void foo (const T&){ cout « " co n s twl v a l u e wref ” ; } tem pla te e c l a s s T> void foo (T&&)

Page 27: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

{ cout « M lv a l u e wo r wrv a lu e wr e f "; } Stack<int> S; / / S ниж е— это l v a l ue ! foo(S); / / > lvalue or rvalue re f

1.6. Обобщенная реализация

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

ty p e d e f StackElement double ;

и заменить везде в stack.h/.cpp double на StackElement. Про­блема возникнет, если в одной программе нужны стеки разных типов. Стандартное решение этой проблемы в языке С состоит в написании макроса, принимающего нужный тип элементов и генерирующего описание соответствующего типа стека. В C++ вместо этого класс следует определить как шаблон, принима­ющий типы в качестве параметров, подставляемых во время компиляции.

tem p la te Cclass ElementType> c la ss Stack { public :

e x p l i c i t S t a c k ( s i z e _ t sz — 500);Stack (const Stack<ElcmentType>&); / / копировать Stack (Stack<ElementType>&&); / / переместить “ Stack () ; / / удалить стекStack& o p e ra to r= (c o n s t Stack<ElementType>&);Stack& operator=(Stack<ElementType>&&:);void swap(Stack<ElementType>&;);bool isEmptyO c ons t ;bool i s F u l l Q cons t ;void push ( EleinentType );

Page 28: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

ElementType pop ();ElementType to p () c o n s t;

p r i v a t e :ElementType *clcm; s iz e _ t size_ . to p _ ;

};

Теперь, написав Stack<double>, мы скажем компилятору, что по шаблону Stack<ElementType> надо сгенерировать но­вый тип, подставив вместо ElementType тип double. Имя ново­го типа будет Stack<double>. Если же понадобятся стеки це­лых чисел или символов, то они будут называться Stack<int> и Stack<char>. Следует помнить, что это разные типы.

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

В основе обобщенного программирования лежит абстрак­ция от конкретных типов и алгоритмов и параметризация ти­пов и алгоритмов (определение шаблонов классов и функций) типами или значениями. В C++ подстановка параметров шаб­лонов происходит во время компиляции, предоставляя, таким образом, инструмент программирования процесса порождения кода (метапрограммирование).

В Стандартной библиотеке языка C++ имеется реализация шаблона стека. Продемонстрируем его интерфейс.

# in c lu d e < stack> / / здесь определен стек # in c lu d e < iostream >/ / читать символы, пока не встретится конец файла,/ / затем вывести их в обратном порядке in t main () {

using namespace std ; s tack< char> S; / / стек символов fo r ( ; ; ) {

const char ch = c in .g e t ( ) ;

Page 29: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

i f ( c i n . e o f Q ) break;S . push ( ch ): / / втолкнуть символ в стек

}w hile (! S . empty ()) { / / стек еще не пуст

cout « S . top ();S . pop (); / / удалить символ

/ / std :: stack <T> ::pop () ничего не возвращает}r e tu r n 0;

}Функциональность калькулятора также можно обернуть в

класс. Заложим в него возможность использования чисел раз­ных типов.

tem p la te <c la s s NumberType> c lass S tackCalcu la to r { p u b l i c :

enum Error{ Success, NoLastResult , NotEnoughOperands };/ / инициализацияStackCalcu la to r () : l a s t E r r o r S t a t e ( Success ) {}/ / получить результат последней операции NumberType l a s t R e s u l t Q cons t ;/ / получить информацию о последней ошибке bool hasError ( ) const { r e tu r n l a s t E r r o r Q != Success; }Error l a s t E r r o r Q const { r e tu r n l a s t E r r o r S t a t e ; }/ / положить в стек следующее число void pushNumber(NumberType operand){ ope rands . push(operand ); }/ / операцииvoid add (); / / сложить void sub (); / / ' вычесть void mul (); / / умножить void d iv ( ) ; / / разделить

p r i v a t e :std :: stack<NumberType> operands ;Error l a s t E r r o r S t a t e ;

};

Page 30: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Функции-члены можно определять прямо в месте объяв­ления в классе или структуре (при этом внутри функции до­ступно полное определение класса, как если бы определение функции находилось после определения класса). Обычно так делается, если они достаточно просты, как показано в приме­ре.

Класс StackCalculator<NT> ничего «не знает» о способах взаимодействия с пользователем. Может показаться разумным добавить к нему в качестве члена функцию, реализующую ал­горитм постфиксного калькулятора, но лучше избегать сме­шивания кода, отвечающего за программную модель, и кода, отвечающего за пользовательский интерфейс. Поэтому напи­шем эту функцию отдельно как шаблон функции с параметром NumberType.

tem p la te Cclass NumberType>StackCalcu la to r <NuniberType>:: Error runC a lcu la to r ( S tackCalcula to r <NuniberType> &calc ,

s t d : : i s t r e a m &in , std :: is tream &out) { Token<NumberType> to k e n ; w hile ( token . read ( in )) {

i f ( t o k e n . t a g ( ) = Token :: Number) { calc . pushNumber( token . number ( ) ) ;

} e lse i f ( t o k e n . opera t ion () = ’= ’) {const NumberType r = calc . l a s tR e s u l t (); i f ( calc . hasError ()) b reak; out « r « s t d : ; e n d l ;

} e l se {switch (token . operat ion ()) { case ca lc , add (); break;case : c a l c . s u b (); break; case c a l c .m u l ( ) ; break;case ’ / ’ : c a l c . d i v ( ) ; break:}i f ( calc . hasError ()) break;

}}r e t u r n calc . l a s t E r r o r () ;

}

Page 31: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Ранее упоминавшиеся стандартные функции swap и move являются функциями-шаблонами. Например, «общий» вари­ант реализации функции обмена мог бы выглядеть так:

tem p la te < c la s s Т> void swap(T &а, Т &b){ Т tmp ( а ); а = b ; b = tm p; }

Определять более эффективные реализации — дело авто­ров конкретных классов. Для класса-шаблона Stack оптими­зированный вариант обмена определяется аналогично, но, по­скольку требуется передать параметр шаблона, используется функция-шаблон.

te m p la te < c la s s ЕТ>void swap(Stack<ET> &а, Stack<ET> &b){ a . s w a p ( b ) ; }

[y] (1.2) Измените приведенный выше код общего варианта swap так, чтобы он использовал семантику перемещения. Пере­мещение представлений, используемое стандартной функцией swap, делает ненужным определение собственных вариантов swap для классов, определяющих перемещающие конструкто­ры и операторы присваивания.

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

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

Page 32: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

лятор найдет его самостоятельно (эта технология называется ADL — англ. argument dependent-lookup).|~у] (1.3) Перепишите Token в виде шаблона класса4. Пред­полагать, что операция operator>> (istream&, NumberTypefe) определена. Перепишите «Постфиксный калькулятор», исполь­зуя представленные выше шаблоны классов и Token. Следу­ет корректно обрабатывать ситуацию «неизвестная операция» (сохранять стек нетронутым, выводить сообщение об ошибке).

1.7. Расширение классов

Предположим, кто-то написал класс StackCalculator<NT> и передал его нам. Впоследствии выяснилось, что четырех арифметических действий недостаточно, нужна еще операция возведения в степень.

Все операции извлекают операнды из стека, поэтому при непосредственной реализации все они обращаются непосред­ственно к объекту стека, являющемуся private-полем класса калькулятора. Они также отвечают за выставление ошибки в случае невозможности извлечь операнд. В итоге имеем дубли­рование кода. Можно выделить действие «извлечь операнд из стека или выставить ошибку» в качестве отдельной функции- члена. Эта функция могла бы иметь вид

tem p la te Cclass NumberType> bool StackCalculator<NumbcrType>::

popNumber(NumberType fcoperand) { i f ( operands . empty ()) {

l a s t E r r o r S t a t e = NotEnoughOperands; r e tu r n f a l s e ;

}operand — operands . top () ; operands . pop(); r e tu r n t r u e ;

}

4Не рекомендуется использовать union.

Page 33: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Теперь о стеке, поверх которого построен калькулятор, до­статочно «знать» только функциям-членам pushNumber и рор- Number. Итак, интерфейс класса StackCalculator<NT> распа­дается на две части — «стек» и «арифметические действия».

Определим на основе интерфейса «стека» отдельный класс.

templa te < c la s s ElemType> c la ss CheckedStack { p u b l i c :

/ / попытаться просмотреть вершину стека bool pcekNumber (ElemTypc&;) cons t ;/ / положить ѳ стек следующий элемент void pushNumber (ElemType);/ / попытаться извлечь вершину стека bool popNumber(ElemType&);

p r i v a t e :std :: stack<ElemType> elems ; / / содержимое

Язык C++ позволяет расширять определения классов пу­тем включения в них объектов других классов в виде неявных полей (подобъектов). Допустим, мы хотим построить кальку­лятор с четырьмя арифметическими действиями поверх базо­вой функциональности класса CheckedStack<ET>. Это можно описать следующим образом:

tem p la te e c l a s s NumberType>c la ss S tackC a lcu la to r CheckedStack<NumberType> { p u b l i c :

eniun Error{ Success, NoLastResult , NotEnoughOperands }; S tackC a lcu la to r () : l a s t E r r o r S t a t e ( Success ) {}/ / получить результат последней операции NuinberType l a s t R e s u l t () cons t ;/ / получить информацию о последней ошибке Error l a s t E r r o r ( ) cons t ;/ / положить в стек следующее число void pushNumber(NumberType op){ CheckedStacke'NuinberType >:: pushNumber (op ); }/ / операции

Page 34: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

void add (); / / сложитьvoid sub (); / / вычестьvoid mul (); / / умножитьvoid div (); / / разделить

p r iv a t e :Error la s tE r ro rS ta te ;

};Обратите внимание, что расширяющий класс StackCalcula-

tor <NT> получает непосредственный доступ к открытым чле­нам расширяемого класса CheckedStack<NT> (базового клас­са). Однако по умолчанию эти члены не видны извне расши­ряющего класса (являются закрытыми). Закрытые же члены базового класса недоступны расширяющему классу: они скры­ты для разделения ответственности и предупреждения неожи­данных зависимостей.

В качестве примера приведем реализации last Result и add.

tem p la te < с la ss NumberType>NuiiiberType

StackCalculator<N um berType>:: la s tR e s u lt () const { NumbcrType re s u l t (0 ); / / по умолчанию нуль la s tE r ro rS ta te —

peekNumber( r e s u l t )? Success: N oL astR esu lt; r e tu rn r e s u l t ;

};

tem p la te e c la s s NumberType>void S tack C alcu la to r <N umber Type >:: add () {

NumbcrType x , у ;i f (popNumber(y) && popNumber(x)) {

la s tE r ro rS ta te — Success; pushNumber(x + y );

} e lsela s tE r r o r S ta te = NotEnoughOperands;

}

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

Page 35: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

калькулятора, следует явно указать, что public-члены базового класса должны быть доступны как public-члены расширяюще­го класса. Для этого поставим ключевое слово pub lic перед именем базового класса. Такое «public-расширение» называют наследованием. Класс не только получает содержимое другого класса, но и его интерфейс (подробнее в главе 2).

tem p la te < c la s s NumberType> c lass S tackCalcu la torEx :

pub l ic StackCalculator<NumberType> { p u b l i c :

void pow(); / / возвести в степень};

Проблема возникнет, когда мы попробуем реализовать pow: на уровне StackCalculator<NT> открытые члены CheckedStack <NT> уже были закрытыми, и public-расширение здесь не по­может — закрытые члены базового класса недоступны для рас­ширяющего класса.

В языке Cf + можно предоставить наследникам особый рас­ширенный интерфейс, разместив его в секции p ro tec ted («за­щищенное») вместо p u b lic или p r iv a te . Защищенные члены классов доступны только из самих этих классов и их наслед­ников (сколь угодно отдаленных, так что, например, если бы CheckedStack<NT> имел protected-члены, то они были бы до­ступны и в StackCalculatorEx<NT>).

Помимо public-наследования («просто» наследования) воз­можно protected-наследование, при использовании которого pub­lic- и protected-члены базового класса на уровне наследника по­лучают уровень доступа protected и не доступны никому, кроме самого класса и его наследников.

Изменим заголовок определения классагшаблона StackCal- culator<NT> с целью позволить реализовать StackCalculatorEx <NT>::pow аналогично тому, как это сделано в определениях арифметических действий в StackCalculat.or<NT>.

Page 36: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

tem p la te c c la s s NumberType> c la ss S tac k C a lcu la to r

: p ro te c te d CheckedStack<NumberType> {/ / дальше все как раньше . . .

};В качестве общей рекомендации можно дать принцип ми­

нимизации открытого и защищенного (protected) интерфей­сов. Если классу не обязательно быть настоящим наследником, то следует использовать private-расширение. Если не требуется предоставлять классам-наследникам доступ к некоторым чле­нам, то следует поместить их в private-секцию. Чем меньше будет открытый интерфейс, тем проще управлять его реали­зацией, проще распределять работу в команде разработчиков, проще развивать и сопровождать выпущенный код.

В случае с классом StackCalculatorEx использование public- расширения закладывает в нем «мину замедленного действия», что можно проиллюстрировать следующим кодом.S ta c k C a lc u la to r< in t> * scalc ~

new S tackC alcu lato rE x e in t >; d e le te s c a le ;

Произойдет вызов деструктора StackCalculator < int >, а ne StackCalculatorEx<int>. Конечно, в данном случае оба клас­са имеют одинаковое представление, и их деструкторы дей­ствуют одинаково. Но подобный код потенциально приводит к серьезным и труднообнаружимым ошибкам. Исправить его можно по-разному. Например, можно отказаться от public- расширения в пользу private-расширения, экспортируя унасле­дованный интерфейс с помощью using-объявлений в расширя­ющем классе. Другой способ состоит в объявлении деструктора базового класса виртуальным (см. главу 2).

1.8. СтратегииДальнейшее увеличение гибкости классов-шаблонов дости­

жимо через «подмешивание» (используя расширение) к ним

Page 37: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

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

Наш шаблон CheckedStack<ET> использует стандартный стек, «зашитый» в качестве private-поля. Этот объект — подхо­дящий выбор для делегирования его функционала стратегии.

tem p la te < c la ss ЕіешТуре,c la ss UncheckedStack — std : : s ta c k <Elem Type»

c la ss ChcckedStack : UnchcckedStack { p u b lic :

/ / попытаться просмотреть вершину стека bool peekNumber(ЕІешТуре &el) c o n s t;/ / положить в стек следующий элемент void pushNumber (ElemType );/ / попытаться извлечь вершину стека bool popNumber (Е1ешТуре&);

};Использование стратегий предполагает предоставление

«стратегий по умолчанию» с помощью значений но умолчанию для параметров шаблонов. В нашем случае стратегия по умол­чанию состоит в том, чтобы использовать стандартный стек. Поэтому, если пользователь пе имеет ничего против std::stack, то он может не указывать второй параметр шаблона, достаточ­но указать только тип элементов.

5Термин «политика» используется в рамках разных парадигм программирования с примерно одинаковым смыслом. Здесь он при­меняется в рамках обобщенного программирования.

Page 38: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Реализация с использованием стратегии позволяет напря­мую обращаться к public-членам класса-стратегии из функций- членов определяемого класса как к собственным функциям.

В языке C++ механизмы объектно-ориентированного про­граммирования (классы и наследование) и обобщенного про­граммирования (шаблоны) максимально ортогональны (незаг висимы друг от друга). Стратегии представляют собой инте­ресный пример синтеза этих механизмов.[7] (1.4) Перепишите «Постфиксный калькулятор» на осно­ве шаблона StackCalculatorEx<NT, US>, где US — стратегия «стек», «спускаемая» вниз вплоть до CheckedStack<NT, US>.

1.9. Метапрограммирование

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

[log2nj = 1 + log2 |n /2 j, log2 1 = 0.

Ha C++ это определение обретает следующую форму:

tem pla te cunsigned N> s t r u c t Log2 {

s t a t i c _ a s s e r t (N != 0);enum { value = 1 + Log2<N/2 >:: value };

};te m pla te о / / частная специализация шаблона s t r u c t Log2<l> { enum { value = 0 };};

Page 39: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

[~у] (1.5) Определить шаблон If<bool С, in t Т, in t F>, член value которого разрешается как Т, если С есть tru e , и как F — в противном случае. Используя эту метафункцию, определите Мах<in t A, in t В>.

Значение х п для целого п можно вычислить за 0 (log |n |) время, используя рекурсивное определение:

На C++ это определение выглядит так:

tem p la te < in t X, unsigned Y> s t r u c t Pow { enum { value —

If<Y&l, X, 1>:: value * Pow<X*X, Y/2 >:: value };} ; tem p la te < in t X>s t r u c t Pow<X, 0> { enum { value = 1 };};

py] (1.6) Обобщить определение Pow на показатели степени произвольного знака ( in t Y).

Пусть Ор — бинарная ассоциативная операция над целы­ми числами. Тогда вышеприведенный алгоритм возведения в степень можно обобщить следующим образом:

te m p la te < tem pla te< in t A, i n t В> c la s s Ор> s t r u c t Unit {}; / / единица операции Ор t em pla te < tem pla te< in t A, i n t В> c l a s s Op,

s t r u c t Pow : / / расширение сокращает запись Op<If<Y&l, X, Unit<Op>:: value >:: value ,

Pow<Op, Op<X, X>::value , Y /2> : :va lue> {}; tem pla te < tem pla te< in t A, i n t B> c l a s s Op, i n t X> s t r u c t Pow<Op, X, 0>{ enum { value — Unit<Op>:: value };} ;

[V] (1.7) Напишите шаблон MuKint , int>, выполняющий произведение своих параметров. Примените вышеприведенный Pow к Mul (не забудьте определить Unit<Mul>). Введите новый

mod 2, mod 2,

in t X, unsigned Y>

Page 40: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

шаблон IsAssociative<Op>, поле value которого установлено в true, если оператор Ор ассоциативен, и в f a l s e — в противном случае. Дополните определение Pow автоматическим выбором алгоритма (линейный или логарифмический) в зависимости от значения IsAssociative<Op>: .-value.

На практике не всегда удобно использовать шаблоны, кото­рые принимают в качестве параметров константы. Чтобы све­сти все к единой форме, можно определить вспомогательные шаблоны Int и Bool.

te m p la te c in t I> s t r u c t In t { enum {value = I} ;} ; tem plate< boo l B> s t r u c t Bool { enum {value = B};};

Вычисление метафункции, оперирующей типами, традици­онно производится обращением к вложенному типу type.

tem p la te C c lass Cond, c la ss T, c la ss F> s t r u c t If { /* ошибка * / }; tem p la te c c la s s T, c la ss F>s t r u c t If cB oo lcfa lse >, T, F> { ty p ed ef F type ; }; tem p la te C class T, c la ss F>s t r u c t Ifc B o o lc tru e > , T, F> { ty p ed ef T type; };

Передача метафункций в качестве параметров также более удобна, если метафункция преподносится не как шаблон, а как замкнутый тип. Тогда оперирующие ею метафункции вроде If могут «не знать», с чем они имеют дело. Для этого вычисле­ние метафункции можно переложить на вложенный шаблон­ный класс apply. Если переписать Pow с учетом вышенаписан- ного, получим следующее (обратите внимание на использова­ние ключевых слов tem plate и typename):

tem p la te C class Op, c la ss X, unsigned Y> s t r u c t Pow : tem p la te Op:: apply

cIfcB oolcY & l>, X, UnitcOp>>,PowcOp, tem p la te O p::applycX , X>, Y /2 » {}:

tem p la te c c la s s Op, c la s s X> s t r u c t PowcOp, X, 0>{ ty p e d e f typename U nitcO p>:: type type: } ;

Page 41: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

[у] (1.8) Основная польза метафункций состоит в возможно­сти отвлеченно манипулировать типами в обобщенных библио­течных классах и функциях. Ряд стандартных метафункций определен в заголовочном файле < type_traits> . Изучите его содержимое.

Более сложные метафункции позволяют формировать и об­рабатывать коллекции типов. Наиболее известной библиотекой на языке C++, предоставляющей для этого средства, является Boost.MPL6.

6Boost C++ Libraries [Электронный ресурс]. URL: http: / / www.boost.org/

Page 42: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Глава 2

Объектно- ориентированное программирование

2.1. Значения и объекты

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

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

!См.: Степанов А., Мак-Джоунс П. Начала программирования. М., 2011. Гл. 1. Вводные определения. С. 15-29.

Page 43: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

стей: Правительство России; книга, которую вы читаете; файл notepad.exe. Примеры абстрактных сущностей: число 7г; пря­мой угол; точка плоскости с координатами (—1,2).

Математика — это язык описания абстрактных сущностей. Математическая модель — конструкция, приближенно отра­жающая часть реального мира, опираясь на абстрактные сущ­ности. Занимаясь программированием, разработчики вынуж­дены строить математические модели. Для описания конкрет­ных сущностей вводятся атрибуты — соответствия с абстракт­ными сущностями, позволяющие «закодировать» качества кон­кретных сущностей и встроить их в математическую модель (что и составляет суть абстракции, так как «за бортом» тако­го описания остается множество малозначимых или вовсе неиз­вестных деталей). Примерами атрибутов могут служить физи­ческие величины, цвета, количества составляющих частей.

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

В математике абстрактные сущности организованы в аб­страктные виды. Абстрактные виды могут быть определены путем ввода наборов аксиом. Примеры абстрактных видов: дво­ичное дерево, абелева группа, натуральное число, цвет (в рам­ках некоторой цветовой модели). Наборы атрибутов позволяют выделить конкретные виды. Конкретные сущности, принадле­жащие одному конкретному виду, играют определенную роль в рамках модели и являются взаимозаменяемыми. Примеры: двигатель внутреннего сгорания, чайник со свистком, служа­щий банка.

Абстрагируясь от видов, можно вводить роды сходных ви­дов. Примеры абстрактных родов: линейное пространство, чис­ло, множество, граф. Примеры конкретных родов: участник дорожного движения, неподвижный объект, предмет посуды. Сущность принадлежит одному виду, но может быть отнесена

Page 44: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

В процедурном и функциональном программировании мы имеем значения, которые представляют абстрактные сущно­сти. Можно считать, что значения как таковые неизменны (чис­ло 3 всегда и везде одно и то же). В разное время одно и то же имя просто «привязано» к тому или иному значению, либо (в процедурном программировании) значение может храниться в переменной (которая играет роль места размещения представ­ления значения).

Типы значений отвечают абстрактным видам (в языках с автоматическим выводом типов явно указанные «типы» могут на деле отвечать абстрактным родам). Как правило, типы опи­сываются как абстрактные типы данных. Пример: целые числа и арифметические операции. Множество значений может быть формально бесконечным и не существовать физически.

В рамках обобщенного программирования абстрактные ро­ды (и иногда виды, если им не отвечают имеющиеся в распо­ряжении типы) называют концепциями (англ. concept). Типы, вкладывающиеся в некоторую концепцию, называются моде­лями этой концепции.

В объектно-ориентированном программировании объекты отвечают конкретным сущностям. Множества объектов всегда конечны и разбиваются на классы, определяющие полный (в рамках нашей модели) набор атрибутов некоторого конкретно­го вида. Класс можно считать «чертежом», по которому можно создать объект нужного вида и по которому же ненужный бо­лее объект можно корректно удалить. В свою очередь, абстра­гируясь от конкретики создания, существования (физического представления) и удаления объектов, только на основе огра­ниченного набора атрибутов можно определить интерфейс — отражение конкретного рода в объектно-ориентированном про­ектировании. Класс может служить реализацией набора ин­терфейсов. Итак, под объектно-ориентированным программи-

Page 45: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

рованисм (ООП) понимается программирование, оперирующее конкретными сущностями (объектами) и конкретными видами и родами (классами, абстрактными классами, интерфейсами).

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

Можно считать, что значения кодируются с помощью ко­нечных последовательностей бит, которые называются пред­ставлениями значений. Из равенства представлений следует равенство значений (хотя, строго говоря, не всегда это означает равенство исходных абстрактных сущностей, потому что пред­ставления иногда могут быть неоднозначными2). Из равенства значений следует равенство представлений, если каждой аб­страктной сущности этого вида (типа) соответствует единствен­ное представление. Таким образом, копирование представле­ния возвращает нам значение, отвечающее той же самой аб­страктной сущности, что и исходное.

В свою очередь, объекты также кодируются конечными по­следовательностями бит. Свойство идентичности объектов вы­полняется благодаря уникальным адресам, по которым их пред­ставления расположены в компьютерной памяти. Поэтому, да­же если есть два одинаковых представления, но они располо­жены по разным адресам, они отвечают двум разным объек­там (представляют две разные конкретные сущности). Копи­рование объекта создает новый объект. Более того, исходный объект и копия могут иметь различные представления, так как объект может состоять из других объектов, которые могут на­ходиться в отдельной части памяти и представляться своими адресами. У объекта-копии будут копии этих объектов-частей, занимающие другие адреса.

Вследствие свойства идентичности даже объекты, лишен­ные состояния (например, объекты типа s t r u c t А {> не име­

2Вспомните «проблему 2000-го года».

Page 46: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

ют состояния), в C++ занимают отдельную память (проверьте sizeof(A) и sizeo f (В), где s t ru c t В { А а , Ъ; }).

Понятие равенства разных объектов друг другу приравни­вается к отношению «являться копией». Равные объекты на­ходятся в одинаковом состоянии и имеют равные атрибуты. Отклонение от этого принципа обычно влечет ошибки.

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

Необходимо отметить, что компьютер является реальным физическим устройством, поэтому представления значений и объектов в памяти реального компьютера можно считать кон­кретными сущностями, возможно, моделирующими сущности абстрактные. Соответственно типы значений в языках програм­мирования могут быть более ограничены, чем предполагается для соответствующих абстрактных видов. Например, в языке C++ нет встроенного типа «целое число», зато есть тип int , множество значений которого конечно. Ограниченность реаль­ных представлений ведет к тому, что концепции в C++ мо­гут отвечать как абстрактным родам, так и видам, например, может быть введена концепция «целое число», моделью кото­рой является любой тип, предоставляющий необходимый набор действий над своими значениями и предназначенный для пред­ставления (подмножества) целых чисел, в то время как, строго говоря, абстрактным родом вида «целое число» является, на­пример, «кольцо».

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

Page 47: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

2.2. АТД «Множество» и класс «Множе­ство»

В качестве иллюстрации различия абстрактных типов дан­ных и классов приведем следующий пример3. Опишем «мно­жество целых чисел» в виде АТД.

IntSet type С 2Z тип «множество»empty IntSet = 0 пустое множествоisEmpty IntSet {0,1} проверка на пустотуinsert IntSet X Z IntSet вставить элементcontains IntSet X Z {0,1} проверить наличиеjoin IntSet X IntSet —V IntSet объединениеintersect IntSet X IntSet IntSet пересечение

Это описание отвечает следующему описанию на языке С (интерфейс модуля со скрытой реализацией). Не будем вда­ваться в подробности управления памятью./ / объявление типа "множество" ty p e d e f s t r u c t In tS e tlm p l * In tS e t ;/ / сообщить, что это значение более не нужно void d isp o se ( I n tS e t );/ / получить пустое множество In tS e t em pty ln tSet (void );/ / проверить j является ли множество пустым bool isEmpty ( I n tS e t );/ / построить объединение множества и {п}

3См.: Cook W. R. On understanding data abstraction, revisited / / Proc. of the 24th ACM SIGPLAN conf. on OOPSLA. N. Y., 2009. Vol. 44, iss. 10. P. 557-572.

Page 48: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

In tS e t i n s e r t ( In tS e t , in t n );/ / проверить, принадлежит ли элемент множеству bool con ta in s ( In tS e t , in t n );/ / построить объединение In tS e t jo in ( I n tS e t , I n tS e t) ;/ / построить пересечение In tS e t i n t e r s e c t ( In tS e t , I n tS e t ) ;

Также можно дать формальное описание функционально­го интерфейса объектов, которые могут представлять собой множества целых чисел. Можно считать, что объект реализу­ет интерфейс, если этот объект предоставляет нужный набор функций. Таким образом, интерфейс как тип данных является аналогом индикаторной функции (предикатом принадлежно­сти объекта некоторому множеству объектов).

IntSet interface A интерфейс объектаisEmpty 0 {0,1} проверка на пустотуinsert Z —> IntSet вставить элементcontains Z -> {0,1} проверить наличиеjoin IntSet —► IntSet объединениеintersect IntSet -+ IntSet пересечение

На языке C++ это описание можно приблизить следующим определением (о v i r tu a l и = 0 см. ниже).

s t r u c t In tS e t {v i r tu a l “ In tS e t () •••- 0; v i r tu a l bool isEm ptyO const — 0; v i r tu a l In tS e t* in s e r t ( in t n) const = 0; v i r tu a l bool co n ta in s ( in t n) const — 0; v i r tu a l In tS e t* j o i n ( I n tS e t*) const = 0; v i r tu a l In tS e t* i n t e r s e c t ( I n tS e t*) const = 0;

};

Теперь мы легко можем построить классы IntSet Join («объ­единение») и IntSetlntersect («пересечение»), опирающиеся на функциональные описания методов интерфейса «множества».

Page 49: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

IntSetJoin : class =si, s2 : IntSetisEmpty = si.isEmpty A s2.isEmptyinsert(n) — new IntSetJoin(sl — this, s2 — {n})contains(n) = sl.contains(n) V s2.contains(n)join(s) = new IntSetJoin(sl — this, s2 = s)intersect (s) = new IntSetlntersect (si = this, s2 = s)

IntSetlntersect class =si, s2 IntSetisEmpty = si.isEmpty V s2.isEmptyinsert(n) = new IntSetJoin(sl = this, s2 = {n})contains (n) = sl.contains(n) A s2.contains(n)join(s) = new IntSetJoin(sl = this, s2 = s)intersect(s) = new IntSetIntersect(sl = this, s2 = s)

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

2.3. Основные понятия и принципы

Если используемые объекты являются отражением объек­тов предметной области решаемой задачи (домена), то такие объекты называют доменными. Объекты, роль которых заклю­чается в объединении и передаче родственных данных, назы­вают объектами передачи данных (от англ. data transfer object, DTO).

Page 50: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Аналогично инвариантам циклов в структурном програм­мировании в ООП используется понятие инварианта класса — свойства, необходимо выполняющегося для объектов некото­рого класса, находящихся в корректном состоянии. Например, инвариантом объекта класса «населенный пункт» может быть условие «население неотрицательно».

Методология ООП стремится увеличить подобие програм­мы и домена. С точки зрения собственно программирования ООП можно считать развитием структурного и модульного (понимая классы как развитие модулей) программирования. В процессе разработки накапливается набор программных ком­понент, которые иногда могут быть использованы повторно в других проектах, что может упростить их разработку, поэтому одной из задач в процессе разработки становится облегчение повторного использования. Принципы ООП направлены в том числе и на это. Впрочем, на практике важнее оказывается воз­можность развития программного проекта большого размера, разработкой которого занимаегся много людей. Для написания программы в тысячу строк, как правило, нет никакого смысла использовать полный «заряд» методологии ООП и занимать­ся проектированием (хотя всегда может быть полезно сделать схематический набросок решения на бумаге). Это затрудняет демонстрацию ООП на примерах, так как эти примеры долж­ны быть достаточно объемными.

2.3.1. Инкапсуляция

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

Page 51: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

2.3.2. Наследование

Новые классы могут расширять ранее определенные клас­сы, выделяя некоторые важные возможные подмножества объ­ектов (как правило, обладающие дополнительными атрибута­ми). В C++ наследующий класс фактически является надстрой­кой над классом-базой, объект классагнаследника неявно вклю­чает в себя объект класса-базы.

Классы-наследники в полной мере предоставляют пользо­вателям интерфейс класса-базы (выполняют его контракт). До­полнительный интерфейс для классов-наследииков (возможно, более низкоуровневый) размещается в protected-секции.

2.3.3. ПолиморфизмПри вызове метода выбор конкретной функции, его реа­

лизующей, выполняется не во время компиляции программы (раннее связывание), а во время ее исполнения (позднее связы­вание). Позднее связывание выполняется как переход по хра­нящемуся в памяти указателю, вместо перехода по «зашитому» в код адресу (как это происходит в случае раннего связывания).

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

Принцип подстановки4: пусть свойство q(x) является свой­ством, верным относительно объектов х некоторого класса X , тогда q(y) также должно быть верным для объектов у класса Y , если Y — класс-наследник класса X.

В C+-I ключевое слово v i r tu a l позволяет указать функции- члены, подлежащие позднему связыванию (виртуальные функ­ции). Позднее связывание позволяет выполнять код, соответ-

4См.: Liskov В. Data abstraction and hierarchy / / Add. to the proc. on OOPSLA. N. Y., 1987. Vol. 23, iss. 5. P. 17-34.

Page 52: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

ствующий реальному классу объекта, а не типу указателя или ссылки на объект в месте вызова, известному компилятору. Нередко в C++ только виртуальные функции называют соб­ственно «методами». Компилятор реализует позднее связыва­ние для виртуальных функций автоматически. Для этого в объект добавляется скрытое поле — указатель на таблицу вир­туальных функций, которая состоит из указателей на функ­ции. объявленные виртуальными в классе этого объекта. В С аналогичный эффект может быть достигнут включением ука­зателей на функции в качестве полей структуры и инициали­зации их «вручную» при создании объекта того или иного ти­па. Указатель на таблицу виртуальных функций может играть роль поля типа — специального значения, позволяющего опре­делять реальный тип объекта во время исполнения.

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

• контравариантность классов параметров методов — ме­тоды наследника не могут сужать множества значений принимаемых параметров;

• ковариантность классов значений, возвращаемых мето­дами, — методы наследника не могут расширять множе­ства значений возвращаемых значений.

Рассмотрим пример, иллюстрирующий контравариантность и ковариантность.c l a s s ComnioiiMaterial { . . . );c l a s s Spec lMater ia l : pub lic CommonMaterial { . . . }:c l a s s RarcMaterial : pub l ic SpeclMateria l { . . . };c l a s s CommonProduct { . . . };c l a s s SpeclProduct : pub l ic CommonProduct { . . . }; c l a s s RareProduct : pub li c SpeclProduct { . . . }; c l a s s Factory { pub l i c :

v i r t u a l SpeclProduct* produce (Spec lM ate r ia l* ) ;};

Page 53: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

c la ss R areFactory : p u b lic Factory { p ub lic :

/ / контраѳариантный и ковариантный метод RareProduct * produce ( CommonMaterial *);

/ / SpeclProduct и SpeclMater ial тоже подошли бы };c la ss FlawedFactory : p u b lic Factory { p u b lic :

/ / нарушение контравариантности SpeclProduct* produce( R arcM aterial *);/ / нарушение ковариантности CommonProduct* produce ( S p eclM ateria l *):

};

Термин «ковариантность» отражает тот факт, что направ­ления отношений наследования классов, содержащих метод, и классов возвращаемых объектов совпадают, «Ксштравариант- ность» соответствует обратному направлению наследования.

«Ко»: RareFactory —> FactoryRareProduct -» SpeclProduct.

«Контра»: RareFactory —> FactoryCommonMaterial <— SpeclMaterial.

Классическим примером нарушения принципа подстановки является проблема взаимоотношения между классами Круг и Эллипс. Формально круг является частным случаем эллипса, что соответствует наследованию крута от эллипса. Но с точки зрения реализации эллипс описывается большим объемом ин­формации, чем круг: две полуоси (и, возможно, угол поворо­та) против радиуса. Любое направление наследования наруша­ет принцип подстановки, поэтому при возникновении подобной ситуации следует перестроить отношеішя между классами.

В языке C++ не проводится различия между понятиями «интерфейс» и «класс», равно как и между понятиями «ре­ализация интерфейса» и «наследование класса». Полезно раз­личать все эти понятия если не на уровне программирования, то на уровне проектирования. Аналогом интерфейса в C++ мо­жет служить абстрактный класс.

Page 54: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Лбст.рактный класс является отражением некоторого до­статочно общего понятия (существование объектов собственно этого класса не имеет смысла) и должен использоваться только как базовый для классов, на основе которых уже могут быть созданы объекты. Абстрактный класс отвечает конкретному роду, в то время как «конкретный» класс отвечает конкретно­му виду.

В C++ абстрактным классом считается класс хотя бы с од­ной чисто виртуальной функцией (их также называют аб- страктными методами). Класс-наследник должен дать опре­деления для унаследованных чисто виртуальных функций.

Объявления чисто виртуальных функций оформляются с помощью суффикса = 0:

v i r t u a l void childC lassD efinesM e () = 0;

До тех пор пока не даны определения для всех унаследован­ных чисто виртуальных функщій, объекты класса создавать будет нельзя.

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

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

Page 55: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

2.3.4. О ф орм ление классов и интерф ейсов в U M L

Unified Modeling Language (UML) представляет собой визу­альный язык объектно-ориентированного проектирования5.

Диаграмма классов UML (пример на рис. 2.1) предназначе­на для описания отношений между классами (и интерфейсами) проекта, а также их атрибутов и методов, и является одним из основых видов диаграмм UML. Ниже показаны код и соответ­ствующие элементы диаграммы классов.

c la ss MyClass { p r iv a t e :

in t m yP riva te ; void onlySelfCanDo ();

p ro te c te d :bool m yP ro tected ; bool onlyChildCanDo ():

p u b l ic :s tr in g myPublic ;void d o lt (const s tr in g &what):

};s t r u c t IMovable {

v i r tu a l void move( f lo a t tim e) = 0;};s t r u c t IT ranspo rt {

v i r tu a l void load ( o b jec t *) = 0; v i r tu a l ob jec t* un load() — 0; v i r tu a l bool em pty() const = 0:

};c la ss Vehicle : p u b lic IM ovable. p u b lic IT ra n sp o rt{ A ••• * / };

UML-диаграммы могут не содержать некоторых деталей или вовсе опускать значительные части проекта. Их назначе­ние — представить вид на него под некоторым углом зрения, и таких «видов» может быть много. Кроме того, UML не при­вязан к конкретному языку программирования, поэтому дета-

5См.: Буч Г., Рамбо ДжЯкобсон И. Язык UML : рук. пользо­вателя. 2-е изд. М.. 2006.

Page 56: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

IMovable+move(time)

ITransport+load(object) +unload(): object +empty(): bool

Vehicle

_________MyClass-myPrivate: int #myProtected: bool+myPublic: string______-onlySelfCanDoO #onlyChildCanDo(): bool +dolt(what: string)

Рис. 2.1. Оформление классов в UML

ли языка могут быть не отражены (например, отсутствие кон­струкции «интерфейс» и даже различие между виртуальными и невиртуальными функциями в C++).

2.3.5. Виды отнош ений м еж ду классами

На рис. 2.2 показано, каким образом отображаются отно­шения между классами в UML.

• Наследование (отношение is-а): класс А является наслед­ником класса В.

• Агрегация (отношение has-a): объект класса А может со­держать объекты класса В (также «агрегация по ссыл­ке»).

• Композиция (усиленный вариант агрегации): объект класса А включает объекты класса В как свою необхо­димую часть, управляя временем их жизни («агрегация по значению»).

• Использование (направленная ассоциация): методы клас­са А используют объекты класса В (В может «не знать» об А).

Page 57: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

наследование использование

Vehicle ----------- ч> ЮгіѵаЫе Саг ownership Person

реализация интерфейса ассоциация

Car <С>— ”— Passenger Vehicle ♦ — - Engine1 0..п 1 1

агрегация композиция

Рис. 2.2. Оформление отношений между классами в UML

• Ассоциация (взаимное использование): классы А и В «знают» друг о друге, используют объекты друг друга.

Продемонстрируем эти отношения на примере класса «ав­томобиль», возможного в некоторой системе моделирования дорожного движения. Автомобиль является транспортным средством (наследование), у автомобиля обязательно есть дви­гатель и колеса (композиция), автомобиль может везти пасса­жиров (агрегация), автомобиль имеет владельца (использова­ние или ассоциация), автомобиль взаимодействует с объектами на дороге (ассоциация).[у] (2.1) Допустим, требуется построить систему, связанную с моделированием дорожного движения. Выделите набор клас­сов доменных объектов. Хорошим начальным приближением служит список существительных (словарь предметной обла­сти), используемых для описания различных ситуаций на доро­ге. Составьте схему отношений между ними, определите виды отношений.

2.3.6. Геттеры и сеттеры

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

Page 58: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

позволяющий установить новое значение поля данных, называ­ют сеттером. Когда некоторые данные получают интерфейс в виде пары геттер-сеттер, то эти данные называют свойством (хотя нередко «свойством» называют любое поле данных объ­екта).

В целом методы, не изменяющие состояние объекта, назы­вают аксессорами или селекторами, и в C++ они помечаются модификатором const, например:

v i r t u a l in t g e t ld ( ) const = 0;

Методы, изменяющие состояние, называют мутаторами или модификаторами, например:

v i r t u a l void nextStep (double timeStamp) — 0;

Геттер является частным случаем метода-аксессора, а сет­тер — частным случаем метода-мутатора. В тривиальном слу­чае пара «геттер-сеттер» может казаться (или действительно быть) избыточной:

c la s s Person { p u b l ic :

const s tring& getNameQ const{ r e tu rn name; }void setName( s tr in g value){ name = move (v a lu e ) ; }

p r i v a t e :s tr in g name;

};

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

void Person :: setName( s tr in g value) {i f ( value . empty ()) throw "emptywnameM ; name -- m ove(value):

}

Page 59: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Более того, свойство может быть реализовано неявно (не храниться в виде значения поля). Например, извлекаться из базы данных.

c la ss Person { p u b l ic :

s tr in g getNameQ const{ re tu rn d b . g e tF ie ld (" P e rso n s" , "Name", k ey ); } void setName( s tr in g value) {

i f ( value . empty ()) throw "emptywname" ; db. s e tF ie ld ("P e rso n s” , "Name", key, v a lu e ) ;

}p r iv a t e :

DataBaseKey key ;};

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

2-3.7. SOLID

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

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

Page 60: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

решения и выбрать вариант, наилучшим образом отвечающий этим критериям качества.

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

К сожалению, как правило, получается, что «просто» не значит «легко». В качестве критериев качества ООП реше­ния могут выступать предложенные Р. Мартином принципы SOLID.

Принцип одной ответственности (Single responsibility prin­ciple, SRP): каждый класс должен иметь строго определенную зону ответственности и не иметь лишних методов, не отвечаю­щих этой зоне ответственности.

Это позволяет изменять классы в соответствии с изменени­ями требований к программному решению, независимо друг от друга. Таким образом, «ответственность» — это причина для изменения. Рассмотрим пример нарушения SRP. Пусть есть класс Rectangle.

c la s s R ectangle {double area () c o n s t; / / площадь void draw() c o n s t; / / нарисовать / / ...

};

Недостаток этого класса в том, что он отвечает сразу за две зоны ответственности: геометрию (area) и графику (draw). По­этому в соответствии с SRP его следует разделить на чисто «геометрический» класс и класс, который будет отвечать за рисование, опираясь на представление прямоугольника, задан­ное «геометрическим» классом.

Принцип открытой закрытости (Open/closed principle, ОСР): классы должны быть открыты для расширения, но за­крыты для модификации.

Page 61: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

С другой стороны, использование полей типа в связке со sw itch-case вместо виртуальных функций является примером нарушения ОСР. Если класс объявляет хотя бы одну вирту­альную функцию, то его объекты содержат неявное поле — указатель на таблицу виртуальных функций, определяемую классом объекта, который может использоваться в качестве по­ля типа, однако такое его использование также может приво­дить к нарушению ОСР. В C++ приведение типов указателей и ссылок внутри иерархии наследования может производиться с помощью ключевого слова dynamic_cast. При невозможно­сти приведения вариант dynamic__cast с указателем возвраща­ет n u llp tr , вариант со ссылкой бросает исключение.

Base b o b j;Derived dobj ;Base *p = fcbobj , *q = &dobj ;Derived *ql — dynam ic_cast< D erived*> (q); / / успешноDerived *pl = dynam ic_cast< D erived*> (p); / / n u llp trDerived &p2 = dynam ic_cast<D erived& >(*p); / / ошибка

Нарушением ОСР является использование dynamic_cast в кас­кадном if-e lse : такой код придется изменить при добавлении новых классов-наследников, иначе возникнет «мина замедлен­ного действия»:

i f (au to р = dynamic__cast<DerivedA*> (o b j)) {/ / obj является указателем на объект DerivedA ,/ / и с ним можно работать через р как с DerivedA

} e lse i f (au to p = dynam ic_cast<D erivcdB*>(o b j) ) {/* работаем с DerivedB *p — obj * /

} e lse if (au to p = dynam ic_cast<D erivedC*> (o b j)) {/* работаем с DerivedC *p = obj * /

} e lse { /* неизвестно * / }

Page 62: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

Принцип подстановки Б. Дисков (Liskov substitution prin­ciple, LSP): см. выше в подразделе «Полиморфизм».

Принцип разделения интерфейсов (Interface segregation prin­ciple, ISP): каждый интерфейс должен отвечать одной роли, пользователи классов не должны зависеть от интерфейсов, ко­торыми они не пользуются. Этот принцші можно считать SUP, примененным к интерфейсам. Пример нарушения ISP:

s t r u c t DataConnection {v i r tu a l void connect (co n st string& ) = 0; v i r tu a l void d isconnect () = 0;v i r tu a l bool send (ch ar) — 0; / / отправить байт v i r tu a l bool recv(char& ) = 0; / / принять байт

};

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

s t r u c t Connection { / / соединениеv ir tu a l void connect (co n st string& ) = 0; v i r tu a l void d isconnect () - 0;

};s t r u c t DataChannel { / / канал передачи данных

v ir tu a l bool send (ch ar) — 0; / / отправить байт v i r tu a l bool recv(char& ) = 0; / / принять байт

};c la ss RemoteFile

: pub lic Connection , p u b lic DataChannel{ ••• };

Page 63: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Принцип обращения зависимостей (Dependency inversion principle, DIP): высокоуровневые компоненты должны опи­раться на абстракции низкоуровневых компонент, а не исполь­зовать их непосредственно. При этом абстракции моделируют­ся не на основе поведения низкоуровневых компонент, а на ос­нове нужд высокоуровневых компонент и размещаются вместе с высокоуровневыми компонентами.

Сходством с DIP обладает принцип отделенного интерфей­са (separated interface principle, SIP), на котором основана биб­лиотека iostreams (часть Стандартной библиотеки Cf+). SIP предполагает независимость отделенного интерфейса как от низкоуровневых, так и от высокоуровневых компонент, что по­вышает пригодность первых для повторного использования.

2.4. Паттерны ООП

Паттерном (от англ. pattern — образец) называют некото­рый характерный способ организации элементов программы, часто используемый для достижения определенного эффекта. Паттерны подсказывают разумный подход к решению типич­ных задач проектирования и имеют общепринятые названия, используемые как термины. Характерный набор паттернов за­висит от конкретного языка программирования: если опреде­ленный механизм поддержан непосредственно на уровне языка, то он не считается паттерном. Например, С не предоставляет средств поддержки наследования и полиморфизма, их имита­ция может считаться паттерном, в то время как в C++ они есть и паттернами уже не считаются.

Паттерны ООП определяют способ организации структуры и взаимодействия классов. Их принято разделять на три кате­гории: паттерны создания (creational), структуры (structural) и поведения (behaviorial).

Page 64: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

2.4.1. П аттерны создания

Иногда возникает необходимость в создании глобальных объектов, которые существуют в единственном экземпляре (на­пример, объект Приложение или Система), соответствующий паттерн называется одиночка (синглтон — от англ. singleton). Для создания таких объектов в C++ следует: а) запретить про­извольное создание объектов, скрыв конструкторы (для этого можно поместить их в секцию p riv a te ); б) обеспечить созда­ние единственного экземпляра. Последнего можно добиться с помощью статической функции-члена (этот подход называют «синглтон Мейерса», возможны и другие подходы).

c la ss A pp lication {A pplication () ; / / скрыт A pp lication (co n st A p p lic a tio n ^ );

p u b l ic :s t a t i c A pplication& g e tO b jec tQ {

s t a t i c A pp lica tion ap p ; re tu rn app ;

/ / при первом вызове getO bject ()/ / будет вызван A p p lica tio n :: A pp lica tio n ()

}};

В Gh- невозможно определить виртуальный конструктор, однако можно реализовать подобное поведение. Например, можно создать интерфейс «фабрика», объекты реализаций ко­торого будут создавать объекты некоторого продукта. Такой паттерн называется абстрактная фабрика (abstract factory), пример представлен выше в разделе «Полиморфизм» в каче­стве иллюстрации ко- и контравариантности. Метод, порож­дающий объекты-продукты, называют фабричным методом (factory method) или даже виртуальным конструктором.

Особую роль играет конструктор копирования. Паттерн на основе фабричного метода, реализующего копирование, имеет собственное название: прототип (prototype).

Page 65: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

s t r u c t IC loneable {v i r tu a l “ IC loneab le () {}v i r tu a l IC loneable* clone () const — 0:

>;s t r u c t Something : IC loneab le {

Something* clone () co n st { re tu rn new Som ething(* th i s ); }

};

В метод прототипа можно «примешивать» к классу, применяя паттерн CRTP (curiously recurring template pattern). CRTP предполагает наследование от класса-шаблона, которо­му в качестве параметра передается сам класс-наследник.

te m p la te < c la ss Derived> s t r u c t Cloneable : IC loneab le {

Cloneable<D erived>* clone () const { r e tu rn new Derived

(* s ta tic_ _ ca st< co n s t D eriv ed * > (th is ) ) ; }};s t r u c t Something : C loneable<Som ething>{ . . . }: / / clone уже определять не надо

[у] (2.2) Используя CRTP, реализовать класс-шаблон Sing- leton<T>, делающий своего наследника, класс Т, «одиночкой».

2.4.2. П аттерны структуры

Адаптером (adapter) или оберткой (wrapper) называют объ­ект, «склеивающий» два разных интерфейса. Особым случаем адаптера можно считать паттерн pimpl (характерен для Сь+).

Pimpl (private implementation). Данный паттерн позволяет максимально разделить клиентский интерфейс класса и его ре­ализацию, которая выносится в отдельный, скрытый от клиен­та класс. В .h файле публикуется класс-обертка, управляющий объектом-реализацией через указатель и содержащий внешний интерфейс к этому объекту. Собственно реализация «спрята­на» в отдельный .срр файл. Такой подход также является удоб­ным для создания объектно-ориентированных оберток поверх

Page 66: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

необъектных библиотек (например, написанных на С), реали­зация которых в исходном коде даже может быть недоступна.

c la ss Window {WindowDesc *wnd; / / указатель на pim pl—объект

pu b lic :Window (); / / реализация в . срр Window (co n st Window &); v i r tu a l ~Window (); void show (bool v i s ib l e = t r u e ) ; void s e t T i t l e (co n st s t r i ng&) ;/ / •••

};Если применить паттерн адаптер «сам к себе», создав класс-

наследник некоторого базового класса, содержащего ссылку на объект этого же класса, то получим паттерн декоратор (decora­tor). Декоратор предоставляет способ расширения функцио­нальности классов в ситуации, когда использовать наследова­ние не представляется разумным (из-за комбинаторного взры­ва вариантов наследников и загромождения иерархии классов). На рис. 2.3 и 2.4 приведен пример декоратора6.

2.4.3. П аттерны поведения

Итератор (iterator) — объект, позволяющий перебирать эле­менты из некоторого набора (коллекции). Объект, хранящий коллекцию объектов, по запросу создает объект итератора, уста­новленный на первый объект в коллекции. Клиент последова­тельно вызывает метод next, перебирая объекты коллекции по одному. Итератор может сообщать об окончании коллекции с помощью специального метода empty или попросту возвращая нулевой указатель при очередном вызове next.

Итератор позволяет скрыть реализацию хранилища объек­тов. Более того, при необходимости объекты могут создавать­ся итератором. Дальнейшее применение паттерна декоратор

6См.: Mössenböck H. Object-Oriented Programming in Oberon-2. В., 1995.

Page 67: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Рис. 2.3. Ситуация комбинаторного взрыва

Frame

+draw()< -

«decorates»

TextFrame GraphicFrame Decorator

+draw() +draw() #component+draw()

ScrolledPecorator

+draw()

BorderedDecorator

+draw()

draw border; ^ component.draw();

Рис. 2.4. Результат применения декоратора

Page 68: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

к итератору позволяет организовать фильтры и обработчики коллекций.|~у] (2.3) Напишите реализацию динамического массива, ко­торая позволяет перебрать содержимое массива с помощью объекта-итератора.

При использовании итератора клиент, как правило, переби­рает все оставшиеся элементы коллекции с помощью простого цикла. Вместо того чтобы отделять действие «выбрать следу­ющий элемент» в метод next, можно отделить этот цикл це­ликом: коллекция лучше «знает», каким способом перебирать свои элементы. В этом случае коллекция может предоставить метод accept ( IV is ito r &ѵ), который передает каждый эле­мент коллекции объекту ѵ, вызывая его метод v i s i t . Данный паттерн (рис. 2.5) называется посетитель (visitor). Например, описать обход дерева с помоіцью посетителя можно намного проще и естественней, чем с помощью итератора.

Посетитель может реализовать интерфейс для обработки объектов разных типов. В свою очередь, класс можно считать коллекцией своих полей, тогда класс вызывает методы посе­тителя для своих полей. Так можно описывать объекты заг ранее неизвестных типов. Обрабатываемая структура может быть не линейной коллекцией, а сложной иерархией объектов: сами элементы могут содержать вложенные элементы и вызы­вать посетителя для них, корректно представляя древовидную структуру. Посетитель может, например, сохранять состояния объектов на диск (выполнять сериализацию), при этом разные посетители могут реализовывать разные форматы файлов.|~У~| (2.4) Реализуйте изображенную иерархию классов. Json- Formatter должен формировать текстовое представление в фор­мате JSON. Можно воспользоваться следующей (упрощенной) грамматикой, где число является текстовым представлением double, а строка оформляется аналогично строковому лите­ралу С (в двойных кавычках с escape-последовательностями). Значение null может соответствовать n u llp tr . Объекты пред­стают в виде словарей с произвольными типами значений.

Page 69: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

lElement+accept (IVisitor)

Ellipseelems: lElementn+accept (IVisitor)

Rectangleelems: lElementn♦accept (IVisitor)

Text♦accept (IVisitor)

Linetext: Text♦accept (IVisitor)

—i

_ J

IVisitor

+visit (Line)+visit (Rectangle) +visit (Ellipse)4-l/IC f# / T o v f l* V lUli I I S/Al J

JsonFormatter♦visit (Line)♦visit (Rectangle) ♦visit (Ellipse) ♦visit (Text)

Рис. 2.5. Паттерн «посетитель»

Page 70: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

JS O N —> значение

значение —> null | булево \ число | строка | массив | объект

null —> «null»

булево -> «true» | « false»

массив «[» [значение {«,» значение}] «]»

объект —> «{» [пара {«,» пара}] «}»

п ар а -* имя «:» значение

и м я —у строка

Предположим, что взаимодействие объектов в системе про­изводится путем передачи сообщений. Есть объекты — источ­ники и объекты — получатели сообщений. При этом один ис­точник может передать сообщение группе получателей. Наблю­дателем (observer) будем называть интерфейс получателя со­общений. Передача сообщений популярна в библиотеках грат фического интерфейса пользователя. Клик мышкой, нажатие клавиши, изменение размеров окна — все это примеры событий графического интерфейса, уведомление о которых может про­изводиться посредством механизма передачи сообщений. Ис­точники сообщений хранят списки наблюдателей, которые бу­дут получать сообщения.

tem p la te c c l a s s Message> s t r u c t IObservcr {

v i r t u a l ~IObserver () {}v i r t u a l void update ( cons t Messaged) = 0;

};tem p la te <c l as s Message> c l ass McssagcSource {

std :: set<IObserver*> observer s ; publ i c :

void a t t ach ( IObserver &obs){ o b s e r v e r s . i n s e r t (&obs); }

Page 71: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

void detach ( IObserver fcobs){ o b s e r ve r s . erase(&obs); } void not i fy (const Message &msg) {

fo r (auto o b s : observers ) obs->update (msg) ;

}};

Тип сообщения вынесен в параметр шаблона. Используется стандартный контейнер set (см. главу 3). В программе приме­няется форма цикла for , введенная в ISO С++11, позволяющая пройти по всем элементам контейнера, если для него определе­ны функции begin и end. Случай, когда update непосредственно вызывает notify, может привести к ситуации неограниченной рекурсии с последующим переполнением стека. Тогда новые сообщения можно предварительно помещать в очередь, затем, в определенный момент, notify вызывается для извлекаемых из очереди сообщений в «цикле обработки событий».

2.4.4. МѴС и М Ѵ Р

При проектировании приложений полезно максимально раз­делять пользовательский интерфейс, выполняющий визуали­зацию данных и объектов приложения и собственно эти дан­ные (доменные объекты). Совокупность классов, реализующая элементы представления данных для пользователя, называют видом (view). Классы, описывающие модель мира, данные, ко­торыми оперирует приложение, составляют модель (model).

В случае простого приложения данного разделения может быть достаточно. В ином случае важно обеспечить взаимодей­ствие между ними, обеспечив низкий уровень сцепления. Наи­более известный (классический) способ состоит в добавлении ответственного за управление взаимодействием компонента — контроллера (controller, в целом паттерн (рис. 2.6) называет­ся model-view-controller, МѴС). Как правило, за контроллером скрывается обработчик пользовательского ввода, «переводя­щий» его в вызовы методов модели и вида и реализующий,

Page 72: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Рис. 2.6. Паттерн МѴС

таким образом, логику приложения. Уведомление реализуется с помощью паттерна наблюдатель.

В некоторых случаях МѴС не является удобным способом организации взаимодействия модели и вида. Другой популяр­ный способ (рис. 2.7) состоит в «линеаризации» взаимодей­ствия и полном расцеплении модели и вида с помощью проме­жуточного компонента — представителя (presenter, соответ- ственно паттерн называют МѴР). Например, при организации веб-службы вид (клиентская часть) реализуется в браузере, мо­дель — в базе данных на сервере, собственно серверная ком­понента веб-службы, к которой обращается браузер, является « представителем ».

2.5. Множественное наследование

Множественным наследованием называют использование более одного класса в качестве непосредственных базовых клас­сов. Ряд языков (например, Java и С #) допускают лишь оди­ночное наследование для «обычных» классов (имеющих состо­яние) и множественное наследование (реализацию) для интер­фейсов, ближайшим аналогом которых в C++ являются чисто абстрактные классы. C++ допускает множественное наследова­ние классов с соблюдением следующих условий:72

Page 73: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Модель/ \ ответ1Азапрос VПредставитель/ \ ответ1

запрос VВид

Рис. 2.7. Паттерн MVP

• класс не должен непосредственно наследовать от одного и того же класса более одного раза (нельзя: class В: А,А);

• класс ни в какой форме не должен наследовать самого себя.

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

s t r u c t А { i n t х; };s t r u c t В { f l oa t х; };s t r u c t С : А, В {

/ / унаследовано два разных х , необходимо/ / явно указывать подобъект, содержащий х С() { А: :х = 42; В : : х = -1 .5 ; }

};

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

73

Page 74: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

доступен только при указании корректного «адреса» — пути в иерархии наследования, по которому он расположен. Напри­мер, ниже в объекте класса D есть два подобъекта повторно унаследованного класса А: подобъект В::А и подобъект С::А:s t r u c t А { i n t х; }; s t r u c t В : А {}; s t r u c t С : А {}; s t r u c t D : В, С {

/ / два разных подобъекта А — два разных х D() { В :: А :: х — 0; С : : А : : х = 1; }

};Приведение указателя на D к указателю на А возможно через промежуточный указатель на В или на С. Обратное приведение также требует получения промежуточного указателя на В или С.

Виртуальным наследованием называют объединение под- объектов повторно унаследованных классов в один подобъект. Для этого перед именем базового класса требуется поставить ключевое слово v ir tu a l . Рассмотрим тот же пример, что и вы­ше, но уже с использованием виртуального наследования.s t r u c t А { i n t х; };s t r u c t В : v i r t u a l А {};s t r u c t С : v i r t u a l А {};s t r u c t D : В, С {

/ / один подобъект А — один х D() { х = 0; }

};На рис. 2.8 показана схема наследования, иллюстрирующая

виртуальное наследование на примере интерфейса ICloneable. Благодаря характерному виду схемы такая модель наследова­ния также называется ромбовидным наследованием.

При наличии нетривиальных конструкторов у виртуально­го базового класса за инициализацию разделяемого подобъекта отвечает «нижний» класс (в примере выше — D). В C++ вир­туальное наследование обычно применяется при использова­нии абстрактных классов, предоставляющих некоторую базо-

Page 75: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

ICIoneable

.V у:;Receiver Transmitter

Tranceiver

Рис. 2.8. Ромбовидное наследование

вую функциональность своим наследникам или оішсывающих общие интерфейсы, которые легко могут быть объединены в листовых наследниках.

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

Page 76: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Глава 3

Элементы Стандартной библиотеки

Стандартная библиотека С+-+ включает:

• вспомогательные компоненты (language support), исполь­зуемые в других частях библиотеки;

• обработку ошибок (diagnostics), включая стандартные классы исключений (< exception>, <stdexcept>);

• функционал общего назначения (general utilities), включая распределители памяти (allocators), кортежи (<tuple>), обертки функций (std:function), инструменты для работы с датой и временем (<chrono>), массивы бит (<bitset>);

• строки (<string>);

• средства локализации (localization);

• контейнеры и итераторы;

• обобщенные алгоритмы (< algorithm>, <numeric>);

Page 77: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

• средства для поддержки вычислений (numerics), вклю­чая комплексные числа (<complex>), векторную арифме­тику (<valarray>) и генераторы псевдослучайных чисел (<random>);

• средства ввода-вывода, включая iostrearns;

• регулярные выражения (<regex>);

• атомарные операции в параллельных алгоритмах;

• нити исполнения (threads) и механизмы синхронизации;

• Стандартную библиотеку С.

Под Стандартной библиотекой шаблонов (Standard Template Library, STL) понимаются контейнеры + итераторы + алгорит­мы + некоторые вспомогательные компоненты, используемые в связке с контейнерами и алгоритмами. Строки и ѵаіаггау близ­ки контейнерам, а алгоритмы из < numeric> используют ите­раторы, поэтому их часто рассматривают вместе с STL, хотя формально и не относят к ней.

В рамках данной книги состав Стандартной библиотеки C++ невозможно охватить даже поверхностно. Ее подробное описание можно найти в источниках, указанных в дополни­тельной литературе. Здесь же мы рассмотрим некоторые эле­менты Стандартной библиотеки, демонстрирующие примене­ние подходов обобщенного и объектно-ориентированного про­граммирования .[у] (3.1) Изучите и проанализируйте зависимости меж­ду стандартными классами исключений, определенными в <exception> и <stdexcept>.|~у] (3.2) Изучите и проанализируйте зависимости меж­ду стандартными классами-шаблонами (<ios>) ios_base, basic_ios, (<ostream>) basic__ostream, (<istream>) basic _is- t ream, basic _ iost ream, (< ss tream >) basic _ ist ringst ream,basic__ostringstream, basic_stringstream. Какие реализации

77

Page 78: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

абстрактных классов istream, ostream, iostream помимо опре­деленных в < sstream > предлагает Стандартная библиотека?

3.1. Контейнеры и итераторыКонтейнер — это программный компонент, способный хра­

нить набор значений одного типа. Контейнер предоставляет средства доступа к своему содержимому. В Стандартной биб­лиотеке C++ эти средства доступа строятся на обобщении по­нятия «указатель на элемент массива», которое носит название итератор. В зависимости от внутреннего устройства контей­нера не все характерные для указателей операции могут быть выполнены эффективно на итераторах. Например, при досту­пе к связному списку обращение по числовому индексу может потребовать значительного числа операций. Итераторы могут не поддерживать неэффективные операции. Чтобы выделить характерные виды итераторов, в Стандарте C++ определены категории итераторов. Как правило, итератор нельзя исполь­зовать для модификации структуры контейнера (кроме спе­циальных итераторов-адаптеров) без вызова функций самого контейнера.

3.1.1. Итераторы

Итератор ввода предназначен только для однократного чтения (ввода) последовательности, основная конструкция вы­глядит так:

Value value - * i t ++ ; / / i t — итератор

Итератор можно передвигать на одну позицию вперед (инкре­мент) и разыменовывать (операции * и ->), получая доступ к текущему значению. Итераторы можно сравнивать между со­бой на равенство и неравенство.

Итератор вывода предназначен только для однократной записи (вывода) последовательности. В остальном аналогичен итератору ввода. Основная конструкция выглядит так:78

Page 79: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

* it-f-f = value ;

Однонаправленный итератор является расширением кон­цепций «итератор ввода» и «итератор вывода». Итератор до­пускает многократное чтение и запись линейной последова­тельности, по которой можно двигаться только в одну сторону (как по односвязному списку).

Двунаправленный итератор является расширением концеп­ции «однонаправленный итератор». Итератор допускает дви­жение в двух направлениях: вперед (++) и назад (--).

Итератор произвольного доступа является расширением концепции «двунаправленный итератор». Итератор допускает адресацию по индексу (оператор []). сдвиг в обе стороны на некоторое количество позиций (добавление и вычитание целого числа), вычисление расстояния с помощью вычитания и срав­нение на «меньше» и «больше» (согласованное с расстоянием, которое имеет знак). Итератор произвольного доступа наибо­лее близок концепции указателя. Указатель на элемент массива является итератором произвольного доступа.

Для упрощения работы с итераторами Стандартная биб­лиотека предоставляет ряд средств (заголовочный файл < itera­tor >). Перечислим их.

Класс характеристик iterator_traits<T>. Классом характе­ристик называют класс-шаблон, предоставляющий для своего параметра набор некоторых базовых определений, как прави­ло типов и констант. В случае iterator_traits это набор типов: valuetype — тип значения, на которое указывает итератор; reference — тип ссылки, возвращаемой при разыменовании ите­ратора; pointer — тип указателя, возвращаемого при обращении к объекту итератора через operato r-> ; differencetype — цело­численный тип, представляющий значения смещений итерато­ров относительно друг друга, и, наконец, самый главный тип — iterator_category, являющийся синонимом одного из предопре­деленных теговых классов: in p u tite ra to rta g , output_jterator_ tag, forward_iterator_tag, bidirectional_iterator_tag и random_ac- cess_iterator_tag. Под теговыми классами понимаются пустые

Page 80: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

Класс-шаблон iterator<Category, Т, Distance = ptrdiff t, Poin­ter = T *. Reference = T & > . Здесь Category — один из тегов, пе­речисленных выше, а Т — тип значения, на которое указывает итератор. Данный класс используется в качестве базового при создании других классов итераторов: он добавляет к определе­нию вложенные типы, доступные затем через iterator_ traits.

Вспомогательные функции: advance(p, n), distance(pf q), next (р) и ргеѵ(р). Функция distance вычисляет расстояние между парой переданных ей итераторов (количество применений опе­ратора инкремента к первому итератору до достижения им второго либо обычная разность для итераторов произвольного доступа). Функция advance сдвигает итератор (принимает по ссылке) на заданное число шагов (сдвиг назад определен для двунаправленных итераторов). Функции next и ргеѵ возвраща­ют итератор, сдвинутый соответственно вперед или назад на одну позицию. Также есть перегруженные варианты, прини­мающие число шагов.

Итераторы-адаптеры будут рассмотрены после стандарт­ных контейнеров.

3.1.2. Стандартные контейнеры

Стандартные контейнеры можно разбить на две большие группы: линейные и ассоциативные. В свою очередь, линейные контейнеры можно разделить на связные списки (forward_ list и list) и контейнеры произвольного доступа (deque, vector и array). Ассоциативные контейнеры представлены восемью кон­тейнерами, являющимися комбинациями следующих вариан­тов: множество (*set) или словарь (*іпар), допускающие повто-

Page 81: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

рение элементов (*multi*) или не допускающие, упорядоченные или неупорядоченные (unordered*).

Все контейнеры содержат вложенные типы iterator и const_ iterator, определяющие итераторы чтения-записи и только чте­ния соответственно. Диапазон итераторов, охватывающий со­держимое контейнера, можно получить с помощью функций begin и end, а также cbegin и cend (только const__iterator). Все контейнеры можно проверять на пустоту функцией empty. Кон­тейнер cont пуст, если c o n t . begin () == c o n t . end (), end () воз­вращает итератор, указывающий на условный элемент, нахо­дящийся за последним элементом контейнера. Количество эле­ментов можно получить с помощью функции size (за исключе­нием forward list). Контейнер можно очистить от содержимого вызовом функции clear (кроме array).

В <iterator> также определены свободные шаблоны функ­ций begin, end, cbegin, cend, rbegin, rend, crbegin и crend1, no умолчанию переадресующие вызов одноименным функциям- членам, кроме того, даны определения этих функций для ста­тических массивов и std::valarray. Именно на них (с исполь­зованием механизма ADL) опирается новая форма цикла fo r . Пусть сг — некоторый «контейнер» (в том числе, возможно, статический массив), тогда запись

for (Т X : сг) work(x) ;

семантически эквивалентна записи

{ using std :: begin ; us ing s t d : : e n d ; const auto e - e n d ( c r ) ; for ( auto p = begin ( c r ) ; p != e; ++p){ T X -- *p; work(x); }}

Линейные контейнеры (кроме array) можно заполнить зна­чениями из заданного диапазона вызовом функции assign (ста­рое содержимое будет уничтожено) и изменить их размер функ­цией resize (при уменьшении размера лишние элементы удаля­

1 Последние две пары введены в черновом варианте ISO С++14.81

Page 82: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

ются с конца, при увеличении размера новые элементы добав­ляются в конец).

Прямой доступ к первому элементу контейнера (кроме неупорядоченных ассоциативных контейнеров) можно полу­чить функцией front. Все контейнеры, итераторы которых яв­ляются по крайней мере двунаправленными, предоставляют функцию back для доступа к последнему элементу, а так­же противоположно направленные итераторы reverse iterator и const_reverse_ iterator, соответствующие диапазоны можно по- лзчить с помощью функций rbegin, rend и crbegin, crend.

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

Для управления динамической памятью стандартные кон­тейнеры используют специальные классы, называемые аллока­торы. Аллокатор привязан к типу элемента, который опреде­ляет минимальную единицу управления памятью и предостав­ляет ряд вспомогательных определений. Работа осуществля­ется с помощью четырех основных функций: allocate для вы­деления памяти под заданное количество элементов, deallocate для освобождения памяти, construct для вызова конструктора и destroy для вызова деструктора. Аллокатор должен предостав­лять метафункцию rebind, позволяющую получить «аналогич­ный» аллокатор для элементов другого типа:

t y pe de f typename Alloc :: rebind<U>:: other AllocForU ;

Стандартная библиотека предоставляет allocator<T>, яв­ляющийся оберткой операторов new/new [] и d e le te /d e le te [] - Его можно использовать в качестве модели при написании сво­их аллокаторов.

3.1.3. Л инейны е контейнеры

Односвязный список forward list<Tr А = allocator<T>> пре­доставляет доступ к элементам типа Т через однонаправлен-

Page 83: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

ный итератор. Для создания узлов список использует алло­катор, полученный через A::rebind. Особенность односвязного списка состоит в том, что элементы можно добавлять и уда­лять только после заданной позиции: insert after — вставить элемент; emplace_after создать новый элемент, вызвав кон­структор для переданных параметров; erase_after — удалить элемент. Имеется дополнительная фиктивная позиция «перед первым элементом», возвращаемая функциями before begin и cbefore_begin (const__iterator). Элементы можно вставлять в начало: вызов f l.p u s h _ fro n t(ite m ) эквивалентен

f l . i n s e r t _ a f t e r ( f l . before _ begin () , i tem)

аналогично f l .e m p la c e _ fro n t( . . .) эквивалентен

fl . emplacc_af ter ( fl . before_begin () , . . . )

Функция pop front удаляет первый элемент списка. Особенно­стью списков STL также является поддержка более высоко­уровневых операций, что проистекает из невозможности эф­фективного использования одних только итераторов для ре­ализации этих операций: merge сливает два отсортированных списка в один, элементы не копируются, а передаются из пра­вого в левый; splice after — вставляет переданный список це­ликом после указанного элемента; remove — удаляет все эле­менты, значение которых равно заданному; remove_ if — удаля­ет все элементы в соответствии с предикатом; reverse — обра­щает порядок элементов; unique — удаляет все подряд идущие дубликаты; sort — сортирует список на месте. В целом данные функции аналогичны соответствующим стандартным алгорит­мам (см. ниже).

Двусвязиый список listcT, А = allocator<T>> предостав­ляет доступ к элементам через двунаправленный итератор. В отличие от односвязного списка элементы вставляются пе­ред заданной позицией (функции insert, emplace, splice), до­ступна вставка и удаление с конца (push_back, emplace_back, pop back), удаление элемента, на который указывает итератор (erase). Функции вида *_after отсутствуют.

Page 84: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Дек deque<T, А = allocator<T>> предоставляет доступ к элементам через итератор произвольного доступа. Так же, как и list, позволяет эффективно добавлять и удалять элементы с обоих концов. Для доступа по индексу предназначены две функции: оператор [] и аѣ(индекс). В отличие от первой вторая проверяет индекс и в случае недопустимого значения бросает исключение out_of_range. Контейнер deque допускает выпол­нение вставки и удаления элементов в произвольной позиции аналогично list, однако в случае deque эти операции затрат­ны: могут требовать времени линейного по размеру контейне­ра. Кроме того, необходимо помнить, что вставка и удаление элементов может «испортить» ранее сохраненные итераторы или указатели из-за потенциального перемещения хранимых элементов в памяти (в то время как итераторы списков сохра­няются, если соответствующие элементы не были удалены).

Динамический массив vector<T, А = allocator<T>> предо­ставляет доступ к элементам через итератор произвольного до­ступа. В отличие от deque не позволяет вставлять и удалять элементы в начале. Динамический массив гарантирует распо­ложение хранимых элементов подряд в непрерывном участке памяти, адрес которого возвращает функция data. При добав­лении элементов и исчерпании заранее выделенного хранили­ща может быть выделен новый динамический массив большего размера, куда будут перенесены элементы. Старое хранилище при этом удаляется, все сохраненные итераторы «пропадают» и становятся эквивалентны указателям на удаленные объек­ты. Массив позволяет заранее подготовить хранилище доста­точного размера с помощью функции reserve (может вызвать перемещение элементов). Узнать размер хранилища можно спомощью функции capacity. Функция shrink to fit выделяетхранилище размера, равного size, и переносит туда элементы, освобождая незанятую память.

Статический массив arraycT, N> предоставляет доступ к элементам через итератор произвольного доступа и является оберткой над статическим массивом T[N], адрес которого мож­

Page 85: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

но получить функцией data. Адресовать элементы по индексу можно так же, как в случае deque и vector. Изменять количе­ство элементов нельзя, поэтому никаких функций для вставки и удаления элементов array не предоставляет. Функция fill за­полняет массив копиями переданного значения.

3.1.4. Ассоциативны е контейнеры

Все ассоциативные контейнеры поддерживают следующие операции: count возвращает количество элементов, эквивалент­ных заданному; find возвращает итератор, указывающий на некоторый хранимый элемент, эквивалентный заданному, либо end(), если таковых нет; equal_range возвращает пару итера­торов, задающих полуоткрытый диапазон всех хранимых эле­ментов, эквивалентных заданному.

Упорядоченные контейнеры построены па сбалансирован­ном двоичном дереве и опираются на операцию «меньше». Два элемента считаются эквивалентными, если ни один из них не меньше другого. Все упорядоченные контейнеры предоставля­ют доступ к элементам через двунаправленные итераторы и позволяют найти позицию первого элемента, не меньшего ис­комого с помощью функции lower_bound, и первого элемента, большего искомого, с помощью функции upper_bound, поэтому для контейнера ас вызов ac.equal_range() по смыслу эквива­лентен вызову

make_pair ( ас . lower_bound () , ас . upper_bound ())

Упорядоченное множество уникальных элементов set<Kf С = less<K>, А = allocator<K>>. Данный контейнер не позволя­ет изменять хранимые значения «на месте», set<K>:-.iterator и set.<K> :: const _ iterator функционально эквивалентны и воз­вращают const К& при разыменовании. Для того чтобы из­менить хранимый в set объект, его нужно сначала удалить, а затем вставить новый вариант. Вставка элементов произво­дится функцией insert, имеющей несколько вариантов: встав­ка значений из диапазона принимает пару итераторов ввода,

Page 86: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

вставка одного значения с указанием возможного места встав­ки (оптимизация, которую библиотека может игнорировать) и, наконец, основной вариант — вставка заданного значения. По­следний вариант insert возвращает пару (итератор, булевское значение), первый элемент которой указывает место вставлен­ного или найденного значения, второй же позволяет узнать, было значение вставлено (true) или уже находилось во мно­жестве на момент вставки (false). Для двух последних видов insert существуют аналоги emplace hint и emplace, принимаю­щие параметры конструктора и создающие значения «на ме­сте». Удаление элементов выполняется функцией erase, кото­рая принимает итератор или диапазон итераторов, задающие элементы множества, или значение. Первые два варианта воз­вращают итератор, указывающий на элемент, следующий за удаленными. Последний вариант возвращает количество уда­ленных элементов (0 или 1 в случае set).

/ / пример использования erase в цикле: удалить из / / множества все строки с заданной подстрокой void e rase_subs

( s e t < s t r i n g > &s , co n st s t r i ng &subs) { au to p -= s . begin ( ) , pc — s . c nd ( ) ; w hile (p != pe) {

i f (p—>find ( s ubs ) != s t r i ng :: npos) p — s . e r a s e ( p ) ;

e l se+-tp;

}}

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

/ / сортировка деревом с помощью m u ltise t te m p la te e c l a s s Fwdlt>void tree__sort (Fwdlt begin, Fwdlt end) {

ty p e d e f typenamei t e r a t o r _ t r a i t s <FwdIt > :: value_ type VT;

Page 87: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

mul t i set <VT> t ree ( make _ move iterator (begin ) , make_move_i terator ( end )) ;

copy( t r e e . begin () , t r e e . end () , b e g i n );}

Упорядоченный словарь с уникальными ключами mapcK, Т, С = less<K>, А = a!locator<pair<const К, Т > > > хранит зна­чения типа std::pair<const К, Т > , где К играет роль ключа, по которому осуществляется выборка, а Т — хранимое значе­ние. Поэтому при разыменовании итератора поле first позволя­ет прочитать ключ, а поле second предоставляет доступ к хра­нимому значению. Основная операция т а р — индексирование с помощью o pera to r [], которому передается значение ключа. Данный оператор возвращает ссылку на значение, отвечающее переданному ключу. Если в словаре не было значения с таким ключом, то будет создано новое значение (конструктором по умолчанию), ссылка на которое и будет возвращена. Таким об­разом, opera to r [] не позволяет узнать, было значение найдено или создано. Так как индексирование может изменять струк­туру контейнера (создавать новые узлы), оно неприменимо к const map. Пусть map<K, Т> m, тогда запись m[k] = t по смыс­лу эквивалентнаm. i ns e r t (make_pai r (k , Т ( ) ) ) . f i r s t —>second = t

Действительная реализация может быть эффективнее и не со­здавать лишний раз объект Т. Если поведение operato r [] пред­ставляется неудобным, то ему есть по крайней мере две альтер­нативы. Во-первых, можно использовать find, чтобы по ключу получить итератор, указывающий на соответствующую пару (ключ, значение), либо итератор end О , если ключа в слова­ре нет. Во-вторых, можно использовать функцию at, принима­ющую ключ и возвращающую ссылку на значение. В случае отсутствия искомого ключа в словаре at бросает исключение.

Упорядоченный словарь с неуникальными ключами multi­map не определяет o p era to r [] и, по сути, напоминает multiset пар, поиск среди которых ведется только по первому полю (ключу). В качестве примера использования multimap рассмот­

Page 88: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

рим задачу об обращении словаря, хранимого в текстовом фай­ле. Для простоты положим, что словарь состоит из пар слов, упорядоченных по первому слову, слова могут повторяться. В примере используется функция сору из <algorithm> (см. раз­дел, посвященный стандартным алгоритмам).

/ / пара слое — элемент словаря ty p e d e f pa i r<s t r i ng , s t r i n g > Entry ;/ / словарьty p e d e f niul t imap<string , s t r i ng> Dic t ionary;/ / ввод—вывод namespace std {istream& o p e r a t o r » ( i s t r e a m &is , Entry &en){ r e t u r n is » en. f i r s t » en. second; } ostreamfc o p e r a t o r « ( o s t r e a m &os , const Entry &en){ r e t u r n os « en. f i r s t « ’w’ « e n . second ; } istreamfc o p e r a t o r » ( i s t r e a m &is , Dict ionary &d) {

i s t r e a m_i t e r a t o r < En t r y> begin ( i s ) , end; d = Dict ionary ( begin , end); r e t u r n is ;

}ostrcam& o p e r a t o r «

(ostream &os , const Dict ionary &d) { os t r eam_ i t erator <Entry> out (os, " \ n ,f); c opy ( d . beg i n () , d . e n d ( ) , out ) ; r e t u r n o s ;

}}/ / операция обращения словаря Dict ionary reverse (cons t Dict ionary &d) {

Dict ionary r e s u l t ;for (cons t auto &el : d)

r e s u l t . emplace ( el . second , cl . f i r s t ); r e t u r n r e s u l t ;

}/ / чтение—обращение—записьvoid Dict ionaryReversc ( i s t rcam &from , ostream &to) {

Dict ionary read; from » read ; to « r e v e r s e ( read ):

}

Page 89: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

[У] (3.3) Как переделать пример с обращением словаря, что­бы не пришлось хранить одни и те же слова дважды (предпо­лагая, что прочитанный словарь нам больше не требуется и его можно уничтожить в процессе обращения)?

Неупорядоченные контейнеры построены на хэш-таблице (обычно это хэш-таблица списков — «закрытая адресация») и опираются на некоторую хэш-функцию (по умолчанию std::hash < Т > из <functional>) и операцию «равно». Два элемента счи­таются эквивалентными, если их хэши равны и операция «рав­но» возвращает истину. Неупорядоченные контейнеры предо­ставляют доступ к элементам через однонаправленные итера­торы.

К стандартным неупорядоченным ассоциативным контей­нерам относятся классы-шаблоны unordered_set<Kf Н = hash < К > , Е = equal_to<K>, А = a!locator<K>>, unordered_map<K, Т, Н = hash < К > , Е = equal_to<K>, А = allocator< pair<const К, Т > > > , unordered_multiset, unordered_multimap. Они предостав­ляют похожую на аналогичные упорядоченные ассоциативные контейнеры функциональность.|~у~1 (3.4) Изучите отличия интерфейсов неупорядоченных ас­социативных контейнеров от интерфейсов упорядоченных кон­тейнеров.

3.1.5. И тераторы -адаптеры

Класс «обратный итератор» reverse_iterator<lterator> обо­рачивает объект двунаправленного итератора Iterator, обра­щая порядок обхода последовательности, т. е. инкремент об­ратного итератора приводит к декременту базового итерато­ра. Извлечь его можно с помощью функции-члена base. Стан­дартные контейнеры используют reverse_iterator для реализа­ции rbegin и rend. Чтобы обеспечить корректность диапазона [rbegin, rend), базовый итератор сдвинут на одну позицию впе­ред. Таким образом, если г — обратный итератор, то истинно выражение & *r == & *p re v (r .b aseO ).

Page 90: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Класс move_iterator<lterator> является оберткой, подменя­ющей копирующее присваивание перемещением. Создать объ­ект данного класса на месте можно с помощью функции make_ move _ iterator( iter).

Класс back_insert_iterator<Container> является итератором вывода, реализующим операцию записи через вызов функции- члена push_back для контейнера, указатель на который хра­нится в объекте итератора. Создать такой итератор на месте можно с помощью функции back_inserter(container), что быва­ет удобно при сохранении последовательности, размер которой заранее неизвестен (пример см. ниже).

Класс front_insert_iterator<Container> аналогичен предыду­щему, но вызывает функцию push_front. Создать объект на месте можно с помощью front inserter(container).

Класс insert_iterator<Container> похож на два предыдущих, но предназначен для вставки элементов в произвольной пози­ции внутри контейнера, поэтому помимо указателя на контей­нер хранит итератор, задающий позицию вставки в этом кон­тейнере. При записи вызывает insert и обновляет позицию. Со­здать объект insert_iterator на месте можно с помощью функ­ции inserter(container, iterator).

Класс istream_iterator<T, CharT = char, Traits = char_traits <CharT>, Dist = ptrdiff t> является итератором ввода, пред­назначенным для чтения из basic_istream<CharT, Traits>. Ти­пы CharT и Traits обеспечивают возможность использования пользовательских типов символов. Второй из них — класс хаг рактеристик, содержащий ряд базовых типов и операций над символами и передаваемыми по указателю строками. Для стан­дартных символьных типов определены соответствующие вер­сии стандартного шаблона char_traits.

Объект ist ream _ iterator, привязанный в момент создания к потоку, сбрасывается при невозможности прочитать следую­щее значение и становится равным объекту, созданному кон­структором по умолчанию.

Page 91: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Вместе с back_inserter istream _iterator можно использовать для организации считывания последовательности чисел произ­вольной длины с потока ein в контейнер xs:

сору ( i s t ream _ i t e r a t o r e i n t >(cin ) ,is t re am _ i t e r a to r <int >( ) , b a ck_ i ns e r t e r ( xs ) ) ;

Класс ostream_iterator<T, CharT = char, Traits = char_traits <C harT>> является итератором вывода, предназначенным для записи э объект basic_ostream<CharT, Traits>. Кроме указа­теля ца поток вывода итератор хранит указатель CharT* на строку-разделитель, которая выводится после каждой записи (если указатель ненулевой). Выведем последовательность чи­сел xs, разделенную запятыми в поток cout.

copy ( xs . b eg i n () , x s . e n d ( ) .os t ream _ i t e ra tor <i n t > (cout ,

Заметим, что запятая будет поставлена и после последнего вы­веденного числа, что может быть нежелательным. В этом слу­чае последний элемент следует выводить отдельным вызовом. [у] (3.5) Помимо char_traits к классам характеристик можно отнести определенный в <limits> шаблон numeric_limits<T>, где Т — числовой тип. Целью этого класса является обобщение и замена макросов С, описывающих особенности числовых ти­пов (например, из <float.h>). Изучите этот класс. Сопоставь­те его возможности с соответствующими возможностями Стан­дартной библиотеки С.

3.2. Алгоритмы и функторы

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

Page 92: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Функтором в C++ называют класс, объекты которого мож­но использовать в качестве функций. Технически это оформ­ляется с помощью перегрузки opera to r О . Данный «оператор» является единственным оператором C++, допускающим пере­грузку с произвольной сигнатурой, поэтому объекты функто­ра могут имитировать произвольные функции. Соответственно о функторах часто говорят как о функциях: «функтор вызы­вается», «функтор принимает и возвращает значения» и т. п. Кроме того, обычные функции, передаваемые по указателю, могут считаться частным случаем функторов, поскольку мо­гут быть использованы в тех же контекстах.

Генератором называют функтор, который не принимает аргументов и возвращает некоторую (генерируемую) последо­вательность значений. В качестве примера можно привести ге­нератор псевдослучайных чисел.

Предикатом называют функтор, возвращающий булевское значение и используемый, например, при фильтрации после­довательностей. Обычно предикаты являются одноместными (унарными), т. е. принимают один параметр. Двуместные (би­нарные) предикаты, принимающие два параметра и отвечаю­щие некоторому отношению между ними, называют компара­торами. Компараторы используются, например, в упорядочен­ных ассоциативных контейнерах и в стандартном алгоритме sort для определения отношения «меньше».

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

Большая часть стандартных алгоритмов определена в за­головочном файле <algorithm>. Далее приведен список неко­торых стандартных алгоритмов с краткими описаниями.

copy(from, from_end, to) — копирует диапазон [from, from_end) в диапазон [to, to_end), в процессе получая и воз­вращая итератор to_end, что позволяет конкатенировать по­следовательности цепным вызовом сору; например, чтобы по­

Page 93: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

лучить конкатенацию [fl, el) и [f2, е2), начинающуюся в d, вы­зовем copy (f 2, е2, copy( f l , e l , d)) ;

copy if(fr°m , from_end, to, pred) - отличается от copy тем,что копирует только те элементы, для которых pred возвраща­ет истину («фильтр»);

move(from, from_end, to) — отличается от сору тем, что вме­сто копирования выполняет перемещение, поэтому элементы исходной последовательности [from, from_end) могут потерять исходные значения;

find(begin, end, value) — ищет среди [begin, end) первое вхож­дение value, возвращает итератор, указывающий на найденное вхождение, либо end, если value не найдено;

find if(first, end, pred) — отличается от find тем, что вместопоиска конкретного значения, выполняет поиск первого эле­мента, для которого выполняется предикат pred;

count(first, end, value) — считает, сколько элементов из [begin, end) равны value;

count if(first, end, pred) — считает, для скольких элементовиз [begin, end) выполняется предикат pred;

adjacent find(begin, end, bipred) — пытается найти первыйэлемент из [begin, end) такой, что для него и следующего за ним элемента выполняется двуместный предикат bipred, воз­вращает позицию найденного элемента или end. если ничего не найдено; существует также вариант adjacent_find(begin, end), использующий в качестве bipred оператор равенства;

fill(to, to_end, value) — записывает value в каждый элемент диапазона [to, to_end), функция ничего не возвращает;

fill n(to? n, value) — записывает value в [to, to + n), возвра­щает (to + n), полученный в процессе; значения n < 1 игнори­руются;

generate(to, to_end, gen) заполняет [to, to_end) значения­ми, сгенерированными генератором gen; например, заполнить вектор V псевдослучайными числами можно так:

generate (ѵ . begin () , v . e n d ( ) , r and) :

Page 94: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

generate_n(to, n, gen) — соответствует generate так же, как fill_n соответствует fill, например, вывести в cout семь псевдо­случайных чисел можно так:o s t r e a m _ i t e r a t o r < i n t > o u t _ i t ( c o u t , ,fwH); generate_n ( ou t _ i t , 7, r and) ;

reverse(begin, end) — обращает последовательность [begin, end), выполняя серию обменов (по умолчанию с помощью std::swap);

rotate(begin, new_begin, end) — выполняет циклический сдвиг («вращение») элементов последовательности [begin, end) таким образом, что new_ begin становится новым началом, а элементы [begin, new_begin) попадают в конец; возвращает (begin + (end — new_begin));

swap__ranges(a, a_end, b) — последовательно обменивает со­держимое элементов [a, a_end) и элементов [b, b_end), в про­цессе находя b_end, возвращает b end;

for_each(begin, end, fun) — применяет fun к каждому эле­менту из [begin, end), возвращает fun;

transform(from, from end, to, fun) — применяет одноместный функтор к [from, from_end), записывая возвращенные им зна­чения в [to, to_end), возвращает to_end;

transform(a, a e n d , b, to, fun) — применяет двуместный функ­тор к парам элементов из [a, a_end), [b, b_end), записывая возвращенные им значения в [to, to_end), возвращает to_end;

remove(begin, end, value) - перемещает содержимое [begin, end) от конца к началу, затирая все вхождения value, возвра­щает конец новой последовательности (не содержащей value), для реального удаления «хвоста» из контейнера требуется вы­звать функцию-член erase;

remove_if(begin, end, pred) — действует аналогично remove, но затирает те элементы, для которых pred возвращает истину;

unique(begin, end, eq) — действует аналогично remove if, но используег двуместный предикат eq, применяемый к парам подряд идущих элементов; существует также вариант unique (begin, end), использующий в качестве eq оператор равенства,

Page 95: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

позволяющий удалять из последовательности идущие подряд дубликаты (откуда и происходит название функции);

replace(begin, end, old val, new_val) — пробегает [begin, end)и заменяет каждое вхождение old_val на new_val;

replace if(begin, end, pred, new_val) — заменяет в [begin, end)на new_val все элементы, для которых истинен предикат pred;

max_element(begin, end, comp) — возвращает первое вхожде­ние максимального элемента из [begin, end), comp задает ком­паратор; существует вариант max_element(begin, end), исполь­зующий в качестве comp оператор меньше;

min_element(begin, end, comp) — аналогичен max_element, но возвращает позицию первого минимального элемента;

minmax_element(begm, end, comp) — за один проход находит и минимальный, и максимальный элементы, возвращает пару итераторов, семантически эквивалентен

make_pai r (min_element( begin , end, comp), max_element ( begin , end, comp));

partition(begin, end, pred) — разделяет [begin, end) на два участка: [begin, par), для элементов которого предикат pred выполняется, и [par, end), для элементов которого pred не вы­полняется; возвращает точку разбиения par;

sort(begin, end, comp) — сортирует [begin, end) на месте за линейно-логарифмическое время в соответствии с компарато­ром comp (неустойчивая сортировка; как правило, это гибрид­ный алгоритм на основе быстрой, пирамидальной сортировок и сортировки вставками); алгоритмы сортировки в STL тре­буют итераторы произвольного доступа; алгоритмы поиска и сортировки, принимающие компаратор, также существуют в вариантах, где comp не передается, а в качестве компарато­ра используется оператор «меньше», например, sort(begin, end) сортирует по возрастанию;

stable_sort(begin, end, comp) — отличается от sort тем, что гарантирует устойчивость (сохраняет исходный порядок экви­валентных элементов), однако работает несколько медленнее;

Page 96: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

partial sort(begin, mid, end, comp) — выполняет частичнуюсортировку [begin, end) так, что участок [begin, mid) представ- ляет собой начало отсортированной последовательности, а по­рядок элементов «хвоста» [mid, end) не определен;

nth_element(begin, nth, end, comp) — выполняет частичную сортировку [begin, end), пока в [begin, nth) не окажутся эле­менты, которые не больше любых элементов из [nth, end);

lower_bound(begin, end, val, comp) — двоичным поиском на отсортированной в соответствии с компаратором comp после­довательности [begin, end) находит первый элемент, который «не меньше» в смысле comp, чем ѵаі; возвращает соответству­ющий итератор, или end, если таковой не был найден;

upper_bound(begin, end, val, comp) — похож на lower_bound, но возвращает позицию первого элемента, который «больше» в смысле comp, чем val, либо end;

equaІ_ гаnge(begin, end, val, comp) — находит диапазон эле­ментов, эквивалентных val в смысле comp, по семантике то же, что и

make__pair(lower_bound( begin , end, val , comp) , upper_bound ( begin , end, val , comp));

merge(a, a_end, b, b_end, to, comp) — сливает две заранее отсортированные последовательности [a, a_end) и [b, b__end), копируя элементы в [to, to_end), возвращает to_end;

includes(a, a e n d , b, b_end, comp) — предполагая, что после­довательности [a, a end) и [b, b_end) отсортированы, прове­ряет, что все элементы второй содержатся в первой;

set_difference(a, a end, b, b_end, to, comp) — предполагая, что последовательности [a, a_end) и [b, b_end) отсортирова­ны, строит теоретико-множественную разность [a, a_end) \ [b, b_end), записывая результат в [to, to_end), возвращает to_end;

set_intersection(a, a_end, b, b_end, to, comp) — действует аналогично set_difference, но строит теоретико-множественное пересечение;

Page 97: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

set_union(a, a e n d , b, b_end, to, comp) — действует анало­гично set_difference, но строит теоретико-множественное объ­единение, в отличие от merge не сохраняет дубликаты;

set_symmetric_difference(a, a_end, b, b_end, to, comp) — дей­ствует аналогично set_ difference, но строит симметрическую разность;

equal(a, a_end, b, b_end, eq) — проверяет равенство после­довательностей [a, a_end) и [b, b_end), оператор равенства за­дается предикатом eq; как и в других подобных случаях, есть вариант данного алгоритма без предиката, который использует оператор «равно»;

lexicographical_compare(a, a end, b, b end, comp) — проверя­ет, меньше ли [a, a_end), чем [b, b_end), лексикографически, comp — реализация оператора «меньше» для элементов;

search(a, a__end, s, s_end, eq) — ищет начало первой под­последовательности [a, a_end), совпадающей с [s, s_end), ис­пользуя отношение «равно», заданное двуместным предикатом eq;

search_n(a, a end, n, val, eq) — ищет начало первой подпо­следовательности [a, a_end), состоящей из п идущих подряд элементов, равных ѵаі (в смысле предиката eq);

mismatch(a, a_end, b, b_end, eq) — возвращает пару ите­раторов, указывающих на первые элементы [a, a_end) и [Ь, b_end), для которых компаратор eq возвращает ложь.

Ряд «вычислительных» стандартных алгоритмов опреде­лен в заголовочном файле <numeric>. Перечислим их.

iota(begin, end, val) — заполняет [begin, end) последователь­но инкрементируемыми значениями val; например, присвоить элементам контейнера ѵ значения, равные их индексам, можно вызовом i o t a (v . beg i n ( ) , v . endO, 0);

accumulate(begin, end, sO, adder) — накапливает «сумму» эле­ментов [begin, end), начиная с sO и «добавляя» элементы к наг копленной сумме, по умолчанию в качестве adder используется оператор «плюс»; так, посчитать сумму элементов контейнера

Page 98: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

V можно вызовом accumulate (v. beginO , v . endO, 0), а если мы хотим произведение, то запишемaccumulate (ѵ. begin () , v . en d ( ) , 1.0,

mul t ip l i es <double > ());

inner_product(a, a_end, b, sO, adder, mult) — вычисляет внут­реннее (скалярное) произведение двух последовательностей [а, a_end), [b, b_end) равной длины, которое определяется как сумма (задается adder, по умолчанию сложение) sO и попарных произведений элементов последовательностей (задается mult, по умолчанию умножение); внутреннее произведение можно использовать нетривиально: например, пусть многоугольник задан контейнером с, содержащим точки, для которых опре­делена операция d is t , возвращающая расстояние, тогда пери­метр многоугольника можно посчитать вызовомinner_ p r oduc t ( n e x t ( с . begin ()) , с . end ( ) , с . begin ( ) ,

di s t (с . f ront () , с . back ()) , p luscdouble >( ) , d i s t ) ;

adjacent_difference(begin, end, to, diff) — записывает в [to, to_end) разности (задается diff, по умолчанию разность) со­седних элементов из [begin, end), при этом вначале выполняет ♦to = *begin, поэтому to_end отстоит от to на столько же ша­гов, насколько end от begin: возвращает to_end;

partial_sum(begin, end, to, adder) — записывает в [to, to_end) частичные суммы, накопленные при суммировании элементов [begin, end), т. е. (условно) to [0] = beg in [0] ; t o [ i ] = adder ( t o [ i - l ] , begin [ i ] ) .

3.2.1. И диом а удаления элементов из контейнера

При удалении элементов из контейнера с помощью алго­ритмов remove, remove_if и unique требуется результат вызова алгоритма передать в функцию контейнера erase для удаления «хвоста». На деле среди стандартных контейнеров эту схему эффективно можно применить лишь к deque и vector.

Следующий код считывает последовательность слов с по­тока ввода, сортирует слова, удаляет дубликаты и выводит по­

Page 99: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

лученную последовательность. Обратите внимание, что в нем не встречается ни одного ключевого слова Сі-+.

ve c t o r <s t r i n g > words;copy ( i s t r ea m_ i t e r a to r < s t r i n g > ( c i n ) ,

i s t r e a m _ i t e r a t o r <s t r i ng >( ) , back inser ter ( words ) ) ; ein . clear ();sort (words . begin () , words . end ( ) ) ; words . erase ( unique ( words . begin () . words . end ()) ,

words . end ( ) ) ; copy (words . begin () , words . end () ,

os t ream__i te ra tor<st r ing >(cout , ,fw" )) ;

|~y] (3.6) Изучите документацию, описывающую содержимое <algorithm> и <numeric>.

3.2.2. С редства конструирования ф ункторов

Заголовочный файл <fuiictionai> содержит ряд классов- шаблонов, являющихся функторами-обертками соответствую­щих операций: компараторы less, greater, less_equal, greater_ equal, equal_to. not_equal_to, двуместные операции plus, mi­nus, multiplies и др. Например, отсортировать vec to r< in t> v по убыванию можно так:

s o r t ( v . b e g i n ( ) , v . end () , g r e a t e r < i n t > 0 ) ;

Кроме того, < functional> предоставляет средство для обо­рачивания функций-членов mem_fn, средство связывания па­раметров функтора bind и контейнер произвольного функтора function.

Рассмотрим пример с использованием mem_fn. Допустим, требуется убрать из вектора strings все пустые строки. Строка string имеет функцию-член empty, которая выступает в роли предиката, но не может быть вызвана как свободная функция, принимающая const stringfc. Обертка позволяет решить эту проблему: новый функтор будет принимать в качестве первого параметра ссылку или указатель на объект.

s t r i n g s . erase ( remove_i f

Page 100: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

( s t r i n g s . be g i n () , s t r i ngs . end () , mem_fh(&;string:: empty) ) , s t r i n gs , end ( ) ) ;

Функция bind принимает базовый функтор и набор пара­метров. ему передаваемых при вызове обертки. При передаче значения его копия сохраняется в обертке и передается при каждом вызове. Чтобы сохранить параметр по ссылке, сле­дует использовать функции ref и cref (const-ссылка). Чтобы связать параметр, передаваемый в базовый функтор с пара­метром, принимаемым оберткой, нужно указать местодержа- тель, имеющий вид placeholders::_п, где п — число (обязатель­но поддерживаются значения 1 и 2), задающее номер парамет­ра обертки (отсчитывается от единицы). Например, получить удвоенную последовательность ѵ2 из ѵі (считая, что они одина^ ковой длины), содержащей f lo a t , можно следующим образом:

t ransform (v l . begin () , v l . e n d ( ) , v2. begin ( ) ,bind ( mul t ip l i es <f l oa t >( ) , 2 . f , p l aceholders : : 1));

Другой пример: добавим в конец каждой строки из диа­пазона [begin, end) другую строку suffix, которую передадим обертке по ссылке, воспользовавшись функцией cref.

for _each ( begin , end, bind (mem_fn(&st r ing :: append) , p l aceholders : : 1, c r e f ( s u f f i x ) ) ) ;

Тем не менее возможности bind довольно ограничены. Наи­более сильным средством, появившимся в ISO С++П, являют­ся замыканѵя — объекты анонимных функторов, созданные на месте использования с помощью лямбда-выражения. Структу­ра лямбда-выражения слагается из следующих элементов:

« [ перечисление захваченных замыканием переменных ]», если функтор не нуждается ни в каких внешних переменных, то следует поставить []; указание [=] предписывает компи­лятору помещать в замыкание копию каждой используемой внешней переменной; указание [&] предписывает компилято­ру помещать в замыкание не копии, а ссылки на переменные, а, например, указание [=, &s] предписывает помещать копии на все переменные, кроме s, которая будет захвачена по ссылке;

Page 101: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

«( параметры функт,ора )» — список параметров, оформ­ляется так же, как у обычной функции; можно не указывать, если параметров нет;

«mutable» — указывается, чтобы разрешить замыканию мо­дификацию захваченных по значению переменных; необяза­тельный элемент: если он отсутствует, то все захваченные за­мыканием копии переменных будут считаться константами;

«-> возвращаемый тип» — описание возвращаемого замы­канием типа; указывать необязательно, если возвращаемый тип void, либо если тело функтора состоит лишь из одной инструк­ции re tu rn (тогда тип выводится автоматически);

«{ т.ело функции }» — собственно код.Например, с помощью лямбда-выражений предыдущие два

примера можно переписать так:

t ransform ( v l . begin ( ) , v l . e n d ( ) , v 2 . begin ( ) ,[ { ( f l o a t x) { r e t u r n 2 . f * x; });

for_each ( begin , end, [&suf f ix ]( s t r i n g &s){ s . append( suffiX ); });

Замыкания можно сохранять в переменные. При этом клю­чевое слово auto решает проблему неизвестного типа.

Еще одним важным элементом < functional > является класс- шаблон function<T(Args...)>. В качестве параметра шаблона передается функциональный тип, указывающий возвращаемый тип и сигнатуру. Данный класс позволяет разместить в своем объекте произвольный функтор с подходящей сигнатурой (в том числе замыкание), а также указатель на функцию. Таким образом, за счет увеличения накладных расходов по памяти (может быть выделен блок динамической памяти) и процес­сорному времени при вызове (дополнительные косвенные обра­щения) достигается максимально возможная гибкость. Данный класс-шаблон может быть использован при реализации, напри­мер, паттерна наблюдатель для привязки обработчиков сооб­щений, заданных в произвольной форме. Узнать, что лежит внутри объекта function, можно с помоіцью функций ta rg e tcT > и target type. Первая возвращает указатель на хранимый объ­

Page 102: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

ект Т, если он имеет такой тип, либо nullptr в противном слу­чае. Вторая возвращает type_info, описывающий тип храни­мого объекта. Объект function может быть «пуст», если к нему не привязали функтор или функцию. Проверить это можно, приведением к bool или с помощью операции «!».

3.3. Ѵаіаггау

Класс-шаблон valarray<T>, определенный в заголовочном файле <valarray>, является динамическим массивом объек­тов Т, предоставляющим ряд дополнительных возможностей2, предназначенных для вычислений. Набор встроенных «обыч­ных» функций-членов данного класса не велик: swap, size, resize не должны вызвать вопросов. Кроме того, имеются sum, min и max, позволяющие посчитать сумму, минимум и максимум со­ответственно, элементов массива. Функция apply(fun) создает новый объект ѵа1аггау<Т> из результатов применения функ­ции fun к каждому элементу массива.

Функции-члены shift(n) и cshift(n) выполняют линейный и циклический сдвиги элементов в массиве (похоже на сдвиг бит в целом числе: г-й элемент становится на место і + п, длина сохраняется) и возвращают новый объект valarray<T>, новые значения создаются конструктором Т().

Кроме того, для ѵа1аггау<Т> определен полный набор ариф­метико-логических операций, которые применяются для каж­дого элемента или каждой пары элементов с равными индек­сами (поведение не определено, если массивы имеют разные размеры). Также определены перегрузки стандартных матема­тических функций abs, sqrt, exp, log, loglO, pow и тригономет­рических, вычисляемые поэлементно.

2Вероятно, именно этот класс следовало назвать vector, но исто­рия распорядилась иначе: к моменту введения в Стандартную биб­лиотеку ѵаіаггау название vector уже закрепилось за динамическим массивом.

Page 103: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Библиотека и компиляторы могут предлагать различные оптимизации операций с ѵаіаггау. Поэтому, например, для ѵа- la rray < flo a t> х, у код

au to z — X * X — у * у ;

может оказаться быстрее, чем его «наивный» аналог

v a l a r r a y < f l o a t > z ( x . s i z e ( ) ) ;for ( s i ze_ t k -• 0, sz - x . s i z e ( ) ; k < s z ; -H-k)

z[k) — XIkJ * X[k ] - у [kI * у [k ];

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

В случае valarray доступны четыре прокси. Их возможности ограничены присваиванием и операторами вида «*=». Простей­шим из прокси является slice_array<T>, получаемый с помо­щью valarray<T >:: o p e ra to r [] ( s l i c e ) , где slice (срез) есть тройка (start, size, stride), характеризующая выборку элемен­тов из исходного массива, индексы которых начинаются со start и идут с шагом stride, всего size элементов.

Обобщенным срезом gslice называется тройка (start, size, stride), где size и stride — объекты valarray< size_ t> . Соответ­ствующим прокси является gslice_array<T>. С помощью сре­зов можно организовать работу с уложенным в valarray мно­гомерным массивом. Размерность равна размеру size и stride; размеры измерений задаются size; шаги между элементами в исходном массиве с зафиксированными индексами по всем из­мерениям, кроме заданного индексом, задаются stride.

Page 104: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Оставшиеся два прокси представлены классами mask_array<T> и indirect_array<T>. Первый порождается valarray<T>: : o p e ra to r[ ] (valarray<bool> mask) и вклю­чает только те элементы исходного массива, для которых в соответствующих позициях mask стоит истина. Маска должна иметь тот же размер, что и исходный массив. Второй порождается valarray<T>: : operato r П (valarray<size_t> in d ices) и позволяет сделать выборку элементов по задан­ным в indices индексам. Const-версии оператора [] вместо прокси-объектов возвращают объекты ѵаіаітау, содержащие копии соответствующих элементов.

Операции сравнения двух объектов ѵа1аггау<Т> также вы­полняются попарно и порождают объекты valarray<bool>, ко­торые затем можно использовать в качестве масок. Подобная схема является типичной при векторизации ветвлений на мас­сово-параллельных архитектурах с векторными арифметико- логическими устройствами.

Рассмотрим пример реализации матричных операций по­верх ѵаіаггау.tem p la te <c l a s s Т>c l ass Matrix : valarray<T> {

s i z e _ t rs , c s ; / / размеры: строки, колонки p u b l i c :

t y pe de f valarray<T> Data; ty p e d e f slice array<T> View;Matr ix() : r s ( 0 ) , cs(0) {}Matrix ( s i ze_ t rows, s i ze_ t cols)

: r s ( r ows ) , c s ( c o l s ) , Data(rows * cols) {} s i ze_ t rows() const { r e t u r n r s ; } s i ze_ t cols () const { r e t u r n c s ; }

Удобно уметь обращаться и к нижележащему массиву, для это­го введем функцию data, а также экспортируем оператор [] из ѵаіаггау, позволяющий не писать лишний раз .data( ) для получения срезов. Оператор () предназначен для адресации элементов матрицы по номерам строки и столбца.

Data& data () { r e t u r n * t h i s ; }

Page 105: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

const Data& dat a( ) con st { r e t u r n * t h i s ; } using Data :: ope r a t o r [ ];/ / получить элемент no индексам T& ope r a t o r () ( s i z e _ t i , s i ze_ t j ){ r e t u r n ( * t h i s ) [ i * cs 4 j ]; }const T& opera t o r () ( s i ze_ t i , s i z e_ t j ) const{ r e t u r n ( * t h i s ) [ i * cs -f j ]; }

Функции row и col позволяют получить вид (slice_array) или копию (valarray) соответственно строки и столбца с заданным индексом.

/ / адресовать строчку View row(size__t i){ r e t u r n ( * t h i s ) [ s l i c e ( i * cs , cs , 1)]; }Data row(s i ze_t i) const{ r e t u r n (* t h i s )[ s l i c e ( i * cs , cs , 1)]; }/ / адресовать столбец View col ( s ize_ t j ){ r e t u r n (* t h i s )[ s l i c e (j . rs , c s ) ] ; }Data c o l ( s i z e _ t j ) const { r e t u r n (* t h i s )[ s l i c e (j , rs , c s ) ] ; }

Операция «+=» выполняет поэлементное суммирование матриц средствами valarray.

Matrix<T>&; o p e ra to r+ = (c o n st Matrix<T> Mother) { a s s e r t ( r s = o t h e r . rs && cs = o t h e r . c s ) : da t a ( ) += other . da t a (); r e t u r n * t h i s ;

}T t r ace () const { / / след матрицы

a s s e r t ( rs = cs );r e t u r n data ()[ s l i c e (0 , cs , cs + 1) ]. sum ();

}};Для того чтобы можно было перебирать элементы матрицы в цикле for . определим соответствующие варианты begin и end (на основе уже существующих стандартных функций, опреде­ленных для valarray). Синтаксис заголовка функции, введен­ный в ISO С++11 вкупе с новым ключевым словом decltype,

Page 106: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

позволяет вывести возвращаемый тип средствами компилято­ра, опираясь на известные к тому времени параметры функции (выводимый тип есть тип значения, переданного decltype вы­ражения. при этом значение этого выражения не вычисляется). Два аналогичных варианта begin и end, принимающие не-const ссылки, не приводятся для краткости.

tem p la te c c l a s s Т>i n l i n e auto begin (const Matrix<T> &m)

-> d ec lty p e ( begin (m. data () ) ){ r e t u r n begin (m. data ( ) ) ; }te m p la te Cclass T>i n l i n e au to end (const Matrix<T> &m)

-> dec l type (end (m. data ()) ){ r e t u r n end(m. data ( ) ) ; }

Имея в наличии конструктор копирования и операции вида «+=», легко определить соответствующие неприсваивающие фор­мы операций как свободные функции.

te m p la te c c l a s s Т> i n l i n e Matrix<T> operator4-

( cons t MatrixcT> &a, const MatrixcT> &b){ r e t u r n Matrix<T>(a) += b; }

Операции сравнения, определенные для valarray, выполняют покомпонентные сравнения элементов и возвращают результат в виде объекта valarray<bool>. Поэтому сравнение матриц на равенство придется выполнять «вручную».

te m p la te c c l a s s Т> i n l i n e bool o p e r a to r =

(cons t MatrixcT> &a, const MatrixcT> &b) { r e t u r n a . rows( ) = b . r o w s ( ) &&

a . c o l s ( ) = b . c o l s ( ) &&; e q u a l ( b e g i n ( a ) . e n d ( a ) , b eg i n (b ));

}Имея операцию ==, легко определить операцию !=. Похожим образом на основе операции < строятся <=, > и >=.te m p la te c c l a s s Т>

Page 107: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

i n l i ne bool operator ! =(const Matrix<T> &a, co n st Matrix<T> &b)

{ r e t u r n ! ( a b ); }

Самым сложным элементом данного примера является опера­ция умножения. По определению произведение N x K матрицы и К X М матрицы есть N х М матрица скалярных произведе­ний строк первой матрицы со столбцами второй. Это несложно выразить имеющимися средствами.

tem pla te <c l a s s Т>Matrix<T> opera tor*

(const Matr ix<T>&a, con st M atrix</T> &b) { const au to N = a . rows () , M - b . cols (); a s se r t (a. cols () = - b . r o w s Q ) ;Matrix<T> c(N, M) ; / / нули for ( s i z e _ t i 0; i < N; H-i )

for ( s i ze_ t j = 0; j < M; -H-j)с ( i , j ) = ( a . r o w ( i ) * b . col ( j ) ) . sum ();

r e t u r n с ;}

Очевидно, данный алгоритм требует O ( N M K ) операций. Мож­но заметить, что приведенная реализация весьма неэффектив­на с точки зрения работы с памятью: даже если для каждо­го i j -элемента не создается новый объект ѵаіаггау размера К из произведений (продуманная реализация ѵаіаггау способна избежать создания лишних объектов), то все равно остаются промахи кэшей процессора при последовательном обращении к элементам столбца из-за их удаленности друг от друга. Можно изменить порядок обхода элементов так, чтобы не обращаться к столбцам.

const auto N = a. rows () , К — a. cols (); a s se r t (К = b . rows ( ) ) ;Matrix<T> c(N, b. cols ( ) ) ; / / пули for ( s i z e _ t i = 0; i < N; -H-i)

fo r (size__t k = 0; k < K; -H-k) c . r o w( i ) +— a( i . k) * b . row(k) ;

r e t u r n с ;

Page 108: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Впрочем, если реализация ѵаіаггау примитивна, то данный вариант все равно хуже аналогичного кода, написанного «вруч­ную». Для сравнения приведем и его.

const auto N = a. rows () . М — b. cols () ,К = а . с о 1 s ():

a s se r t (К -== b . r o w s ( ) ) ;Matrix<T> c(N, М) ; / / пулиТ *г — &с. d a t a ( ) [ОJ ;const Т *р = &а. data () [0] , *q = &b. data () [0];for ( s i z e _ t i = 0; i < N; -H-i , г += М. p += K) {

const T *qk = q;fo r ( s i z e _ t k — 0; k < K; ++k, qk -f= M) {

co n st T aik = p [ k ]; for ( size t. j -= 0; j < M; -f+j )

г [ j ] += aik * qk [ j ];}

}r e t u r n с ;

В случае Microsoft Visual С-н-2012 второй вариант оказался намного быстрее первого, а третий — намного быстрее второго.

рУ~| (3.7) Сравните быстродействие всех трех вариантов на матрицах разных размеров. Существуют и другие, более слож­ные и асимптотически эффективные алгоритмы умножения матриц. Реализуйте алгоритм умножения матриц Штрассена3, каково его быстродействие?

На практике valarray не снискал большой популярности, хо­тя во многих случаях он способен выступить более эффектив­ной заменой vector (если не требуется вставлять элементы). Вычислительные задачи, связанные, например, с матрицами, обычно решают, привлекая сторонние библиотеки.

3См.: Алгоритмы: построение и анализ. / Т. Кормен, Ч. Лейзер- сон, Р. Ривест, К. Штайн. 2-е изд. М., 2005. С. 833-839.

Page 109: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

3.4. Статическая диспетчеризация

Предположим, дана пара итераторов и требуется вычис­лить расстояние между ними. Операция «—» доступна только для итераторов произвольного доступа, в остальных случаях придется пройтись от первого итератора до второго и посчи­тать число шагов. Определим функцию distance, которая будет делать это. Поле iterator_category класса iterator__traits позво­ляет осуществлять статическую диспетчеризацию вызова — выбор реализации этой функции во время компиляции.tem p la te c c l a s s I t>typename i t e r a t o r _ t r a i t s <It >:: d i f f e r e nce_ t ype distance impl

( I t from, It t o , f o r w a r d _ i t e r a t o r _ t a g ) { typename i t e r a t o r _ t r a i t s <I t >:: d i f f e rence_type d = 0; whi le (from != to) -H-from, +-hd; r e t u r n d:

}

tem p la te <c l as s I t >typename i t e r a t o r _ t r a i t s <I t > :: d i f f e r e nc e _ t ype dis tance_impl

( I t from, It t o , r andom_a cces s _ i t e r a t o r _ t a g ){ r e t u r n to - f rom; } tem p la te Cclass I t>typename i t e r a t o r _ t r a i t s <I t >:: d i f f e r e nc e _ t ype di s t ance ( I t from, It to) {

r e t u r n di s t ance_impl (from , t o , typenamei t e r a t o r _ t r a i t s <I t > : : i t e r a t o r _ c a t e g o r y ( ) ) ;

}Благодаря выстроенной в Стандартной библиотеке иерар­

хии наследования теговых классов, отражающих категории итераторов, вариант реализации функции для forward_itera- to r_ tag будет выбран автоматически для итератора с категори­ей bidirectional_iterator_tag. А вот попытка вызвать функцию distance для итератора с категорией inpu t_ iterato r_ tag приве­дет к ошибке компиляции.

Page 110: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

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

tem p la te Cclass It , c l a s s Vector> void append_impl ( Vector &cv, It begin, It end,

i nput _ i t era tor _ t a g ){ copy(begin, end, ba c k_ i ns e r t e r ( v )) ; } tem p la te Cclass It , c l a s s Vector> void append_irnpl(Vector &v, It begin, It end,

fo rward_ i t e r a t o r _ t a g ) { const auto o ld_s izc = v . s i z e ( ) ;V. res i ze ( o ld_s ize + d i s t ance ( begin , end) ) ; copy( begin . end, v. begin () + o l d_s i ze ) ;

}tem p la te c c l a s s It , c l a s s Vector> void append (Vector &v, I t begin, I t end) {

append_impl (v , begin , end , typename i t e r a t o r _ t r a i t s c I t > : : i t e r a t o r _ c a t e g o r y ( ) ) ;

}

3.5. Умные указатели

Логично предположить, что внедрение семантики переме­щения заменяет упомянутое в главе 1 правило трех правилом пят и , дополняя конструктор копирования, деструктор и опе­ратор присваивания, формирующих класс, управляющий ре­сурсом (принцип R A II от англ. resource acquisition is initializa­tion — «выделение ресурса есть инициализация»), перемещаю­щими конструктором и оператором присваивания.

Однако Стандартная библиотека СМ-+ нередко позволяет свести правило пяти к «правилу нуля», так как необходимые

Page 111: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

элементы уже реализованы в классах Стандартной библиоте­ки.

Наиболее ярким примером этого служат классы-шаблоны «умных указателей» (от англ. smart pointers) unique_ptr и sha- red_ptr, определенные в заголовочном файле <memory>. По­мимо использования одиночных объектов этих классов воз­можно эффективное размещение их в стандартных контейне­рах (в то время как размещение в контейнерах обычных ука­зателей может легко повлечь ошибки управления памятью).

Класс unique_ptr<T, Deleter = default_delete<T>> берет управление динамически созданным объектом на себя. Объек­ты этого класса можно перемещать, но нельзя копировать. Вто­рой параметр шаблона определяет политику удаления управ­ляемых объектов (по умолчанию вызов обычного d e le te или d e le te []). Конструктор по умолчанию инициализирует такой указатель нулем (состояние «нет управляемого ресурса»). По­лучить хранимый адрес можно с помощью функции-члена get, «забрать» управляемый ресурс — с помощью функции-члена release, которая, возвращая указатель, сбрасывает хранимый адрес в n u llp tr . Можно заменить (уничтожив) старый ресурс на новый с помощью функции-члена reset(pointer).

/ / автоматически удаляемый динамический массив unique p t r < i n t [ ] > histogram (new in t [ blocks ]);

Класс shared_ptr<T> хранит указатель на разделяемый ре­сурс со счетчиком ссылок. Копирование объекта shared_ptr увеличивает счетчик. Уничтожение объекта shared p tг умень­шает счетчик. Если счетчик достиг нуля, ресурс освобожда­ется. Кроме функций reset и get, похожих по функционалу (с поправкой на наличие счетчика ссылок) на аналоги, опреде­ляемые unique_ptr, класс shared_ptr позволяет узнать теку­щее значение счетчика (use_count) или проверить его на равен­ство единице (unique). При создании нового объекта shared_ptr можно передать непосредственно указатель на ресурс и, если потребуется, функтор, ответственный за освобождение этого ресурса. При этом в динамической памяти будет создан блок

Page 112: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

управления, хранящий счетчик ссылок и, возможно, передан­ный функтор. Более эффективное выделение памяти (одним блоком) позволяет ускорить выполнение операций объектами через shared_ptr. Для этого предназначена стандартная функ­ция make_shared<T, Args...>(args), которой необходимо указать тип управляемого объекта Т и передать параметры args жела­емого конструктора Т.

Потенциальной проблемой при активном использовании sha- red_ptr является возможность возникновения циклических ссы­лок, т. е., например, ресурс А хранит shared_ptr, указывающий на Б, а ресурс Б, в свою очередь, хранит shared_ptr, указываю­щий на А (в этой цепочке зависимостей может быть произволь­ное число звеньев). Такие ресурсы не будут удалены, даже если будут уничтожены все внешние указывающие на них объекты shared_ptr. Имеем ситуацию утечки памяти.

Проблема циклических ссылок возникает при формирова­нии структуры объектов с «предками» и «потомками», в кото­рой и предки имеют указатели на потомков, и потомкам желаг тельно иметь указатели на предков. В данной ситуации Стан­дартная библиотека предлагает воспользоваться объектом клас­са weak_ptr<T>, хранящим ссылку на управляемый sh a r ed p t r ресурс, но не участвующим в изменении числа ссылок.

«Слабый указатель» можно сбросить вызовом reset (не при­нимает параметров). Можно узнать количество ссылок с помо­щью use_count либо удостовериться в уже произошедшем осво­бождении ресурса с помощью функции expired. Внимательный читатель может задаться вопросом, каким образом организо­вана работа этой функции. Дело в том, что и shared_ptr, и w eak_ptr увеличивают и уменьшают второй неявный счетчик ссылок — не на ресурс, а на блок управления. Пусть ресурс уничтожен, но блок управления со счетчиками будет существо­вать, пока к нему привязан хотя бы один объект weak_ptr.

Чтобы получить возможность работать с объектом, на кото­рый указывает weak_ptr, следует «защитить» его от возможно-

Page 113: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

го удаления, получив ссылающийся на него объект shared_ptr. Это делается с помощью функции-члена lock.

s t r u c t Child {weak_ptr<Parent> parent ; void no t i f y_pa r en t () {

i f ( auto p — p a r e n t . lock ())p—> n o t i f y ( ) : / / p — это shared_ptr

} };Стандартный предикат owner_less<T> задает строгое упо­

рядочение на объединении shared_ptr< T > и w eak_ptr<T>, предлагая замену сравнению «меньше» в контекстах, где тре­буется упорядочивать смешанные коллекции указателей.

Для приведения типов «умных указателей» в <memory> определены функции-шаблоны static_pointer_cast<U>, dyna- mic_pointer_cast<U> и const_pointer_cast<U>, создающие из объектов *_p t r <T> объекты *_pt r<U>, привязанные к тем же ресурсам. Действие функций определяется семантикой при­ведения типов указателей на Т к указателям на U, определен­ной для соответствующих операторов приведения типа.

Если требуется создать класс Т, объекты которого мо­гут управляться через shared_ptr< T > , то следует рассмотреть возможность наследования его от enable_shared_from_this<T> (паттерн CRTP), примешивающего к Т закрытый weak__ptr и открытую функцию-член shared_from_this, позволяющие по­лучать корректные объекты shared_ptr для объекта данного класса, которые будут разделять блок управления с ранее со­зданными shared_ptr, пусть даже о них ничего не известно в локальном контексте. В противном случае невозможно создать shared_ptr, имея адрес некоторого объекта, временем жизни которого мы не распоряжаемся.[У] (3.8) Создайте класс Node : e n a b le .sh a re d .fro m .th is <Node>, имеющий поля weak.ptr<Node> paren t и vector<sha- red _ p tr <Node», позволяющий описать произвольное дерево. Проверьте корректность автоматического удаления объектов данного класса вместе с принадлежащими им поддеревьями.

Page 114: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Список рекомендуемой литературыАлександреску А. Современное проектирование на Си- / А. Алск- сандреску. — М. : Вильямс, 2002. *— 336 с.Буч Г. Объектно-ориентированный анализ и проектирование /Г. Буч. — 3-е изд. — М. : Вильямс, 2008. — 720 с.Приемы объектно-ориентированного проектирования. Паттерны про­ектирования / Э. Гамма [и др.]. — СПб. : Питер, 2012. — 368 с.Макконнелл С. Совершенный код / С. Макконнелл. — СПб. : Питер, 2005. - 896 с.Мартин Р. Быстрая разработка программ. Принципы, примеры, прак­тика / Р. Мартии, Дж. Ныокирк, Р. Косс. — М. : Вильямс, 2004. — 752 с.Мартин Р. Чистый код: создание, анализ и рефакторинг / Р. Мар­тин. — СПб. : Питер, 2010. — 464 с.Мейерс С. Эффективное использование STL / С. Мейерс. — СПб. : Питер, 2003. — 224 с.Мэйерс С. Эффективное использование C++ / С. Мэйерс. — 3-е изд. — М. : ДМК Пресс, 2006. — 300 с.Прата С. Язык программирования C++ : лекции и упражнения: учеб. / С. Прата. — 5-е изд. — М. : Вильямс, 2007. — 1171 с.Саттер Г. Решение сложных задач на C++ / Г. Саттер. — М. : Вильямс, 2008. —* 400 с.Степанов А. Начала программирования / А. Степанов, П. Мак- Джоунс. — М. : Вильямс, 2011. — 272 с.Страуструп Б. Программирование. Принципы и практика исполь­зования С+̂ - / Б. Страуструп. — М. : Вильямс, 2011. — 1248 с.Страуструп Б . Язык программирования C++ : спец. изд. / Б. Стра­уструп. — М. : Бином-Пресс, 2011. — 1136 с.

Page 115: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Учебное издание

Кувшинов Дмитрий Рустамович Осипов Сергей Иванович

ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО-ОРИЕНТИРОВАННОГО

ПРОГРАММИРОВАНИЯСтандартная библиотека шаблонов

Учебное пособие

Page 116: ОСНОВЫ ОБОБЩЕННОГО И ОБЪЕКТНО …elar.urfu.ru/bitstream/10995/45634/1/978-5-7996-1014-2_2013.pdf · ной библиотеки» в основном посвящена

Заведующий редакцией М. А. Овечкина Редактор Т. А. ФедороваКорректор Т. А. ФедороваОригинал-макет Д. Р. Кувшинов

План выпуска 2013 г. Подписано в печать 14.11.2013. Формат 60х841/іб- Бумага офсетная. Гарнитура Times.

Уч.-изд. л. 6,2. Уел. печ. л. 6,7. Тираж 70 экз. Заказ 2487. Издательство Уральского университета 620000, г. Екатеринбург, пр. Ленина, 51.

Отпечатано в Издательско-полиграфическом центре УрФУ 620000, Екатеринбург, ул. Тургенева, 4.

Тел.: + (343) 350-56-64, 350-90-13 Факс -1-7 (343) 358-93-06

E-mail: [email protected]