Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 92
Текст из файла (страница 92)
В примере выше +, 1 (1 и конструктор т не определены в точке определения шаблона. Объявление в шаблоне тоже нелопустимо, так как нельзя задать их типы. Например, + может быть встроенным оператором, функцией-членом или глобальной функцией. Если это функция, она может принимать аргументы типа т, сонэк та и т.д. Это не что иное, как проблема задания ограничений на аргументы шаблона (см. раздел 15.4).
Принимая во внимание тот факт, что ни в точке определения шаблона, ни в точке его использования нет достаточного контекста для инстанцирования, мы должны найти компромиссное решение. Опо заключается в том, чтобы разделить встречающиеся в определении шаблона имена на две категории: ПИИИ1166 Шаблоны Было бы неразумно требовать, чтобы проектировщик класса предоставлял все функции, которые в будущем, возможно, пригодятся автору шаблона. Предвидеть все невозможно. Поэтому понятие «зависит от аргумента шаблона т» должно опираться на контекст в точке инстанцирования, по крайней мере, в такой степени, чтобы уметь находить глобальные функции, используемые вместе с Т.
При этом неизбежно появляется возможность случайно подобрать какую-нибудь лишнюю функцию. Но эта проблема не слишком серьезна. Мы определим «зависимость от аргумента шаблона т» наиболее стандартным способом. Именно так, вызов функции зависит от аргумента шаблона, если был разрешен по-другому или вообще не разрешен в случае отсутствия в программе фактического типа шаблона. Для компилятора проверить такое условие относительно просто.
Вот примеры вызовов, зависящих от аргумента шаблона т: о у вызванной функции есть формальный параметр, зависящий от т в соответствии с правилами выведения типа (см. раздел 15.6.1). Например, № (Т), 1(чессог<т>), 1(сопэс Т*); о тип фактического аргумента зависит от т в соответствии с правилами выведения типа (см раздел 15 6 1) Так, № ( Т ( 1 ) ), № ( С ), 1 ( д ( С ) ) и № ( йг ), если предположить, что с — это т; о вызов разрешается использованием преобразования к типу т, хотя ни фактический аргумент, ни формальный параметр вызываемой функции не принадлежат типу, зависящему от т, так, как в первом и втором случаях.
Последний пример взят из реальной программы, и зависящий от этого правила код был вполне удачен. Вызов 1 (1), на первый взгляд, не зависит от т, как не зависит от т и функция № ( в ), к которой идет обращение. Но тип т аргумента шаблона имел конструктор из типа тпс и являлся производным от В, поэтому Е (1) разрешалось как № ( В ( Т ( 1 ) ) ) . 15. 10.2.2. Неоднозначности Что надо было бы сделать, если в точке № 1 (точка определения шаблона в примере из раздела 15.10.2) и в точке № 2 (точка использования) обнаруживаются различные функции? Мы могли бы: сз отдать предпочтение №1; (з предпочесть №2; о выдать ошибку.
Отметим, что в точке №1 можно искать только не-функции и функции, для которых типы аргументов уже известны в точке использования в определении шаблона. Поиск же остальных имен откладывается до точки №2. Первоначальное правило требует предпочесть точку №2, откуда следует, что применимы обычные требования разрешения неоднозначности. Ведь только в случае, когда в точке № 2 найдено лучшее соответствие, могут возникнуть расхождения с тем, что найдено в точке №1. К сожалению, при этом приходится не верить собственным глазам, читая определение шаблона.
Например; Инстанцированиа шаблонов ИИИВИИИИ с)ояЫе ядгс (с)оыЫе) Сетр1яяе<с1аяя Т> чо1с) 1(Т 'с) ( с(с яЫе яд2 = ядгя (2); // ) Кажется очевидным, что яс(гс(2) вызовет яс(гс (с)опЬ1е). Но в точке ()2 вполне может обнаружиться функция ястгс (1пс). В большинстве случаев это неважно, так как правило «должно зависеть от аргумента шаблона> гарантирует использование именно «очевидного> разрешения в пользу яс)гС (с)оиЬ1е) . Однако если бы т был равен 1пс, то вызов ясггс (2) зависел бы от аргумента шаблона, так что вызов разрешился бы в пользу яс(гс (дпс ) . Это неустранимое следствие того, что мы принимаем во внимание точку ()2, но, по-моему, оно вызывает много путаницы.
Хотелось бы как-то решить зту проблему. С другой стороны, я считал необходимым отдавать предпочтение именно точке () 2, ибо только тогда можно разрешить использование членов базового класса таким же способом, как при работе с обычными (нешаблонными) классами. Например: чо1<) д(); Сеяср1аяе<с1яяя Т> с1яяя Х: риЫТс Т ( чо1с) с() ( д() ' // ): Если в т есть функция-член д ( ), следовало бы вызывать именно эту д ( ), поскольку так ведут себя нешаблонные классы: чо1с( д(): с1аяя Т ( рыЫТс: чогс) д(); ); с1аяя т: рыЫТс Т ( чо1<) 1() ( д(); ) // )' // вызывается Т::д С другой стороны, в самых типичных случаях то, что найдено в точке () 1, обычно корректно.
Так работает в С++ поиск глобальных имен, именно такая модель позволяет на ранних стадиях обнаруживать большую часть ошибок, предварительно компилировать большинство шаблонов и именно такой механизм зашишает от «случайного» заимствования имен в контексте, неизвестном автору шаблона. Несколько разработчиков компиляторов, особенно Билл Гиббонс, убедительно доказывали, что предпочтение следует отдать точке () 1. Какое-то время я склонялся к тому, чтобы считать ошибкой нахождение разных функций в двух данных точках, но зто лишь усложняет задачу разработчиков Шаблоны БИИИИИИВ <)опте яств (<)опЫе); Т> чо1с) Т(Т С) Сешр1аое<с1аяя ( // яс(гя(2)/ япто(Т(2)); // разрешается в точке Ф1 // очевидно зависит от Т // привязка в точке ()2 Тпс д(); т> с1аяя х : риЫ1с т ( сешр1асе<с1аяв чогс( т () ( и(); Т: па(); // разрешается в точке ((1 // очевидно зависит от Т // привязка в точке ()2 ) // ); От автора шаблона требуется более явно выражать свое намерение, когда он хочет использовать некоторую функцию, не видимую в определении шаблона.
Похоже, мы достигаем разумного поведения по умолчанию. 15.10.3. Специаяиззция Шаблон описывает, как определяется функция или класс при любых значениях его аргументов. Например, шаблон Сешр1аое<с1аяя Т> с1аяя СошрагаЫе ( // 1пс оретасог==(сопяс ть а, попас та Ы ( тессгп а==ы ); означает, что для каждого типа Т элементы сравниваются с помощью оператора ==.
К сожалению, это слишком ограничительное условие. В частности, в С строки, представленные типом с)заг*, обычно сравниваются функцией эскобар (). компиляторов, не давая ощутимых выгод пользователям. Кроме того, оказалась бы возможной ситуация, когда употребление определенных имен в контексте использования шаблона портит его удачный код, написанный программистом, думающим, что будут использоваться имена из области действия в точке определения шаблона.
В конце концов нашелся довод, который окончательно склонил меня отдавать предпочтение тому, что было найдено в точке () 1. Он состоял в следующем: некоторый весьма запутанный пример л~ог быть тривиально разрешен автором шаблона Сравните: Инстанцирование шаблонов ЯИВИИИИБ Севр1асе<с1аяя Т> с1аяя чесоог ( // ть орегасог()(1пс 1); ); то можно ввести специализации, то есть отдельные объявления, скажем, для чессог<с)таг> или чессог<совр1ех>:: орегасог () (1пс ): с1аяя чесгог<спаг> ( // свата орегаяог()(1пг 1); совр1еха чессог<совр1ех>::орегасог[)(гпс 1) ( /* ...
*/ ) Это позволяет программисту вводить специализированные реализации для классов, которые либо особенно важны с точки зрения производительности, либо имеют отличакнцуюся от стандартной семантику. Грубый, но очень эффективный механизм. Исходная моя идея состояла в том, чтобы поместить такие специализации в библиотеку и автоматически вызывать их по мере необходимости без вмешательства программиста. Оказалось, что цена такой услуги велика, а ценность сомнительна. Специализация приводила к трулностям для понимания и реализации, так как заранее было неизвестно, что будет подставлено для конкретного набора аргументов шаблона, — даже если у нас перед глазами имелось его определение, — ибо этот шаблон мог быть специализирован в другой единице трансляции.
Например; Севр1аге<с1аяя Т> с1аяя Х ( Т ч; риЬ11с: Т геао() сопяс ( гесагп ч; ) чо1о вггсе('пс чч) ( ч = чч/ ) Во время первоначального проектирования мы нашли множество таких примеров, а также обнаружили, что «особые случаи» зачастую чрезвычайно важны. Благоларя им уластся придать единообразие языку и повысить производительность.
Строки в стиле С дают прекрасный примср. Поэтому я пришел к выводу, что нужен механизм для специализации шаблонов. Это можно было сделать, либо приняв общие правила перегрузки, либо с помощью специального подхода. Я выбрал последнее, так как лумал, что решаю в основном проблему нерегулярности, берущую свое начало в С, а также потому, что предложение о перегрузке неизменно встречает массу протестов. В первоначальном проекте специализация была определена как ограниченная и аномальная форма перегрузки и плохо увязывалась с остальными частями языка.