Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 89
Текст из файла (страница 89)
Итак, критерий сортировки не встраивается ни в контейнер, ни в тип элемента. Вместо этого он передается в виде операции, которую следует выполнить. Если в качестве примера взять строки, состоящие из слов — личных шведских имен, тогда какую схему упорядочения применить для сравнения? Для шведского языка обычно применяются две разные схемы. Но нет сомнения, что нельзя давать сведения о соглашениях, принятых для такой операции, ни общему типу строки, ни общему алгоритму сортировки. Таким образом, любое общее решение включает выраженный в общих терминах алгоритм сортировки, который можно определить не для конкретного типа, а лля конкретного использования данного типа.
Давайте, например, обобщим стандартную библиотечную функцию в сгсшр ( ) для работы со строками любого типа т. Сначала определяется шаблон класса с семантикой сравнения объекта типа т по умолчанию: Гетр1асе<с1авв Т> с1авв СИР ( рпЫ!с: вгагйс !пг ес((т а, т ы ( гегагп а==ь; ) всаг1с 1пс 1С(Т а, Т Ы ( гегцгп а<Ь; ) ); Здесь необходимо использовать шаблон, поскольку мы проеюируем контейнер. Наследование от класса Сопгаьпег необходимо, чтобы Сопггошей сопга1пег можно было использовать в качестве контейнера.
Использование ар~умента шаблона л позволяет применять различные распределители. Например: ~ИИИИИИВ Шаблоны В шаблоне функции сотраге ( ] для сравнения аргументов типа Ьаяфс всгфпд используется такая форма: тевр1ате,<с1аяя Т> а1аяя Ьаятс ятттпд ( // ); тевр1атесс1аяя т, с1аяя С = СМР<Т» тпт совраге(аопят Ьав'с ятг1пд<т>й ятг1, сопят Ьая[а втт1пд<Т>й яаг2] ( сот(1пт 1=0; 1<ятт1.1епдтЬ() йй 1<втт2.1епдтп(); 1++) 1й (!С::ес)(втт1[т),ятг2[1))) тетптп С::1т(яат1[1],ятт2[1]); тетптп втт2. 1епдтп()-ятг1.1еадтЬ(); ) ауредет ьаяйс яаг[пд<аьат> ятттпд; Имея в своем распоряжении шаблоны-члены (см.
раздел 15.9.3), функцию совраге ( ) можно было бы определить и в виде члена класса ьавтс всгтпд. Если требуется, чтобы С<т> производил сравнение без учета регистра, по с учетом специфики конкретного языка, возвращал наибольшее значение в коде па[со([е, когда аргументы неравны (имеется в виду с<т>:: ес) ( ) ) и т.д., то нужно лишь правильным способом определить С<Т>:: ед() и С<Т>:: 1С (] через характерные для типа т операторы. Тогда любой алгоритм (сравнения, сортировки и т.п.) можно выразить в терминах операций, предоставляемых классом Сир и контейнером.
Например: а1аяя Ь1ТЕВАТЕ ятаттс [пт ед(адат а, аьат Ы ( теаитп а==Ь; ) ятаттс [пт 1т(спаг,апат); // использовать книжный порядок чо1б 1(ятт1пд яиеде1, ятг1пд яиеде2) ( соврете(яме<]е1,яиебе2)/ // обычный (телефонный) порядок совраге<спат,ЫТЕЕАТЕ>(яиеде1,яиебе2); // книжный порядок ] чо16 т (ятт1пд я1, ятттпд я2) совраге(я1,я2); аоврате<спаг,НОСАЯЕ>(я1,в2); ] // а учетом регистра // без учета региатра Я передаю критерий сравнения в виде параметра шаблона, поскольку именно так можно передать операции без лишних затрат во время выполнения. В частности, операторы сравнения ес[() и 1г () легко встроить. Аргумент по умолчанию используется, чтобы не обременять пользователей громоздкой нотацией.
Другие варианты этой техники рассматриваются в [2п([, 98А]. Более характерный пример — это сравнение с учетом и без учета регистра: Соотношения между шаблонами классов ДфффЯЯЯЩ Отметим, что шаблон класса смр никогда не используется для определения объектов; все его члены статические и открытые. Поэтому его следовало бы сделать пространством имен (см. главу 17); Сепр1апе<с1аяв Т> паяаврасе СМР ( гпс е!((т а, т ь) ( геспгп а==ь; ) гпс 1с(т а, т Ь) ( гаспгп а<Ь; ) ) К сожалению, шаблоны-пространства имен (пока еще) не включены в С++. 15.9. Соотношения между шаблонами классов Шаблон стоит рассматривать как спецификацию для создания конкретных типов. Другими словами, реализация шаблона — это механизм генерирования типов, указанных пользователем.
Согласно правилам языка С++ два класса, сгенерированные из одного и того же шаблона, никак не связаны между собой. Например: севр1асе<с1аяв т> с1авв Бес ( /* ... */ с1авя БЬаре ( /* ... *! ); с1аяв Сггс1е : риьтгс Бпаре ( /* ... */ ); Видя подобные объявления, пользователи зачастую трактуют Яеп<С1гс1е> как класс, производный от яес<БЬаре>, или яес<сггс1е*> — как производный от Яес<ЯЬаре*>. Например: воЫ Г(Бев<БЬаре>а); ясгс д(зес<Сгпс1еа в) ( Й(я) ! ) Этот пример не будет компилироваться, поскольку не существует встроенного преобразования из Бес<с(гс1е>а в яес<БЬаре>а.
Да и не должно его быть; полагать, что яес<с(гс1е> — частный случай Бес<я)таре>, — принципиальная (и не такая уж редкая) концептуальная ошибка. В частности, класс Бее<С(гс1е> гарантирует, что все его элементы принадлежат С(гс1е (окружность), и значит, пользователи могут безопасно и эффективно применять к ним все операции, определенные для окружностей, например запрашивать значение радиуса. Если бы мы разрешили трактовать Яес<сйгс1е> как Бес<Я)таре>, то уже не могли бы дать такой гарантии, поскольку в множество Бес< БЬаре> можно поместить и другие геометрические фигуры, например, треугольники.
15.9Л. Отношения наследования Следовательно, по умолчанию между классами, сгенерированными из одного и того же шаблона, не может быть никаких отношений. Но иногда такое отношение полезно, Нужна ли специальная операция для выражения такого рода отношений7 Шаблоны НИИИИИВФ Я отверг эту идею, поскольку многие полезные преобразования можно выразить с помощью отношений наследования или обычных операторов-конверторов. Однако в результате ие существует способа выразить некоторые отношения.
Например, располагая Генр1асе<с1аяя Т> с1авя Рлг ( // указатель на Т // ); часто хотелось бы для таких определенных пользователем указателей Рсг иметь такие же отношения наследования, к которым мы привыкли при работе со встроенными указателями. Например: уоЫ б(РГг<сьгс1е> рс) ( Рсг<вларе> рв = рс; // имеет ли зто смысл? ) Желательно, чтобы это было разрешено только тогда, когда я)заре действительно является непосредственным или косвенным открытым базовым классом для С1гс1е.
Дэвид Джордан (РауЫ,) огг(ап) от имени консорциума поставщиков объектно-ориентированных баз данных просил комитет по стандартизации обеспечить такое свойство для «умных» указателей. Решение дают шаблоны-члены, которые пока ие включены в С++: Генр1асе<с1авя т1> с1авв Рлг (// указатель на Т1 // генр1аге<с1аяя т2> орегагог Ргг<т2> (); Нам нужно определить конвертор таким образом, чтобы преобразование Ркг<Т1> в Рбг<Т2 > было допустимо только в случаях, когда т1* можно присвоить т2 *. Это можно сделать, предоставив для Рсг дополнительный конструктор: Геир1асе<с1авв Т> с1авв Рсг ( // указатель на Т т* р; ри)>1(с: Ргг(т*)/ Геир1асе<с1аяв Т2> орегалог Рсг<Т2> (); гегигл Ргг<т2>(р); // работает только тогда, когда // р можно преобразовать в т2* ) // ); В этом решении ие используются приведения типов.
Предложение гесигп будет компилироваться, только когда р может являться аргументом для конструктора Рсг<т2 >. В приведенном примере р имеет тип т1*, а конструктору необходим аргумент типа Т2*. Это применение метода ограничения через использование (см. раздел 15.4.2).
Если вы предпочитаете иметь закрытый конструктор, то можете воспользоваться приемом, предложенным Джонатаном Шопиро: Соотношения между шаблонами классов Яфффф$ЯЯЩ Сепр1асе<с1авв Т> с1авя Рог ( // указатель на Т т* гр; Рог(Т*); ггтепб гепр1аге<с1авв т2> с1авв Ргг<т2>; роЫТс: гепр1аге<с1авв т2> орегагог Ргг<т2> (); // )' Шаблоны-члены описаны в разделе 15.9.3. 15.9.2. Преобразования С вышеописанной проблемой тесно связана другая: не существует единого способа определить преобразования между различными классами, сгенерированными из одного и того же шаблона. Рассмотрим, например, шаблон сопр1ех, который определяет комплексные числа для некоторого множества скалярных типов: Гепр1асе<с1авв вса1аг> с1авв сопр1ех ( вса1аг ге, Тп; роЫтс: // ); Мы можем использовать сопр1ех<й1оас>, сопр1ех<с(ои)21е> и т д., но при этом желательно, чтобы существовало преобразовывание из типа сопр1ех с низкой точностью в тип сопр1ех с высокой точностью.
Например: сопр1ех«)ооЫе> вс)гг (сопр1ех<дооЫе>); сопр1ех<11оас> с1(1.2г,б.тг); сопр1ех<дооЫе> с2 = вцгг(с1);// ошибка: несоответствие типов: // ожидается сопр1ех<доиЫе> Было бы неплохо, если бы сушествовал какой-то способ сделать вызов вс(гс допустимым. Для этого программисты отказываются от шаблонов в пользу дублирования определения классов: с1авв г1оас сопр1ех ( г1оас ге, 1п; роЫто: // ): с1авв бооЫе сопр1ех ( бооЫе ге, 1п; роЫ1с: дооЫе сопр1ех(г1оас сопр1ех с) : ге(с.ге), тп(с.тп) () // ): Цель такого дублирования — определить конструктор, который и задает преобразование. Шаблоны ПИИИИИВ И в этом случае, иа мой взгляд, возможны только решения, требующие вложеииых шаблонов в сочетании с той или иной формой ограничений.
Фактически ограничение может быть иеявиым: Севр1асе<с1авв вса1ас> с1авв совр1ех яса1аг ге, 1п1 риь1!с: Сепр1аСе<с1авв Т2> совр1ех(сопвС совр1ех<Т2>а с) се(с.се), (в(с.1в) ( ) // ): Другими словами, скоиструировать совр1ех<Т1> из совр1ех<Т2> удается только в том случае, когда есть возможность инициализировать т1 с помощью Т2. Это представляется разумным. Такое определение включает и обычный копирующий конструктор.
В данной ситуации приведенный выше пример с яс(гс ( ) становится законным. К сожалению, определение допускает и сужающие преобразования комплексных чисел просто потому, что в С++ допустимы сужающие преобразования для скаляров. Естественно, если принять такое определение совр1ех, то компилятор, предупреждающий о сужаюших преобразованиях для скаляров, будет предупреждать и о сужаюших преобразованиях для значений типа совр1ех. Мы можем получить <привычиые> имена с помощью Суре<1ес: сурес(ет совр1ех<11оаС> 11оас совр1ех; Суреает совр1ех<аооЬ1е> аоивте совр1ех; Суреаес совр1ех<1опд с(ооЬ1е> 1опд аооЬ1е совр1ех; Думается, что варианты без суре<(ей читаются лучше.