Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
494 В предыдущем примере конкретизация max(const char*, const char*) предшествует объявлению явной специализации. Поэтому компилятор имеет право предположить, что функция должна быть конкретизирована по обобщенному определению шаблона. Однако в программе не может одновременно существовать явная специализация и экземпляр, конкретизированный по тому же шаблону с тем же множеством аргументов. Когда в исходном файле после конкретизации встречается явная специализация max(const char*, const char*) , компилятор выдает сообщение об ошибке. Если программа состоит из нескольких файлов, то объявление явной специализации шаблона должно быть видимо в каждом файле, в котором она используется. Не разрешается в одних файлах конкретизировать шаблон функции по обобщенному определению, а в других специализировать с тем же множеством аргументов. Рассмотрим следующий пример: } Эта программа состоит из двух файлов. В файле File1.C нет объявления явной специализации max(const char*, const char*). Вместо этого шаблон функции конкретизируется из обобщенного определения. В файле File2.C объявлена явная // --------- max.h ------- // обобщенное определение шаблона template Type max( Type t1, Type t2 ) { /* ... */ } // --------- File1.C ------- #include #include "max.h" void another(); int main() { // конкретизация функции // const char* max< const char* >( const char*, const char* ); const char *p = max( "hello", "world" ); cout << "p: " << p << endl; another(); return 0; } // --------- File2.C ------- #include #include #include "max.h" // явная специализация шаблона для const char* typedef const char *PCC; template<> PCC max< PCC >( PCC s1, PCC s2 ) { /* ... */ } void another() { // явная специализация // const char* max< const char* >( const char*, const char* ); const char *p = max( "hi", "again" ); cout << " p: " << p << endl; return 0; С++ для начинающих 495 специализация, и при обращении к max("hi", "again") именно она и вызывается. Поскольку в одной и той же программе функция max(const char*, const char*) то конкретизируется по шаблону, то специализируется явно, компилятор считает программу некорректной. Для исправления этого объявление явной специализации шаблона должно предшествовать вызову функции max(const char*, const char*) в файле File1.C. Чтобы избежать таких ошибок и гарантировать, что объявление явной специализации шаблона max(const char*, const char*) внесено в каждый файл, где используется шаблон функции max() с аргументами типа const char*, это объявление следует поместить в заголовочный файл "max.h" и включать его во все исходные файлы, в которых используется шаблон max(): } Упражнение 10.10 Определите шаблон функции count() для подсчета числа появлений некоторого значения в массиве. Напишите вызывающую программу. Последовательно передайте в ней массив значений типа double, int и сhar. Напишите специализированный экземпляр шаблона count() для обработки строк. 10.7. Перегрузка шаблонов функций А Шаблон функции может быть перегружен. В следующем примере есть три перегруженных объявления для шаблона min(): // --------- max.h ------- // обобщенное определение шаблона template Type max( Type t1, Type t2 ) { /* ... */ } // объявление явной специализации шаблона для const char* typedef const char *PCC; template<> PCC max< PCC >( PCC s1, PCC s2 ); // --------- File1.C ------- #include #include "max.h" void another(); int main() { // специализация // const char* max< const char* >( const char*, const char* ); const char *p = max( "hello", "world" ); // .... С++ для начинающих 496 Type min( Type, Type ); // #3 Следующее определение main() иллюстрирует, как могут вызываться три объявленных таким образом функции: } Разумеется, тот факт, что три перегруженных шаблона функции успешно объявлены, не означает, что они могут быть также успешно вызваны. Такие шаблоны могут приводить к неоднозначности при вызове конкретизированного шаблона. Например, для следующего определения шаблона min5() int min5( T, T ) { /* ... */ } функция не конкретизируется по шаблону, если min5() вызывается с аргументами разных типов; при этом процесс вывода заканчивается с ошибкой, поскольку из фактических аргументов функции выводятся два разных типа для T. // определение шаблона класса Array // ( см. раздел 2.4) template // три объявления шаблона функции min() template Type min( const Array Type min( const Type*, int ); // #2 template #include { Array // Type == int; min( const Array // Type == int; min( const int*, int ) int ival1 = min( ia, 1024 ); // Type == double; min( double, double ) double dval0 = min( sqrt( iA[0] ), sqrt( ia[0] ) ); return 0; template С++ для начинающих 497 min5 ( i, ui ); Для разрешения второго вызова можно было бы перегрузить min5(), допустив два различных типа аргументов: int min5( T, U ); При следующем обращении производится конкретизация этого шаблона функции: min5( i, ui ); К сожалению, теперь стал неоднозначным предыдущий вызов: min5( 1024, i ); Второе объявление min5() допускает наличие у функции аргументов различных типов, но не требует этого. В нашем случае и T, и U типа int. Оба объявления шаблонов могут быть конкретизированы вызовом, в котором два аргумента функции имеют один и тот же тип. Единственный способ указать, какой шаблон более предпочтителен, устранив тем самым неоднозначность, – явно задать его аргументы. (О явном задании аргументов шаблона см. раздел 10.4.) Например: min5 Однако в этом случае мы можем обойтись без перегрузки шаблона функции. Поскольку шаблон min5(T,U) подходит для всех вызовов, для которых подходит min5(T,T), то одного объявления min5(T,U) вполне достаточно, а объявление min5(T,T) можно удалить. Мы уже говорили в главе 9, что, хотя перегрузка допускается, при проектировании таких функций надо быть внимательным и использовать ее только при необходимости. Те же соображения применимы и к определению перегруженных шаблонов. int i; unsigned int ui; // правильно: для T выведен тип int min5( 1024, i ); // вывод аргументов шаблона заканчивается с ошибкой: // для T можно вывести два разных типа template // правильно: int min5( int, usigned int ) // ошибка: неоднозначность: две возможных конкретизации // из min5( T, T ) и min5( T, U ) // правильно: конкретизация из min5( T, U ) С++ для начинающих 498 В некоторых ситуациях неоднозначности при вызове не возникает, хотя по шаблону можно конкретизировать две разных функции. Если имеются следующие два шаблона для функции sum(), то предпочтение будет отдано первому даже тогда, когда конкретизированы могут быть оба: int ival1 = sum Как это ни удивительно, такой вызов не приводит к неоднозначности. Шаблон конкретизируется из первого определения, так как выбирается наиболее специализированное определение. Поэтому для аргумента Type принимается int, а не int* Для того чтобы один шаблон был более специализирован, чем другой, оба они должны иметь одни и те же имя и число параметров, а для параметров разных типов, как, скажем, T* и T в предыдущем примере, параметр в одном шаблоне должен быть способен принять более широкое множество фактических аргументов, чем соответствующий параметр в другом. Например, для шаблона sum(Type*, int) вместо первого формального параметра функции разрешается подставлять только фактические аргументы типа “указатель”. В то же время в шаблоне sum(Type, int) первому формальному параметру могут соответствовать фактические аргументы любого типа. Первый шаблон sum(Type*, int) допускает более узкое множество аргументов, чем второй, т.е. он более специализирован, а следовательно, он и конкретизируется при вызове функции. 10.8. Разрешение перегрузки при конкретизации A В предыдущем разделе мы видели, что шаблон функции может быть перегружен. Кроме того, допускается использование одного и того же имени для шаблона и обычной функции: double sum( double, double ); Когда программа обращается к sum(), вызов разрешается либо в пользу конкретизированного экземпляра шаблона, либо в пользу обычной функции – это зависит от того, какая функция лучше соответствует фактическим аргументам. (Для решения такой проблемы применяется процесс разрешения перегрузки, описанный в главе 9.) Рассмотрим следующий пример: template Type sum( Type*, int ); template Type sum( Type, int ); int ia[1024]; // Type == int ; sum // Type == int*; sum // шаблон функции template Type sum( Type, int ) { /* ... */ } // обычная функция (не шаблон) С++ для начинающих 499 } Будет ли при обращении к sum(dd,ii) вызвана функция, конкретизированная из шаблона, или обычная функция? Чтобы ответить на этот вопрос, выполним по шагам процедуру разрешения перегрузки. Первый шаг заключается в построении множества функций-кандидатов состоящего из одноименных вызванной функций, объявления которых видны в точке вызова. Если существует шаблон функции и на основе фактических аргументов вызова из него может быть конкретизирована функция, то она будет являться кандидатом. Так ли это на самом деле, зависит от результата процесса вывода аргументов шаблона. (Этот процесс описан в разделе 10.3.) В предыдущем примере для вывода значения аргумента Type шаблона используется фактический аргумент функции dd. Тип выведенного аргумента оказывается равным double, и к множеству функций-кандидатов добавляется функция sum(double, int) . Таким образом, для данного вызова имеются два кандидата: конкретизированная из шаблона функция sum(double, int) и обычная функция sum(double, double) После того как функции, конкретизированные из шаблона, включены в множество кандидатов, процесс вывода аргументов шаблона продолжается как обычно. Второй шаг процедуры разрешения перегрузки заключается в выборе устоявших функций из множества кандидатов. Напомним, что устоявшей называется функция, для которой существуют преобразования типов, приводящие каждый фактический аргумент функции к типу соответствующего формального параметра. (В разделе 9.3 описаны преобразования типов, применимые к фактическим аргументам функции.) Нужные трансформации существуют как для конкретизированной функции sum(double, int), так и для обычной функции sum(double, double). Следовательно, обе они являются устоявшими. Проведем ранжирование преобразований типов, примененных к фактическим аргументам для выбора наилучшей из устоявших функций. В нашем примере оно происходит следующим образом: Для конкретизированной из шаблона функции sum(double, int): • для первого фактического аргумента как сам этот аргумент, так и формальный параметр имеют тип double, т.е. мы видим точное соответствие; • для второго фактического аргумента как сам аргумент, так и формальный параметр имеют тип int, т.е. снова точное соответствие. Для обычной функции sum(double, double): • для первого фактического аргумента как сам этот аргумент, так и формальный параметр имеют тип double – точное соответствие; • для второго фактического аргумента сам этот аргумент имеет тип int, а формальный параметр – тип double, т.е. необходимо стандартное преобразование между целым и плавающим типами. void calc( int ii, double dd ) { // что будет вызвано: конкретизированный экземпляр шаблона // или обычная функция? sum( dd, ii ); С++ для начинающих 500 Если рассматривать только первый аргумент, то обе функции одинаково хороши. Однако для второго аргумента конкретизированная из шаблона функция лучше. Поэтому наиболее подходящей (лучшей из устоявших) считается функция sum(double, int). Функция, конкретизированная из шаблона, включается в множество кандидатов только тогда, когда процесс вывода аргументов завершается успешно. Неудачное завершение в данном случае не является ошибкой, но кандидатом функция считаться не будет. Предположим, что шаблон функции sum() объявлен следующим образом: int sum( T*, int ) { ... } Для описанного вызова функции вывод аргументов шаблона будет неудачным, так как фактический аргумент типа double не может соответствовать формальному параметру типа T*. Поскольку для данного вызова и данного шаблона конкретизировать функцию невозможно, в множество кандидатов ничего не добавляется, т.е. единственным его элементом останется обычная функция sum(double, double). Именно она вызывается при обращении, и ее второй фактический аргумент приводится к типу double. А если вывод аргументов шаблона завершается удачно, но для них есть явная специализация? Тогда именно она, а не функция, конкретизированная из обобщенного шаблона, попадает в множество кандидатов. Например: } При обращении к sum() внутри manip() в процессе вывода аргументов шаблона обнаруживается, что функция sum(double,int), конкретизированная из обобщенного шаблона, должна быть добавлена к множеству кандидатов. Но для нее имеется явная специализация, которая и становится кандидатом. На более поздних стадиях анализа выясняется, что эта специализация дает наилучшее соответствие фактическим аргументам вызова, так что разрешение перегрузки завершается в ее пользу. Явные специализации шаблона не включаются в множество кандидатов автоматически. Лишь в том случае, когда вывод аргументов завершается успешно, компилятор будет рассматривать явные специализации данного шаблона: // шаблон функции template // определение шаблона функции template // явная специализация для Type == double template<> double sum // обычная функция double sum( double, double ); void manip( int ii, double dd ) { // вызывается явная специализация шаблона sum С++ для начинающих 501 } Шаблон функции min() специализирован для аргумента double. Однако эта специализация не попадает в множество функций-кандидатов. Процесс вывода для вызова min() завершился неудачно, поскольку аргументы шаблона, выведенные для Type на основе разных фактических аргументов функции, оказались различными: для первого аргумента выводится тип double, а для второго – int. Поскольку вывести аргументы не удалось, в множество кандидатов никакая функция не добавляется, и специализация min(double, double) игнорируется. Так как других функций-кандидатов нет, вызов считается ошибочным. Как отмечалось в разделе 10.6, тип возвращаемого значения и список формальных параметров обычной функции может точно соответствовать аналогичным атрибутам функции, конкретизированной из шаблона. В следующем примере min(int,int) – это обычная функция, а не специализация шаблона min(), поскольку, как вы, вероятно, помните, объявление специализации должно начинаться с template<>: int min( int, int ) { } Вызов может точно соответствовать как обычной функции, так и функции, конкретизированной из шаблона. В следующем примере оба аргумента в min(ai[0],99) имеют тип int. Для этого вызова есть две устоявших функции: обычная min(int,int) и конкретизированная из шаблона функция с тем же типом возвращаемого значения и списком параметров: } Однако такой вызов не является неоднозначным. Обычной функции, если она существует, всегда отдается предпочтение, поскольку она реализована явно, так что перегрузка разрешается в пользу обычной функции min(int,int). // определение шаблона функции template Type min( Type, Type ) { /* ... */ } // явная специализация для Type == double template<> double min // ошибка: вывод аргументов шаблона неудачен, // нет функций-кандидатов для данного вызова min( dd, ii ); // объявление шаблона функции template T min( T, T ); // обычная функция min(int,int) int ai[4] = { 22, 33, 44, 55 }; int main() { // вызывается обычная функция min( int, int ) min( ai[0], 99 ); С++ для начинающих 502 Если перегрузка разрешилась таким образом, то изменений уже не будет: если позже обнаружится, что в программе нет определения этой функции, компилятор не станет конкретизировать ее тело из шаблона. Вместо этого на этапе сборки мы получим ошибку. В следующем примере программа вызывает, но не определяет обычную функцию min(int,int) , и редактор связей выдает сообщение об ошибке: } Зачем определять обычную функцию, если ее тип возвращаемого значения и список параметров соответствуют функции, конкретизированной из шаблона? Вспомните, что при вызове конкретизированной функции к ее фактическим аргументам в ходе вывода аргументов шаблона можно применять только ограниченное множество преобразований. Если же объявлена обычная функция, то для приведения типов аргументов допустимы любые трансформации, так как типы формальных параметров обычной функции фиксированы. Рассмотрим пример, показывающий, зачем может потребоваться объявить обычную функцию. Предположим, что мы хотим определить специализацию шаблона функции min . Нужно, чтобы именно эта функция вызывалась при обращении к min() с аргументами любых целых типов, пусть даже неодинаковых. Из-за ограничений, наложенных на преобразования типов, при передаче фактических аргументов разных типов функция min // шаблон функции template T min( T, T ) { ... } // это обычная функция, не определенная в программе int min( int, int ); int ai[4] = { 22, 33, 44, 55 }; int main() { // ошибка сборки: min( int, int ) не определена min( ai[0], 99 ); |