А. Александреску - Современное проектирование на C++ (1119444), страница 18
Текст из файла (страница 18)
О Определение класса нц11туре дано в главе 2 суредет туре1т'зс<т'пс, нц11туре> опетуреоп1у; Определение класса туре15 зс, состояшего из трех типов с)заг, принимает следуюший вид. суреоет туре1тзс<сЬаг, туре1)зс<з)дпеа сбаг, туре1(ьс<цпз)дпед с(заг, ни11туре» > л11спагтурез; Следовательно,мы получили шаблон неограниченного списка типов туре11зс, кото- рый может содержать любое их количество. Посмотрим теперь, как можно манипулировать списками типов. (Как и прежде, это относится к типам туре1)зс, а не к объектам типа туре11зс.) Приготовьтесь к приключениям.
С этого момента мы погружаемся в подземелье языка С++, мир странных новых правил — мир программирования на этапе компиляции. 3.3. Линеаризация создания списков типов Сами по себе списки типов слишком напоминают конструкцию из языка (.)ЯР, поэтому их нелегко использовать. Такие конструкции очень нравятся программистам на языке (.БР, но они не очень хорошо согласуются с языком С++ (не говоря уже о пробелах между символами <, которые нельзя забывать). Например, вот как выглядит список целочисленных типов. суреде1 туре1ззс<зтдпед спаг, туре1тзс<зпогс (пс, туре1(зс<(пс, туре1(зс<1апд (пс, яц11туре» » в)дпедспседга1з; Списки типов были бы превосходной концепцией, но они явно нужлаются в более привлекательной упаковке.
Для того чтобы упростить процедуру создания списков типов„ в файле туре1тзс.(з из библиотеки (.о)с( определено большое количество макросов. преобразуюших рекур- сию в простое перечисление, правда, за счет утомительных повторений. Однако эта .— пе проблема. Повторение выполняется только один раз, в библиотечном коде. Это по- зволяет масштабировать списки типов для большого количества элементов (50). Ти- пичный макрос выглядит следуюшим образом.
Часть б Методы №деЕтпе ТТРепвт 1(т1) туре1зэс<т1, вц11туре> №деФтпе ттаеь15т 2(т1, т2) туре1зэс<Т1, ттреь15Т 1(т2) > №деЕтпе тчРЕЬ15Т 3(т1, т2. т3) туре11эс<Т1, ттРЕСЕ5т 2(т2„тЗ) > №деб(пе туРеьх5т 4(т1, т2, тЗ, т4) т Туре11эС<Т1, Туре1зэС 3(т2, т3, т4) №деНпе туРЕЕ15т 50(...) Каждый макрос использует предыдущий, что позволяет пользователю библиотеки при необходимости легко увеличивать верхний предел. Теперь можно сформулировать более удобное опрелеление списка целочисленных типов 5№йпед1птейга15, суреде( ттРЕь15т 4(э(дпед сиаг, эиогс Зпс, 1опд Зпс) 5зйпедгпсейга15; Линеаризация создания списков типов — всего лишь начало.
Манипуляции со списками типов все еще неудобны. Например, доступ к последнему элементу списка 5тйпедспсейга1 вынуждает использовать конструкцию 5зйпедспсейга15::тат1::таз1::неад. Не вполне понятно, как мы сможем манипулировать списками типов в обобщенном виле. Итак, пришло время определить основные операции над списками типов в терминах элементарных операций над списками значений. 3.4. Вычисление длины списка Рассмотрим простую операцию. Задан список типов тьзэс.
На этапе компиляции нужно получить константу, равную его длине. Эта константа должна быть статической, поскольку список типов является статической конструкцией, и естественно ожидать, что все вычисления, относящиеся к нему, выполняются именно на этапе компиляции. Идея, лежащая в основе большинства операций над списками типов, заключается в применении рекурсивных шаблонов, использующих для своего определения собственные конкретизации.
При этом такие шаблоны передают разные списки шаблонных аргументов. Рекурсия, полученная таким способом, заканчивается явной специализацией предельного варианта (Ьогдег сазе). Код, вычисляющий длину списка типов, довольно лаконичен. севр1асе <с1ааа ТСзэС> эСгисС СепОСЬ; севр1асе <> эсгисс ьепйсЬ<иц11туре> ( епцв ( ча1це = 0 ); ); севр1асе <с1ааэ т, с1аээ ц> эсгцсс еепйсЬ< 'туре1(эс<т, ц» ( елков ( ча1це = 1 +сепйсЬ<ц>: гва1не ); )' В переводе на человеческий язык, это означает: "Длина нулево~о списка типов равна О.
Длина любого другого списка типов равна 1 плюс длина его оставшейся части*'. Реализация структуры сепйсЬ использует часпшчную юадлаиную саециалазацаю (глава 2), позволяющую различать нулевой тип и список типов. Первая специализация структуры сепйсЬ является полной и соответствует только типу ви11туре. Вторая, частичная, специализация соответствует любому классу туре1зэс<т, ц>, включая составные списки, в которых класс Ц, в свою очередь, является классом Туре13 эс<ч, и>. 75 Глава 3. Списки типов Во второй специализации вычисления проводятся рекурсивно. Величина ча)це в ней определяется как 1 (с учетом головы списка т) плюс длина хвоста списка. Когда в хвосте списка остается единственный класс нц))туре, обнаруживается совпадение с первым определением, и рекурсия останавливается.
В результате вычисляется величина, равная длине списка. Допустим, например, что нам нужно определить массив в стиле языка С, в котором содержатся указатели на объекты класса этг)::туре зпХо для всех целочисленных типов со знаком. Используя структуру ьепдтп, можно написать следующий код, этд: пвуре бпбоь 1птэятт) (ьепдтй<вздпег)тптедга) э>: гма1ие); Во время вычислений на этапе компиляции в памяти будут размещены четыре элемента массива 5 птэяттз'.
3.5. Интермеццо Впервые проблема шаблонных метапрограмм обсуждалась в книге ЧеЫпц(хеп (1995). Затем эта тема глубоко изучалась в работе Схагпес)ц апд ЕЬепес)гег (2000), которая содержала полную коллекцию имитаций выполнения операторов языка С++ на этапе компиляции программы. Идея и реализация структуры ьепдт)з напоминают классический пример рекурсии: алгоритм, вычисляющий длину односвязного списка структур. (Однако есть два существенных отличия: алгоритм для структуры ьепдтп выполняется на этапе компиляции и применяется к типам, а не к значениям.) Возникает вопрос: можно ли разработать итеративный, а не рекурсивный вариант структуры сендеро? Помимо всего прочего, итерация более естественна для языка С++, чем рекурсия. Ответ на этот вопрос приведет нас к реализации других функциональных возможностей класса туре1з эт.
Ответ оказался отрицательным по весьма интересной причине. Средства языка С++, предназначенные для программирования на этапе компиляции, состоят из шаблонов, целочисленных вычислений и определений типов (операторы туредеХ). Посмотрим, как работает каждый из этих инструл1ентов. Шаблоны — точнее, специализации шаблонов — представляют собой эквивалент операторов 5Ф на этапе компиляции. Как мы уже видели на примере реализации структуры ьепдгй, специализация шаблона позволяет отличать списки типов от других типов. целочисленные вычисления ..- это настоящие вычисления, которые осуществляются путем перехода от типов к значениям. Однако здесь есть одна особенность: все значения на этапе компиляции являются неизменяемыми (!пипшаЫе).
Определив целочисленную константу, скажем, перечислимое значение, программист не может его в дальнейшем изменять (например, присваивать одно значение другому). Определения типов (операторы туреде1) могут рассматриваться как введение констант, задающих имя типа. И вновь после определения все эти имена оказываются зафиксированнымн — в дальнейшем переопределить символ, введенный оператором туреде1, невозможно. Эти две особенности вычислений на этапе компиляции делают их принципиально несовместимыми с итерациями. Во время итераций вгячисляется значение итератора, ' Этот массив можно инициализировать, не прибегая к повторению кода.
Предлагаем читателю решить зту задачу самостоятельно. 76 Часть й Методы причем оно может изменяться при выполнении некоторых условий. Поскольку на этапе компиляции переменных сущностей нет, выполнять итерации совершенно невозможно. Слеловательно, хотя язык С++ в основном является императивным, любые вычисления на этапе компиляции должны опираться на методы, характерные лля чисто функциональных языков, которые отличаются тем, что не могут изменять значения. Итак, без рекурсии не обойтись.
3.6. Индексированный доступ К элементам списка типов желательно иметь индексированный доступ. Это позволит организовать линейный лоступ к элементам, упрощая манипуляции со списками. Разумеется, как и все остальные сущности, с которыми мы работаем, индекс должен быть статической величиной. Объявление шаблона для индексированной операции может выглядеть следующим образом. темр1асе «с)аьь тьзьс, цпьз'дпед зпс )пдех> ьтгцсс турель; Перейдем к опрелелению алгоритма.
Следует иметь в виду, что использовать изл~еняемые значения нельзя. турелс Вхог)ные данные: список типов тьзьс, индекс з Результат: внутренний тип яеьи1с Если список тьз ьт не пуст и индекс з равен нулю, то класс яеьц1т — это голова списка тьз ьт. Иначе, если список тьзьс не пуст и инлекс з не равен нулю, то класс яеьц1т получается путем применения алгоритма турель к хвосту списка тьзьс и индексу з-1. Иначе происхолит выход за пределы допустимого диапазона изменения индекса, который порождает сообщение об ошибке на этапе компиляции.
Ниже привелено воплощение алгоритма туредт на языке С++. сеар1асе <с1аьь неад, с1аьь та)1> ьтгцст туредс<туре1)ьс<неад, та)1>, 0> ( суреде1 неад яеьц1с; ): тевр1асе <с1аьь неад, с1аьь таз1, цпьзспед зпс з> ьтгцст туредс<туре1зьс<неад„ та)1>, з> ( суреде1 турепаве туредс<таз1, з-1>.":аеьц1с яеьц1с," Если вы попробуете выйти за пределы допустимого диапаюна изменения инлекса, компилятор сообщит, что специализации турелс<ни11туре, х> не существует.
Здесь символ к означает величину, на которую вы превысили размер списка. Это сообщение могло бы быть более информативным, но сойдет и так. В библиотеке ).о)д (файл туре1зьт.'и) определен вариант структуры турель пол названием туредснопьсгзст. Эта структура реализует некоторые функциональные возможности структуры турель с той лишь разницей, что выход за прелелы лопусти- 77 Глава 3. Списки типов мого диапазона изменения индексов в ней запрещен менее строго и приводит не к порождению сообщения об ошибке на этапе компиляции, а к выдаче в качестве результата типа, заданного пользователем по умолчанию.