Меню сайта |
|
|
Категория |
|
|
Развлечение |
|
|
ON - LINE |
|
|
Опрос |
|
|
Оbserver Ward |
Онлайн всего: 1 Гостей: 1 Пользователей: 0
|
|
Друзья сайта |
|
|
Hаша кнопка |
Для обмена банерами , наша кнопка для размещения у вас на сайте
|
|
|
10. Шаблоны функций (2)
10.6. Явная специализация шаблона А
Не всегда удается написать шаблон функции, который годился бы для всех возможных типов, с которыми он может быть конкретизирован. В некоторых случаях имеется специальная информация о типе, позволяющая написать более эффективную функцию, чем конкретизированная по шаблону. А иногда общее определение, предоставляемое шаблоном, для некоторого типа просто не работает. Рассмотрим, например, следующее определение шаблона функции max():
// обобщенное определение шаблона template <class T> T max( T t1, T t2 ) { return ( t1 > t2 ? t1 : t2 ); }
Когда этот шаблон конкретизируется с аргументом типа const char*, то обобщенное определение оказывается семантически некорректным, если мы интерпретируем каждый аргумент как строку символов в смысле языка C, а не как указатель на символ. В этом случае необходимо предоставить специализированное определение для конкретизации шаблона. Явное определение специализации – это такое определение, в котором за ключевым словом template следует пара угловых скобок <>, а за ними – определение специализированного шаблона. Здесь указывается имя шаблона, аргументы, для которых он специализируется, список параметров функции и ее тело. В следующем примере для max(const char*, const char*) определена явная специализация:
#include <cstring>
// явная специализация для const char*: // имеет приоритет над конкретизацией шаблона // по обобщенному определению
typedef const char *PCC; template<> PCC max< PCC >( PCC s1, PCC s2 ) { return ( strcmp( s1, s2 ) > 0 ? s1 : s2 );
Поскольку имеется явная специализация, шаблон не будет конкретизирован с типом const char* при вызове в программе функции max(const char*, const char*). При любом обращении к max() с двумя аргументами типа const char* работает специализированное определение. Для любых других обращений функция сначала конкретизируется по обобщенному определению шаблона, а затем вызывается. Вот как это выглядит:
#include <iostream>
// здесь должно быть определение шаблона функции max() // и его специализации для аргументов const char* int main() { // вызов конкретизированной функции: int max< int >( int, int ); int i = max( 10, 5 );
// вызов явной специализации: // const char* max< const char* >( const char*, const char* ); const char *p = max( "hello", "world" );
cout << "i: " << i << " p: " << p << endl; return 0; }
Можно объявлять явную специализацию шаблона функции, не определяя ее. Например, для функции max(const char*, const char*) она объявляется так:
// объявление явной специализации шаблона функции template< > PCC max< PCC >( PCC, PCC );
При объявлении или определении явной специализации шаблона функции нельзя опускать слово template и следующую за ним пару скобок <>. Кроме того, в объявлении специализации обязательно должен быть список параметров функции:
// ошибка: неправильные объявления специализации // отсутствует template<> PCC max< PCC >( PCC, PCC );
// отсутствует список параметров template<> PCC max< PCC >;
Однако здесь можно опускать задание аргументов шаблона, если они выводятся из формальных параметров функции:
// правильно: аргумент шаблона const char* выводится из типов параметров template<> PCC max( PCC, PCC );
В следующем примере шаблон функции sum() явно специализирован:
template <class T1, class T2, class T3> T1 sum( T2 op1, T3 op2 );
// объявления явных специализаций // ошибка: аргумент шаблона для T1 не может быть выведен; // он должен быть задан явно template<> double sum( float, float ); // правильно: аргумент для T1 задан явно, // T2 и T3 выводятся и оказываются равными float template<> double sum<double>( float, float ); // правильно: все аргументы заданы явно template<> int sum<int,char>( char, char );
Пропуск части template<> в объявлении явной специализации не всегда является ошибкой. Например:
// обобщенное определение шаблона template <class T> T max( T t1, T t2 ) { /* ... */ }
// правильно: обычное объявление функции const char* max( const char*, const char*);
Однако эта инструкция не является специализацией шаблона функции. Здесь просто объявляется обычная функция с типом возвращаемого значения и списком параметров, которые соответствуют полученным при конкретизации шаблона. Объявление обычной функции, являющееся конкретизацией шаблона, не считается ошибкой. Так почему бы просто не объявить обычную функцию? Как было показано в разделе 10.3, для преобразования фактического аргумента функции, конкретизированной по шаблону, в соответствующий формальный параметр в случае, когда этот аргумент принимает участие в выводе аргумента шаблона, может быть применено лишь ограниченное множество преобразований типов. Точно так же обстоит дело и в ситуации, когда шаблон функции специализируется явно: к фактическим аргументам функции при этом тоже применимо лишь ограниченное множество преобразований. Явные специализации не помогают обойти соответствующие ограничения. Если мы хотим выйти за их пределы, то должны определить обычную функцию вместо специализации шаблона. (В разделе 10.8 этот вопрос рассматривается более подробно; там же показано, как работает разрешение перегруженной функции для вызова, который соответствует как обычной функции, так и экземпляру, конкретизированному из шаблона.) Явную специализацию можно объявлять даже тогда, когда специализируемый шаблон объявлен, но не определен. В предыдущем примере шаблон функции sum() лишь объявлен к моменту специализации. Хотя определение шаблона не обязательно, объявление все же требуется. То, что sum() – шаблон, должно быть известно до того, как это имя может быть специализировано. Такое объявление должно быть видимо до его использования в исходном файле. Например:
#include <iostream> #include <cstring>
// обобщенное определение шаблона template <class T> T max( T t1, T t2 ) { /* ... */ }
int main() { // конкретизация функции // const char* max< const char* >( const char*, const char* ); const char *p = max( "hello", "world" );
cout << "p: " << p << endl; return 0; }
// некорректная программа: явная специализация const char *: // имеет приоритет над обобщенным определением шаблона typedef const char *PCC; template<> PCC max< PCC >(PCC s1, PCC s2 ) { /* ... */ }
В предыдущем примере конкретизация max(const char*, const char*) предшествует объявлению явной специализации. Поэтому компилятор имеет право предположить, что функция должна быть конкретизирована по обобщенному определению шаблона. Однако в программе не может одновременно существовать явная специализация и экземпляр, конкретизированный по тому же шаблону с тем же множеством аргументов. Когда в исходном файле после конкретизации встречается явная специализация max(const char*, const char*), компилятор выдает сообщение об ошибке. Если программа состоит из нескольких файлов, то объявление явной специализации шаблона должно быть видимо в каждом файле, в котором она используется. Не разрешается в одних файлах конкретизировать шаблон функции по обобщенному определению, а в других специализировать с тем же множеством аргументов. Рассмотрим следующий пример:
// --------- max.h ------- // обобщенное определение шаблона template <class Type> Type max( Type t1, Type t2 ) { /* ... */ }
// --------- File1.C ------- #include <iostream> #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 <iostream> #include <cstring> #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; }
Эта программа состоит из двух файлов. В файле File1.C нет объявления явной специализации max(const char*, const char*). Вместо этого шаблон функции конкретизируется из обобщенного определения. В файле File2.C объявлена явная специализация, и при обращении к 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():
// --------- max.h ------- // обобщенное определение шаблона template <class Type> Type max( Type t1, Type t2 ) { /* ... */ }
// объявление явной специализации шаблона для const char* typedef const char *PCC; template<> PCC max< PCC >( PCC s1, PCC s2 );
// --------- File1.C ------- #include <iostream> #include "max.h" void another();
int main() { // специализация // const char* max< const char* >( const char*, const char* ); const char *p = max( "hello", "world" );
// .... }
Упражнение 10.10
Определите шаблон функции count() для подсчета числа появлений некоторого значения в массиве. Напишите вызывающую программу. Последовательно передайте в ней массив значений типа double, int и сhar. Напишите специализированный экземпляр шаблона count() для обработки строк. 10.7. Перегрузка шаблонов функций А
Шаблон функции может быть перегружен. В следующем примере есть три перегруженных объявления для шаблона min():
// определение шаблона класса Array // (см. раздел 2.4) template <typename Type> class Array( /* ... */ }; // три объявления шаблона функции min() template <typename Type> Type min( const Array<Type>&, int ); // #1 template <typename Type> Type min( const Type*, int ); // #2 template <typename Type> Type min( Type, Type ); // #3
Следующее определение main() иллюстрирует, как могут вызываться три объявленных таким образом функции:
#include <cmath>
int main() { Array<int> iA(1024); // конкретизация класса int ia[1024];
// Type == int; min( const Array<int>&, int ) int ival0 = min( iA, 1024 );
// 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; }
Разумеется, тот факт, что три перегруженных шаблона функции успешно объявлены, не означает, что они могут быть также успешно вызваны. Такие шаблоны могут приводить к неоднозначности при вызове конкретизированного шаблона. Например, для следующего определения шаблона min5()
template <typename T> int min5( T, T ) { /* ... */ }
функция не конкретизируется по шаблону, если min5() вызывается с аргументами разных типов; при этом процесс вывода заканчивается с ошибкой, поскольку из фактических аргументов функции выводятся два разных типа для T.
int i; unsigned int ui;
// правильно: для T выведен тип int min5( 1024, i );
// вывод аргументов шаблона заканчивается с ошибкой: // для T можно вывести два разных типа min5 ( i, ui );
Для разрешения второго вызова можно было бы перегрузить min5(), допустив два различных типа аргументов:
template <typename T, typename U> int min5( T, U );
При следующем обращении производится конкретизация этого шаблона функции:
// правильно: int min5( int, usigned int ) min5( i, ui );
К сожалению, теперь стал неоднозначным предыдущий вызов:
// ошибка: неоднозначность: две возможных конкретизации // из min5( T, T ) и min5( T, U ) min5( 1024, i );
Второе объявление min5() допускает наличие у функции аргументов различных типов, но не требует этого. В нашем случае и T, и U типа int. Оба объявления шаблонов могут быть конкретизированы вызовом, в котором два аргумента функции имеют один и тот же тип. Единственный способ указать, какой шаблон более предпочтителен, устранив тем самым неоднозначность, – явно задать его аргументы. (О явном задании аргументов шаблона см. раздел 10.4.) Например:
// правильно: конкретизация из min5( T, U ) min5<int, int>( 1024, i );
Однако в этом случае мы можем обойтись без перегрузки шаблона функции. Поскольку шаблон min5(T,U) подходит для всех вызовов, для которых подходит min5(T,T), то одного объявления min5(T,U) вполне достаточно, а объявление min5(T,T) можно удалить. Мы уже говорили в главе 9, что, хотя перегрузка допускается, при проектировании таких функций надо быть внимательным и использовать ее только при необходимости. Те же соображения применимы и к определению перегруженных шаблонов. В некоторых ситуациях неоднозначности при вызове не возникает, хотя по шаблону можно конкретизировать две разных функции. Если имеются следующие два шаблона для функции sum(), то предпочтение будет отдано первому даже тогда, когда конкретизированы могут быть оба:
template <typename Type> Type sum( Type*, int ); template <typename Type> Type sum( Type, int );
int ia[1024]; // Type == int ; sum<int>( int*, int ); или // Type == int*; sum<int*>( int*, int ); ?? int ival1 = sum<int>( ia, 1024 );
Как это ни удивительно, такой вызов не приводит к неоднозначности. Шаблон конкретизируется из первого определения, так как выбирается наиболее специализированное определение. Поэтому для аргумента Type принимается int, а не int*. Для того чтобы один шаблон был более специализирован, чем другой, оба они должны иметь одни и те же имя и число параметров, а для параметров разных типов, как, скажем, T* и T в предыдущем примере, параметр в одном шаблоне должен быть способен принять более широкое множество фактических аргументов, чем соответствующий параметр в другом. Например, для шаблона sum(Type*, int) вместо первого формального параметра функции разрешается подставлять только фактические аргументы типа "указатель”. В то же время в шаблоне sum(Type, int) первому формальному параметру могут соответствовать фактические аргументы любого типа. Первый шаблон sum(Type*, int) допускает более узкое множество аргументов, чем второй, т.е. он более специализирован, а следовательно, он и конкретизируется при вызове функции. 10.8. Разрешение перегрузки при конкретизации A
В предыдущем разделе мы видели, что шаблон функции может быть перегружен. Кроме того, допускается использование одного и того же имени для шаблона и обычной функции:
// шаблон функции template <class Type> Type sum( Type, int ) { /* ... */ } // обычная функция (не шаблон) double sum( double, double );
Когда программа обращается к sum(), вызов разрешается либо в пользу конкретизированного экземпляра шаблона, либо в пользу обычной функции – это зависит от того, какая функция лучше соответствует фактическим аргументам. (Для решения такой проблемы применяется процесс разрешения перегрузки, описанный в главе 9.) Рассмотрим следующий пример:
void calc( int ii, double dd ) { // что будет вызвано: конкретизированный экземпляр шаблона // или обычная функция? sum( dd, ii ); }
Будет ли при обращении к 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, т.е. необходимо стандартное преобразование между целым и плавающим типами.
Если рассматривать только первый аргумент, то обе функции одинаково хороши. Однако для второго аргумента конкретизированная из шаблона функция лучше. Поэтому наиболее подходящей (лучшей из устоявших) считается функция sum(double, int). Функция, конкретизированная из шаблона, включается в множество кандидатов только тогда, когда процесс вывода аргументов завершается успешно. Неудачное завершение в данном случае не является ошибкой, но кандидатом функция считаться не будет. Предположим, что шаблон функции sum() объявлен следующим образом:
// шаблон функции template <class T> int sum( T*, int ) { ... }
Для описанного вызова функции вывод аргументов шаблона будет неудачным, так как фактический аргумент типа double не может соответствовать формальному параметру типа T*. Поскольку для данного вызова и данного шаблона конкретизировать функцию невозможно, в множество кандидатов ничего не добавляется, т.е. единственным его элементом останется обычная функция sum(double, double). Именно она вызывается при обращении, и ее второй фактический аргумент приводится к типу double. А если вывод аргументов шаблона завершается удачно, но для них есть явная специализация? Тогда именно она, а не функция, конкретизированная из обобщенного шаблона, попадает в множество кандидатов. Например:
// определение шаблона функции template <class Type> Type sum( Type, int ) { /* ... */ }
// явная специализация для Type == double template<> double sum<double>( double,int );
// обычная функция double sum( double, double );
void manip( int ii, double dd ) { // вызывается явная специализация шаблона sum<double>() sum( dd, ii ); }
При обращении к sum() внутри manip() в процессе вывода аргументов шаблона обнаруживается, что функция sum(double,int), конкретизированная из обобщенного шаблона, должна быть добавлена к множеству кандидатов. Но для нее имеется явная специализация, которая и становится кандидатом. На более поздних стадиях анализа выясняется, что эта специализация дает наилучшее соответствие фактическим аргументам вызова, так что разрешение перегрузки завершается в ее пользу. Явные специализации шаблона не включаются в множество кандидатов автоматически. Лишь в том случае, когда вывод аргументов завершается успешно, компилятор будет рассматривать явные специализации данного шаблона:
// определение шаблона функции template <class Type> Type min( Type, Type ) { /* ... */ }
// явная специализация для Type == double template<> double min<double>( double, double );
void manip( int ii, double dd ) { // ошибка: вывод аргументов шаблона неудачен, // нет функций-кандидатов для данного вызова min( dd, ii ); }
Шаблон функции min() специализирован для аргумента double. Однако эта специализация не попадает в множество функций-кандидатов. Процесс вывода для вызова min() завершился неудачно, поскольку аргументы шаблона, выведенные для Type на основе разных фактических аргументов функции, оказались различными: для первого аргумента выводится тип double, а для второго – int. Поскольку вывести аргументы не удалось, в множество кандидатов никакая функция не добавляется, и специализация min(double, double) игнорируется. Так как других функций-кандидатов нет, вызов считается ошибочным. Как отмечалось в разделе 10.6, тип возвращаемого значения и список формальных параметров обычной функции может точно соответствовать аналогичным атрибутам функции, конкретизированной из шаблона. В следующем примере min(int,int) – это обычная функция, а не специализация шаблона min(), поскольку, как вы, вероятно, помните, объявление специализации должно начинаться с template<>:
// объявление шаблона функции template <class T> T min( T, T );
// обычная функция min(int,int) int min( int, int ) { }
Вызов может точно соответствовать как обычной функции, так и функции, конкретизированной из шаблона. В следующем примере оба аргумента в min(ai[0],99) имеют тип int. Для этого вызова есть две устоявших функции: обычная min(int,int) и конкретизированная из шаблона функция с тем же типом возвращаемого значения и списком параметров:
int ai[4] = { 22, 33, 44, 55 }; int main() { // вызывается обычная функция min( int, int ) min( ai[0], 99 ); }
Однако такой вызов не является неоднозначным. Обычной функции, если она существует, всегда отдается предпочтение, поскольку она реализована явно, так что перегрузка разрешается в пользу обычной функции min(int,int). Если перегрузка разрешилась таким образом, то изменений уже не будет: если позже обнаружится, что в программе нет определения этой функции, компилятор не станет конкретизировать ее тело из шаблона. Вместо этого на этапе сборки мы получим ошибку. В следующем примере программа вызывает, но не определяет обычную функцию min(int,int), и редактор связей выдает сообщение об ошибке:
// шаблон функции template <class T> T min( T, T ) { ... }
// это обычная функция, не определенная в программе int min( int, int );
int ai[4] = { 22, 33, 44, 55 }; int main() { // ошибка сборки: min( int, int ) не определена min( ai[0], 99 ); }
Зачем определять обычную функцию, если ее тип возвращаемого значения и список параметров соответствуют функции, конкретизированной из шаблона? Вспомните, что при вызове конкретизированной функции к ее фактическим аргументам в ходе вывода аргументов шаблона можно применять только ограниченное множество преобразований. Если же объявлена обычная функция, то для приведения типов аргументов допустимы любые трансформации, так как типы формальных параметров обычной функции фиксированы. Рассмотрим пример, показывающий, зачем может потребоваться объявить обычную функцию. Предположим, что мы хотим определить специализацию шаблона функции min<int>(int,int). Нужно, чтобы именно эта функция вызывалась при обращении к min() с аргументами любых целых типов, пусть даже неодинаковых. Из-за ограничений, наложенных на преобразования типов, при передаче фактических аргументов разных типов функция min<int>(int,int) не будет конкретизирована из шаблона. Мы могли бы заставить компилятор выполнить конкретизацию, явно задав аргументы шаблона, однако решение, при котором не требуется модифицировать каждый вызов, предпочтительнее. Определив обычную функцию, мы добьемся того, что программа будет вызывать специальную версию min(int,int) для любых фактических аргументов целых типов без явного указания аргументов шаблона:
// определение шаблона функции template <class Type> Type min( Type t1, Type t2 ) { ... }
int ai[4] = { 22, 33, 44, 55 }; short ss = 88;
void call_instantiation() { // ошибка: для этого вызова нет функции-кандидата min( ai[0], ss ); }
// обычная функция int min( int a1, int a2 ) { min<int>( a1, a2 ); }
int main() { call_instantiation() { // вызывается обычная функция min( ai[0], ss ); }
Для вызова min(ai[0],ss) из call_instantiation нет ни одной функции-кандидата. Попытка сгенерировать ее из шаблона min() провалится, поскольку для аргумента шаблона Type из фактических аргументов функции выводятся два разных значения. Следовательно, такой вызов ошибочен. Однако при обращении к min(ai[0],ss) внутри main() видимо объявление обычной функции min(int, int). Тип первого фактического аргумента этой функции точно соответствует типу формального параметра, а второй аргумент может быть преобразован в тип формального параметра с помощью расширения типа. Поскольку для второго вызова устояла только данная функция, то она и вызывается. Разобравшись с разрешением перегрузки функций, конкретизированных из шаблонов, специализацией шаблонов функций и обычных функций с тем же именем, подытожим все, что мы об этом рассказали:
1. Построить множество функций-кандидатов. Рассматриваются шаблоны функций с тем же именем, что и вызванная. Если аргументы шаблона выведены из фактических аргументов функции успешно, то в множество функций-кандидатов включается либо конкретизированный шаблон, либо специализация шаблона для выведенных аргументов, если она существует. 2. Построить множество устоявших функций (см. раздел 9.3). В множестве функций-кандидатов остаются только функции, которые можно вызвать с данными фактическими аргументами. 3. Ранжировать преобразования типов (см. раздел 9.3). 1. Если есть только одна функция, вызвать именно ее. 2. Если вызов неоднозначен, удалить из множества устоявших функции, конкретизированные из шаблонов. 4. Разрешить перегрузку, рассматривая среди всех устоявших только обычные функции (см. раздел 9.3). 1. Если есть только одна функция, вызвать именно ее. 2. В противном случае вызов неоднозначен.
Проиллюстрируем эти шаги на примере. Предположим, есть два объявления – шаблона функции и обычной функции. Оба принимают аргументы типа double:
template <class Type> Type max( Type, Type ) { ... }
// обычная функция double max( double, double );
А вот три вызова max(). Можете ли вы сказать, какая функция будет вызвана в каждом случае?
int main() { int ival; double dval; float fd; // ival, dval и fd присваиваются значения max( 0, ival ); max( 0.25, dval ); max( 0, fd ); }
Рассмотрим последовательно все три вызова:
1. max(0,ival). Оба аргумента имеют тип int. Для вызова есть два кандидата: конкретизированная из шаблона функция max(int, int) и обычная функция max(double, double). Конкретизированная функция точно соответствует фактическим аргументам, поэтому она и вызывается; 2. max(0.25,double). Оба аргумента имеют тип double. Для вызова есть два кандидата: конкретизированная из шаблона max(double, double) и обычная max(double, double). Вызов неоднозначен, поскольку точно соответствует обеим функциям. Правило 3b говорит, что в таком случае выбирается обычная функция;. 3. max(0,fd). Аргументы имеют тип int и float соответственно. Для вызова существует только один кандидат: обычная функция max(double, double). Вывод аргументов шаблона заканчивается неудачей, так как значения типа Type, выведенные из разных фактических аргументов функции, различны. Поэтому в множество кандидатов конкретизированная из шаблона функция не попадает. Обычная же функция устояла, поскольку существуют преобразования типов фактических аргументов в типы формальных параметров; она и выбирается. Если бы обычная функция не была объявлена, вызов закончился бы ошибкой.
А если бы мы определили еще одну обычную функцию для max()? Например:
template <class T> T max( T, T ) { ... }
// две обычные функции char max( char, char ); double max( double, double ); Будет ли в таком случае третий вызов разрешен по-другому? Да.
int main() { float fd; // в пользу какой функции разрешается вызов? max( 0, fd ); }
Правило 3b говорит, что, поскольку вызов неоднозначен, следует рассматривать только обычные функции. Ни одна из них не считается наилучшей из устоявших, так как преобразования типов фактических аргументов одинаково плохи: в обоих случаях для установления соответствия требуется стандартная трансформация. Таким образом, вызов неоднозначен, и компилятор сообщает об ошибке. Упражнение 10.11
Вернемся к представленному ранее примеру:
template <class Type> Type max( Type, Type ) { ... }
double max( double, double ); int main() { int ival; double dval; float fd; max( 0, ival ); max( 0.25, dval ); max( 0, fd ); }
Добавим в множество объявлений в глобальной области видимости следующую специализацию шаблона функции:
template <> char max<char>* char, char ) { ... }
Составьте список кандидатов и устоявших функций для каждого вызова max() внутри main(). Предположим, что в main() добавлен следующий вызов:
int main() { // ... max( 0, 'j' ); }
В пользу какой функции он будет разрешен? Почему? Упражнение 10.12
Предположим, что есть следующее множество определений и специализаций шаблонов, а также объявления переменных и функций:
int i; unsigned int ui; char str[24]; int ia[24];
template <class T> T calc( T*, int ); template <class T> T calc( T, T ); template<> chat calc( char*. int ); double calc( double, double );
Выясните, какая функция или конкретизированный шаблон вызывается в каждом из показанных ниже случаев. Для каждого вызова перечислите функции-кандидаты и устоявшие функции; объясните, какая из устоявших функций будет наилучшей.
(a) cslc( str, 24 ); (d) calc( i, ui ); (b) calc( is, 24 ); (e) calc( ia, ui ); (c) calc( ia[0], 1 ); (f) calc( &i, i );
10.9. Разрешение имен в определениях шаблонов А
Внутри определения шаблона смысл некоторых конструкций может различаться в зависимости от конкретизации, тогда как смысл других всегда остается неизменным. Главную роль играет наличие в конструкции формального параметра шаблона:
template <typename Type> Type min( Type* array, int size ) { Type min_val = array[0]; for (int i = 1; i < size; ++i) if ( array[i] < min_val ) min_val = array[i]; print( "Minimum value found: "); print( min_val ); return min_val; }
В функции min() типы переменных array и min_val зависят от фактического типа, которым будет заменен Type при конкретизации шаблона, тогда как тип переменной size останется int при любом типе параметра шаблона. Следовательно, типы array и min_val в разных конкретизациях различны. Поэтому мы говорим, что типы этих переменных зависят от параметра шаблона, тогда как тип size от него не зависит.
Так как тип min_val неизвестен, то неизвестна и операция, которая будет использоваться при появлении min_val в выражении. Например, какая функция print() будет вызвана при обращении print(min_val)? С типом аргумента int? Или float? Будет ли вызов ошибочным, поскольку не существует функции, которая может быть вызвана с аргументом того же типа, что и min_val? Принимая все это во внимание, мы говорим, что и вызов print(min_val) зависит от параметра шаблона. Такие вопросы не возникают для тех конструкций внутри min(), которые не зависят от параметров шаблона. Например, всегда известно, какая функция должна быть вызвана для print( "Minimum value found: "). Это функция печати строк символов. В данном случае print() остается одной и той же при любой конкретизации шаблона, то есть не зависит от его параметров.
В главе 7 мы видели, что в C++ функция должна быть объявлена до ее вызова. Нужно ли объявлять функцию, вызываемую внутри шаблона, до того, как компилятор увидит его определение? Должны ли мы объявить функцию print() в предыдущем примере до определения шаблона min()? Ответ зависит от особенностей имени, на которое мы ссылаемся. Конструкцию, не зависящую от параметров шаблона, следует объявить перед ее использованием в шаблоне. Представленное выше определение шаблона функции min() некорректно. Поскольку вызов
print( "Minimum value found: ");
не зависит от параметров шаблона, то функция print() для печати строк символов должна быть объявлена до использования. Чтобы исправить эту ошибку, можно поместить объявление print() перед определением min():
// ---- primer.h ----
// это объявление необходимо: // внутри min() вызывается print( const char * ) void print( const char * ); template <typename Type> Type min( Type* array, int size ) { // ... print( "Minimum value found: "); print( min_val );
return min_val;
}
С другой стороны, объявление функции print(), используемой для печати min_val, пока не нужно, так как еще неизвестно, какую конкретно функцию надо искать. Мы не знаем, какая функция print() будет вызвана при обращении print(min_val), пока тип min_val не станет известным. Когда же должна быть объявлена функция print(), вызываемая при обращении print(min_val)? До конкретизации шаблона. Например:
#include <primer.h> void print( int ); int ai[4] = {12, 8, 73, 45 };
int main() { int size = sizeof(ai) / sizeof(int); // конкретизируется min( int*, int ) min( &ai[0], size ); }
main() вызывает конкретизированную из шаблона функцию min(int*,int). В этой реализации Type заменено int, и тип переменной min_val, следовательно, равен int. Поэтому при обращении print(min_val) вызывается функция с аргументом типа int. Именно тогда, когда конкретизируется min(int*,int), становится известно, что при втором вызове аргумент print() имеет тип int. В этот момент такая функция должна быть видима. Если бы функция print(int) не была объявлена до конкретизации min(int*,int), то компилятор выдал бы сообщение об ошибке. Поэтому разрешение имен в определении шаблона происходит в два этапа. Сначала разрешаются имена, не зависящие от его параметров, а затем, при конкретизации, – имена, зависящие от параметров.
Но зачем нужны два шага? Почему бы, например, не разрешать все имена при конкретизации? Если вы проектируете шаблон функции, то, вероятно, хотели бы сохранить контроль над тем, когда разрешаются имена в его определении. Предположим, что шаблон min() – это часть библиотеки, в которой определены и другие шаблоны и функции. Желательно, чтобы реализации min() по возможности использовали другие компоненты нашей же библиотеки. В предыдущем примере интерфейс библиотеки определен в заголовочном файле <primer.h>. Как объявление функции print(const char*), так и определение функции min() являются частями интерфейса. Мы хотим, чтобы конкретизации шаблона min() пользовались функцией print() из нашей библиотеки. Первый этап разрешения имени это гарантирует. Если имя, использованное в определении шаблона, не зависит от его параметров, то оно обязательно будет относиться к компоненту внутри библиотеки, т.е. к тому объявлению, которое включено в один пакет с этим определением в заголовочном файле <primer.h>. На самом деле автор шаблона должен позаботиться о том, чтобы были объявлены все имена, использованные в определениях и не зависящие от параметров. Если этого нет, то определение шаблона вызовет ошибку. При конкретизации шаблона компилятор ее не исправляет:
// ---- primer.h ---- template <typename Type> Type min( Type* array, int size ) { Type min_val = array[0]; // ... // ошибка: функция print( const char* ) не найдена print( "Minimum value found: " );
// правильно: зависит от параметра шаблона print( min_val ); // ... }
// ---- user.C ----
#include <primer.h> // это объявление print( const char* ) игнорируется void print( const char* ); void print( int );
int ai[4] = {12, 8, 73, 45 };
int main() { int size = sizeof(ai) / sizeof(int); // конкретизируется min( int*, int ) min( &ai[0], size ); }
Объявление функции print( const char* ) в файле user.C невидимо в том месте, где появляется определение шаблона. Однако оно видимо там, где конкретизируется шаблон min(int*,int), но это объявление не рассматривается при компиляции вызова print("Minimum value found: "), так как последний не зависит от параметров шаблона. Если некоторая конструкция в определении шаблона не зависит от его параметров, то имена разрешаются в контексте самого определения, и результат разрешения в дальнейшем не пересматривается. Поэтому на программиста возлагается ответственность за то, чтобы объявления имен, встречающихся в определении, были включены в интерфейс библиотеки вместе с шаблоном. А теперь предположим, что библиотека была написана кем-то другим, а мы ее пользователи, которым доступен интерфейс, определенный в заголовочном файле <primer.h>. Иногда нужно, чтобы объекты и функции, определенные в нашей программе, учитывались при конкретизации шаблона из библиотеки. Допустим, мы определили в своей программе класс SmallInt и хотели бы конкретизировать функцию min() из библиотеки <primer.h> для получения минимального значения в массиве объектов типа SmallInt. При конкретизации шаблона min() для массива объектов типа SmallInt вместо аргумента шаблона Type подставляется тип SmallInt. Следовательно, min_val в конкретизированной функции min() имеет тот же тип. Тогда как разрешится вызов функции print(min_val)?
// ---- user.h ---- class SmallInt { /* ... */ } void print( const SmallInt & );
// ---- user.C ---- #include <primer.h> #include "user.h" SmallInt asi[4];
int main() { // задать значения элементов массива asi // конкретизируется min( SmallInt*, int ) // int size = sizeof(asi) / sizeof(SmallInt); min( &asi[0], size ); }
Это нормально: мы хотим, чтобы учитывалась именно наша функция print(const SmallInt &). Рассмотрения функций, определенных в библиотеке <primer.h>, недостаточно. Второй шаг разрешения имени гарантирует, что если имя, использованное в определении, зависит от параметров шаблона, то принимаются во внимание имена, объявленные в контексте конкретизации. Поэтому можно быть уверенным, что функции, умеющие манипулировать объектами типа SmallInt, попадут в поле зрения компилятора при анализе шаблона, которому в качестве аргумента передан тип SmallInt. Место в программе, где происходит конкретизация шаблона, называется точкой конкретизации. Знание этой точки важно потому, что она определяет, какие объявления учитывает компилятор для имен, зависящих от параметров шаблона. Такая точка всегда находится в области видимости пространства имен и следует за функцией, внутри которой произошла конкретизация. Например, точка конкретизации min(SmallInt*,int) расположена сразу после функции main() в области видимости пространства имен:
// ... int main() { // ... // использование min(SmallInt*,int) min( &asi[0], size ); } // точка конкретизации min(SmallInt*,int) // как будто объявление конкретизированной функции выглядит так: SmallInt min( SmallInt* array, int size ) { /* ... */ }
Но что, если конкретизация шаблона случается в одном исходном файле несколько раз? Где тогда будет точка конкретизации? Вы можете спросить: "А какая, собственно, разница?” В нашем примере для SmallInt разница есть, поскольку объявление функции print(const SmallInt &) должно появиться перед точкой конкретизации min(SmallInt*,int):
#include <primer.h> void another();
SmallInt asi[4];
int main() { // задать значения элементов массива asi int size = sizeof(asi) / sizeof(SmallInt); min( &asi[0], size ); another(); // ... } // точка конкретизации здесь?
void another() { int size = sizeof(asi) / sizeof(SmallInt); min( &asi[0], size ); } // или здесь?
В действительности точка конкретизации находится после определения каждой функции, в которой используется конкретизированный экземпляр. Компилятор может выбрать любую из этих точек, чтобы конкретизировать в ней шаблон. Отсюда следует, что при организации кода программы надо быть внимательным и помещать все объявления, необходимые для разрешения имен, зависящих от параметров некоторого шаблона, перед первой точкой. Поэтому разумно поместить их в заголовочный файл, который включается перед любой возможной конкретизацией шаблона:
#include <primer.h> // user.h содержит объявления, необходимые при конкретизации #include "user.h" void another();
SmallInt asi[4];
int main() { // ... } // первая точка конкретизации min(SmallInt*,int)
void another() { // ... } // вторая точка конкретизации min(SmallInt*,int)
А если конкретизация шаблона происходит в нескольких файлах? Например, что будет, если функция another() находится в другом файле, нежели main()? Тогда точка конкретизации есть в каждом файле, где используется конкретизированная из шаблона функция. Компилятор свободен в выборе любой из них, так что нам снова придется проявить аккуратность и включить файл "user.h" во все исходные файлы, где используются конкретизированные функции. Тем самым гарантируется, что реализация min(SmallInt*,int) будет ссылаться именно на нашу функцию print(const SmallInt &) вне зависимости от того, какую из точек конкретизации выберет компилятор.
|
Категория: С++ | Добавил: r2d2 (29.09.2011)
|
Просмотров: 1351
| Рейтинг: 0.0/0 |
Добавлять комментарии могут только зарегистрированные пользователи. [ Регистрация | Вход ]
|
|
Born in Ussr |
|
|
Залогиниться |
|
|
Турниры |
|
|
|