Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 127
Текст из файла (страница 127)
Ргед означает унарный предикат, а ВтнРгей — бинарный предикат (818.4.2), Стр обозначает функцию сравнения (517.1.4.1, 518.7.1), Ор обозначает унарную операцию, а В)нОр — бинарную операцию (818.4). Чаше всего для имен параметров шаблонов применяют гораздо более длинные имена. Однако я считаю, что уже после беглого ознакомления со стандартной библиотекой применение длинных имен ухудшает, а не улучшает читаемость.
р(гнераторы произвольного досгнуна (галдит-ассезз йегагогл) могут использоваться как двунаправленные итераторы (Ьибгест(она) 1гегагогт), двунаправленные итераторы — как прямые итлераторы (уогтгагд (гегагогз), а прямые итераторы — как итераторы ввода ()вриг (гегагогз) или итераторы вывода (оигриг (гвгатогз) (см. 819.2.1). Переда- 18.3. Диапазоны (интервапы) и контейнеры 613 ча шаблону типа, не обеспечивающего требуемых операций, приводит к ошибке во время конкретизации шаблона (5С.13.7). А в случае передачи типа, обеспечиваю- щего требуемые операции с иной семантикой, поведение программы во время ее выполнения непредсказуемо (517.1.4). 18.3. Диапазоны (интервалы) и контейнеры го!4 !'(Нм(1пг> е 1!) ( 161<т!>:: Ыега<ог р = Япд (д.
вел (), д. епд (), 42); ~У первое вхождение (г"(р! = Д.епд() ) ( !!в!к1п!>::!!его!ог 4 = !(пд(+»р, 11. епд(), 42); У... У... ) Ф второе вхождение Если быЯпд() выполняла операцию над контейнером, нам потребовался бы какой-нибудь дополнительный механизм для поиска второго вхождения. Очень важно, что выполнить обобщение такого «дополнительного механизма» на все контейнеры и алгоритмы не так-то просто. Вместо этого алгоритмы стандартной библиотеки работают над последовательностями элементов. То есть на вход алгоритма подается последовательность элементов, определяемая парой итераторов.
Первый итератор ссылается на первый элемент последовательности, а второй — на место, расположенное сразу за последним элементом последовательности (53.8, 519.2). Эта последовательность является полуоткрытой, поскольку она включает в себя первый из упомянутых только что элементов, но не второй из них. Такие полуоткрытые последовательности позволяют выразить большинство алгоритмов без выделения пустой последовательность в отдельный случай. Последовательности элементов, особенно последовательности с произвольным доступом, принято называть интервалами или диапазонами (гап8е). Традиционной математической записью полуоткрытых интервалов служит фее!, 1ааг) или Ягм, 1ае!(.
Важно, что интервалы могут включать все элементы контейнера, или подмножество элементов контейнера. Некоторые последовательности, такие как, например, потоки 1/О, не имеют отношения к контейнерам. Однако алгоритмы, выраженные в терминах последовательностей (интервалов) прекрасно работают и в этом случае. Известен хороший универсальный принцип: то, что требуется часто, должно быть самым простым, легким в использовании и безопасным.
Стандартная библиотека нарушает этот принцип ради универсальности. Для нее универсальность важнее всего. Например, мы можем найти два первых вхождения в список числа 42 следующим образом: 18. Алгоритмы и классы функциональных объектов 614 1В.3.1. Входные диапазоны Использование х.
Ьея!и (), х. епй() для записи всех элементов х столь широко распространено, что даже часто утомляет и может привести к ошибке. Например, когда в коде имеется сразу несколько итераторов, можно совершить ошибку, предоставив алгоритму не ту пару итераторов (которые не образуют последовательность): ой!Т(!(п<зл(пе>з !гии, йя<згпле>з сдпт) ( ЗурейеЗ Ы<злтле>:: сопи депйог 1Д Й! р1 = 11пй ((гии. Ьеят ( ), сдпт.
епй (), "арр(е" ); Р тиопфрт ные посзедовотевьности! . 11р2=!тй(угла.дее!л(],Згии.елй(), "арр(ев); УоЬ ИрЗ =Ялй(с(оиз.деятл (),сйгиз.епй(), "реагв); доЬ 11р4 =Згпй(р2,рЗ, "реасЬ"); д нгоп8!(разные лосзедовотельности) В данном примере две ошибки. Первая вполне очевидна (если подозреваешь, что она есть), но компилятору обнаружить ее нелегко. Вторую ошибку трудно обнаружить в реальной программе даже опытному программисту. Но можно упростить задачу, сократив число явно используемых итераторов. Сейчас я очерчу подход к решению проблемы, основанный на явном введении входных последовательностей. В остальной же части данной главы я не буду использовать явных входных последовательностей, чтобы удержать разговор о стандартных алгоритмах строго в рамках стандартной библиотеки.
Ключевая идея подхода заключается в явном формировании входной последовательности. Например: гетр!а(е<с(азз 1п, с(аи Т> 1п1)пй(1п)тзг, 1п !азб соле! Тз г) 17 стандарт ( тьй!е (((гзг! =(аз! з з з)гзз! г о) ++Зтзп ге!игл !!гзз; ) д расширение гетрйие<с(ат 1п, с(азз Т> 1п !)пй (1зеа<1п> г, солт Та г) ( ге(игп ~тй (г. !1 ге!, г.
зесолй, г); ) При использовании аргумента 1зеа перегрузка (513.3.2) позволяет отдать предпочтение версии со входной последовательностью. Естественно, входная последовательность формируется как пара 517.4.1.2) итераторов; <етр(осе<с!азз 1п> зггисг 1зеа: риЫ(с ра(г<1п, 1и> ( Тзее(йп Ь1, 1л (2): ра(г<1о,йп> ((1,12) () )' Мы могли бы и явно сформировать входную последовательность 1зе4, необходимую для вызова второй версии алгоритмайпй(): 11 р=/1пй(1зег!<$Л> ((ги!Г.Ьея(п (),!ги!!.епй() ), парр(е"); 18.4. Классы функциональных объектов 615 Однако это еше более утомительно, чем вызывать оригинальную версиюЯлз(() .
На помощь приходит простая вспомогательная функция. Обычно, входная последовательность формируется на всех элементах контейнера (от Ье81л () до ел!1() ); гетр1аге<с1азз С> 1зея<зурелате С:: Ьегагог> !зея (Са с) 7!для всего контейнера ( ге!игл 1зед<(урелате С::1!егагог> (с. Ьея!п (), с.
еиИ () ); Теперь можно применять алгоритм к контейнеру в очень компактной форме: го(г(7(1!з!<ззгшд>а 1з) ( 11уд<зп(пя>:: 1гегазог р =Япг( (Ь. Ье81л (), Ь. еиИ (), "згапдага" ); 11з!<з!г!п8>:: пега!ос 4 =вша (1зея (В), "ех!епз!оп" ); 77 ... ) Легко определить вспомогательные функции 1зеа (), формирующие входные последовательности Ье!!для массивов, потоков ввода и т.д.
(818.13[6[). Введение Ьеу позволяет явно отразить понятие входной последовательности, а вспомогательная функция !шит () позволяет устранить чреватое ошибками монотонное и утомительное дублирование кода при создании входных последовательностей через пару итераторов. Понятие выходной последовательности также имеет некоторую ценность, но оно сложнее и его не так-то просто применить с пользой в реапьном коде (818.13[7); см, также 819.2.4).
18.4. Классы функциональных объектов Многие алгоритмы работают с последовательностями исключительно через итераторы и значения. Например, мы можем найти первый элемент последовательности со значением 7 следующим образом: го(а'7 (11з!<!п!> а с) ( !В!<!и!>:: Ыега1ог р = )зпг! (с. Ьед!п ( ), с. елФ ( ), 7) 77 „. ) В качестве более интересного примера мы можем заставить алгоритм выполнить код, который мы ему передаем (83.8.4). Например, можно находить первый элемент последовательности со значением, меньшим 7: Ьоо1 !езз !Ьап 7(1ги г) ге!игп о<7; ) гоЫ7(!!з!<ш(>а с) ( 1!зг<!пг>::!!его!ог р =~Ъпг( зу(с. Ьех!п (), с. ела (), 1езз !Ьал 7); 616 Глава 18. Алгоритмы и классы функциональных объектов Функции, передаваемые в качестве аргумента, могут быть: логическими предикатами, арифметическими операциями, операциями извлечения информации из элементов и т.п.
Но писать отдельные функции для каждого случая и неудобно, и неэффективно. К тому же, не существует функции, способной отразить все необходимые иам логические нюансы. Например, часто нужно вызывать функцию для каждого элемента так, чтобы она сохраняла информацию между отдельными вызовами и возвращала некоторый совокупный для всех ее вызовов результат. Здесь лучше подойдет функция-член класса (чем глобальная функция), поскольку объект этого класса может сохранять любую информацию.
Кроме того, класс может обеспечить операции инициализации и извлечения такой информации. Рассмотрим класс, объекты которого ведут себя как функции, призванный вычислять сумму: гетргаге<егазз Т> <1азз оит ( Т гез; риЫЫ: Хит ( Т ! = 0): гез (1) ( ) гоЫ орега!ог() (Тх) (гез е= х; ) Тгезиа() сопи (геьигп гез; ) ): У инициализация й накопление )) возврат суммы Ясно, что класс аит предназначен для арифметических типов, для которых инициализация нулем и операция += определены. Например: гоЫ !' (1(зг<йоиЫе> а 1И) ( 8ит<доиЫе> з; з =)ог еаеа (Ы.аеа!и (), Ы.епг!(), з); )) вызываем з() для каждого элемента 1д сои!« "гае зит и" « з.гезий() « ' ~п'; ) 18.4.1. Базовые классы функциональных объектов Стандартная библиотека предоставляет множество полезных объектов-функций. Д)гя помощи в написании классов функциональных объектов в заголовочном файле <!ипс!!она!> определены два базовых класса: Здесь|юг еас)з() (818.5.1) вызывает Бит<гуоиЫе>::орегаьг() (г1оиЫе) для каждого элемента из Ы, и возвращает объект, переданный в качестве третьего параметра.
Ключевая причина такого поведения состоит в том. что алгоритмуог еасй() не считает, что третий параметр является именно функцией. Вместо этого он полагает, что третий аргумент есть нечто, к чему можно применить операцию вызова и передать при этом необходимый аргумент. Определенный подходящим образом объект подойдет здесь так же, как и функция, и даже лучше. Например, перегруженная в классе операция () поддается встраиванию лучше, чем функция, переданная по указателю на функцию — как следствие выигрывает производительность кода. Объекты классов, перегружающие операцию вызова (операцию () ), называются обьектами-функциями или функциональными обьектами ((ипсг!оп-1))се оЬ)ес!з или (йпс!!опз оЬ)есо), а также функторами ()ипс!огз).