А. Александреску - Современное проектирование на C++ (1119444), страница 32
Текст из файла (страница 32)
Можно предположить, что класс Рипстог не будет скомпилирован вовсе, поскольку в каждой специализации класса Рипстогтюр1 опре- делен только один оператор О, а не группа, как в классе Рипстог, Трюк заключается в том, что в языке С++ функции-члены шаблонных класаов не конкретизируются, поко они не выэвоны. Пока не вызывается неправильный оператор О, компилятору все равно.
При вызове перегруженного оператора О, не имеющего смысла, компилятор попытается сгенерировать его тело и обнаружит несоответствие. // Определяем фуиктор, получающий параметры типа эпт и доиЫе, // и возвращающий значение типа доиЫе. Рипсаог<доиЫе, ттяяьтэт 2(зпа, доиЫе)> юугипссог„' // Вызываем его. // генерируется тело оператора орегатогО(с)оиЫе, эпа). доиЫе геэи1а = юугипстог(4, 5.6); // Неверный вызов, доиЫе геэи1т = юукипстогО; //ошибка! // оператор орегасогО О неверен, // потому что он не определен в классе // Рипстог|юр1<доиЫе, ТУРеь15Т 2(1пт, доиЫе)>. Благодаря этому тонкому трюку класс Рипстог не обязательно частично инициали- зировать для нуля, одного, двух и больше параметров —.
это лишь приведет к дубли- рованию кода, Достаточно определить все версии и позволить компилятору генериро- вать только одну из них. Теперь все вспомогательные аредства созданы, и можно приступать к определению конкретных классов, производных от класса Рипстогтщр1. вэ.6. РабОта С фуНКтсраМИ Начнем работать с функторами. Функторы представляют собой экземпляры классов, в которых определен оператор О, как в классе Рипссог (т.е.
объект класса гипстог и сам являетая функтором). следовательно, конструктор клаааа Рипстог, получаюший объект класса Рипссог в качестве параметра, является шаблоном, параметризованным типом этого функтора. сеюр1ате <турепаще а, с1аьэ тьбэт> с1аээ Рипстог ( как и раньше риЫ)с: аеюр1ате кс1аээ Рип> Рипстог(сопят Рип$ ~ип)," Для того чтобы реализовать этот конструктор, нам нужен простой шаблонный класа Рипстогнапд1ег, производный от класса Рипстог|юр1<я, ть1эс>. Этот класс хранит объект типа Рип и передает ему оператор О. Реализуя оператор О, мы прибегнем к описанному выше трюку.
Чтобы избежать определения слишком большого количества параметров класса Рипссогнапд1ег, сделаем шаблонным параметром саму конкретизацию класса Рипстог. Этот единственный параметр содержит остальные, поскольку он предусматривает внутренний оператор турее)е~. 132 Часть!1, Компоненты севр1асе <с1азд РагепСГипссог, Сурепаве Гип> с1азз гипссогнапд1ег риЫ !с гипссогтвр! сурепаве Рагепсгипссог::нези!стуре, Сурепаве РагепСГипстог::Рагвь!зт > ( риЫ1 с: суредеГ сурепаве Рагепсгипссог:нсези!стуре нези!стуре; гипссогнапд1ег(сопзс гипб Гип) : Гип (Гип) () гипссогнапд1ег* с1опе() сопзс ( гесигп пеи гипссогнапд1ег(+сп!з); нези!стуре орегасогО О ( гесигп Гип 0; ) яеаи1стуре орегасого(сурепаве Рагепсгипссог:: Рагв1 р1) гесигп Гип (р1); ) веьи1 стуре орегасого (сурепрве Рагепсгипссог:: Рагв1 р1, Сурепаве РагепСГипстог::Рагв2 р2) ( гееигп Гип (р1,р2); ) рг1часе: гип Гоп ; ); Класс гипссогнапд1ег почти не отличается от класса гипссог.
Он переадресовывает запросы сохраняемой переменной-члену. Основное отличие заключается в том, что функтор хранится как значение, а не как указатель, Вот почему функторы являются неполиморфными типами с обычной семантикой копирования. Обратите внимание на внутренние типы Рагепсгипссог::яези1стуре, Рагепсгипссог:: Рагв1 и РагепсГипссог.":Рагв2. В классе гипссогнапд1ег реализован простой конструктор, функция клонирования и несколько версий оператора О. Компилятор сам выберет правильный вариант.
Если перегрузка оператора О выполнена неверно, компилятор выдаст сообщение об ошибке. В реализации класса гипссогнад1ег нет ничего необычного, но это вовсе не означает, что она тривиальна. В следующем разделе будет показано, какой универсальностью обладает этот небольшой шаблонный класс. С помощью объявления класса гипссогнапд1ег легко написать шаблонный констРуктор класса гипссог. севр1асе <сурепаве я, с1азз тьззс> севр1асе <сурепаве гип> гипссог<а, тсз'зс>::гипссог(сопзс гипс Гип) зртвр! (пев гипссогнапд1ег<гипссог, гип>(Гип)); Глава 5.
Обобщенные функторы Злесь нет никакой ошибки. Два шаблонных параметра указаны правильно: выражение темр1асе <с1ааа я, с1азз ть!зс> относится к классу еипссог, а семр1ате <сурепаве еип> является параметром конструктора. В стандарте языка такой код называется "шаблонным определением члена вне класса" ("оцт-оГ-с!ааз шешбег !еепр!а(е дейл!с!оп").
В теле конструктора переменная зртмр1 устанавливается на новый объект типа еипссогнапд1ег, конкретизированный и проинициализированный соответствующими аргументами. Есть еше кое-что, о чем следовало бы упомянуть, кое-что, позволяющее лучше понять реализацию класса ецпстогнапд)ег. Обратите внимание на то, что при входе в тело конструктора класса емпссог полная информация о типах уже содержится в шаблонном параметре еип.
При выходе из конструктора эта информация теряется, поскольку всем объектам класса еипсгог известен лишь указатель зртмр1, ссылающийся на базовый класс ецпстогтмр1. Эта очевидная потеря информации весьма примечательна; конструктор знает тип и действует подобно фабрике, преобразующей этот тип в полиморфное поведение.
Информация о типах сохраняется в динамическом указателе на класс ецпссогтшр1. Хотя мы написали лишь часть кода (просто несколько функций, состоящих из одной строки), уже можно кое-что проверить. // предположим, что файл ецпстог.п включен // в реализацию класса еипстог «!пс1нде "ецпстог.!з" «тпс1иде <чозтгеам> // для небольших программ на с++ можно применять // директиву оадпд оз!пд памезрасе зсд; // Определяем тестируемый фуннтор зсгцсг тезгеипссог ( чо!д орегасогО(!пт 1, доиЫе д) соиг « "тезгеопссог::орегатогО(" «! « ", " « д « ") са11ед.
~п"; дпт шачпО ( тезтеипстог т; Еипстог<чо!д, туевьтвт 2(!пт, доиЫе)> сшд(б); смд(4, 4.5); Эта маленькая программа вывалит на печать следующую строку. тезтемпссог::орегасогО(4, 4.5) са11ед. Следовательно, мы достигли своей цели. Теперь можно идти дальше. 5Л. Один пишем, два в уме Читая предыдущий раздел, не залавались ли вы вопросом: "Почему мы не начали с реализации простых указателей на обычные функции?". Зачем мы перескочили сразу к 134 Часть |!. Компоненты функторам, шаблонам и т.п.? Ответ прост — в настоящее время поддержка указателей на обычные функции улге реааизоввна. Изменим немного нашу тестовую программу. Ф пс1цг(е Ч цпстог.)з" Фпс1цде <)озтгеав> цв(пц павезрасе зтг); // Определяем тестируемую функцию чотг) тезтгцпст(оп (з'пт 5, доцЫе д) ( соцт « "тезтгцпст)оп(" «( « ", " « г) « ") са11ед. 'хп"; тпт вазпО гцпссог<чоз'г), тчрвьт5т 2(зпт, г(оцЫе)> свд(тезтгцпст(оп); // выведет на печать строку // "тезтгцпст(оп(4, 4.5) са11ед." свд(4, 4.5); Объяснение этого приятного сюрприза кроется в способе вывода шаблонных параметров.
Когда компилятор обнаруживает класс гцпстог, созданный из функции тезтьцпстз'оп, он вынужден проверить шаблонный конструктор. Затем компилятор конкретизирует шаблонный конструктор шаблонным аргументом чо(б (в) (зот, г)оцЫе), представляющим собой тип значения, возвращаемого функцией тезтгцпстз'оп. Конструктор конкретизирует класс гцпстогнапд1ег<яцпстог<...>, чеза (в)(зпт, доцЫе)>.
Следовательно, переменная бцп в классе яцпссогнапд1ег также имеет тип чо)б (в)()пт, доцЫе). При вызове оператора гцпссогнапд1ег<...>::орегасогО он передается функции %я О. Это вполне допустимая синтаксическая конструкция, означающая вызов функции с помощью указателя. Итак, класс гцпссогнапд1 ег поддерживает указатели на внешние функции по двум причинам: благодаря синтаксической схожести указателей на функции и функторов и особенностям механизма вывода типов в языке Сч-ь. Однако есть одна проблема.
(У всех есть свои недостатки, не так ли?) При перегрузке функции тезтгцпст(оп (или любой другой функции, которая передается классу гцпстог<...>) возникает неопределенность. Ее причина заключается в том, что при перегрузке функции тезтгцпстзоп тип символа тезтгцпст)оп становится неопределенным. Чтобы проиллюстрировать этот факт, поместим перегруженную версию функции тезтгцпст) оп прямо перед функцией вази. // объявление перегруженной функции тезтгцст(оп // (определение не обязательно) чо(д теэтяцпст)оп((пт); Неожиданно компилятор сообшает, что не может распознать, какую из перегруженных версий функции тезтгвпст)оп следует использовать. Посколъку есть две функции с именем тезтьцпст) оп, одного имени для идентификации функции недостаточно. В сущности, при перегрузке есть два способа идентифицировать конкретную функцию: с помощью инициализации (или присваивания) или с помощью приведения типов.
Проиллюстрируем оба метода. // как и выше, функция тезтяопст(оп перегружена (пт ва)пО ( // для удобства используется оператор туребе1 135 Глава б. Обобщенные функторы суредеб чо)д (*тргип)(зпс, доиЫе); // способ 1: инициализация тргип рг = тезсгипсс!оп; гипссог<чосд, тчгвьсвт 2(дпс, доиЫе)> свд1(4, 4.5); // способ 2: приведение типов гипссог<чосд, дпс, доиЫе)> свд2( зсасзс сазс<тргип>(тезсгипссзоп)); // все правильно! д2(4, 4.5); Оба способа инициализации позволяют компилятору распознать требуемую версию функции тезсгисс1оп, которая должна получать параметры типа зпс и доиЫе, а возвращать — значение типа чо)д, 5.8. Преобразование типов аргументов и возвращаемого значения В идеале преобразование типов лля функторов должно выполняться так же, как и при вызовах обычных функций.
Тогда можно было бы написать примерно такой код. Вдпс1иде <зтгдпй> к!пс1иде <чозсгеав> Ипс1иде "гипссог.Ь" изз'по павезрасе зсд; // Аргументы игнорируются — в данном примере // они не представляют интереса сопзс сЬаг* тезсгипссчоп(доиЫе, доиЫе) аеас)с сопзс спаг Ьиттег() = "не11о, иог1д!"; // можно вернуть указатель на статический буфер гесигп Ьи(1ег; спс вазпО гипссог<зсгз'по тчгессБт 2(дпс, 1пс)> свд(тезсгипссдоп); // должен выводить на печать строку ?вог1д!" соис « свд(10, 10).зиЬзсг(7); Несмотря на то что сигнатура фактической функции тезсгипссз'оп немного отличается (она получает два параметра типа доиЫе и возврашает константу типа спать), ей можно поставить в соответствие функтор гипсСог<зтг!пй, ттядссвт 2(!пс, дпс)>.
Предполагается, что это возможно, поскольку тнп 1пс можно неявно преобразовать в тип доиЫе, а значение типа сопл сЬаг* — в тип зсгдпй. Если класс гипссог не поддерживает те же преобразования, что и язык С++, то такие жесткие ограничения следует считать неоправданными.