<< Пред. стр. 46 (из 121) След. >>
// ошибка: спецификатор inline не на местеinline
template
Type min( Array
Упражнение 10.1
Определите, какие из данных определений шаблонов функций неправильны. Исправьте ошибки.
(a) template
void foo( T, U, V );
(b) template
T foo( int *T );
(c) template
T1 foo( T2, T3 );
(d) inline template
T foo( T, unsigned int* );
(e) template
void foo( myT, myT );
(f) template
foo( T, T );
(g) typedef char Ctype;
template
Ctype foo( Ctype a, Ctype b );
Упражнение 10.2
Какие из повторных объявлений шаблонов ошибочны? Почему?
(a) template
Type bar( Type, Type );
template
Type bar( Type, Type );
(b) template
void bar( T1, T2 );
template
void bar( C1, C2 );
Упражнение 10.3
Перепишите функцию putValues() из раздела 7.3.3 в виде шаблона. Параметризуйте его так, чтобы было два параметра шаблона (для типа элементов массива и для размера массива) и один параметр функции, являющийся ссылкой на массив. Напишите определение шаблона функции.
10.2. Конкретизация шаблона функции
Шаблон функции описывает, как следует строить конкретные функции, если задано множество фактических типов или значений. Процесс конструирования называется конкретизацией шаблона. Выполняется он неявно, как побочный эффект вызова или взятия адреса шаблона функции. Например, в следующей программе min() конкретизируется дважды: один раз для массива из пяти элементов типа int, а другой – для массива из шести элементов типа double:
// определение шаблона функции min()
// с параметром-типом Type и параметром-константой size
template
Type min( Type (&r_array)[size] )
{
Type min_val = r_array[0];
for ( int i = 1; i < size; ++i )
if ( r_array[i] < min_val )
min_val = r_array[i];
return min_val;
}
// size не задан -- ok
// size = число элементов в списке инициализации
int ia[] = { 10, 7, 14, 3, 25 };
double da[6] = { 10.2, 7.1, 14.5, 3.2, 25.0, 16.8 };
#include
int main()
{
// конкретизация min() для массива из 5 элементов типа int
// подставляется Type => int, size => 5
int i = min( ia );
if ( i != 3 )
cout << "??oops: integer min() failed\n";
else cout << "!!ok: integer min() worked\n";
// конкретизация min() для массива из 6 элементов типа double
// подставляется Type => double, size => 6
double d = min( da );
if ( d != 3.2 )
cout << "??oops: double min() failed\n";
else cout << "!!ok: double min() worked\n";
return 0;
}
Вызов
int i = min( ia );
приводит к конкретизации следующего экземпляра функции min(), в котором Type заменено на int, а size на 5:
int min( int (&r_array)[5] )
{
int min_val = r_array[0];
for ( int i = 1; i < 5; ++i )
if ( r_array[i] < min_val )
min_val = r_array[i];
return min_val;
}
Аналогично вызов
double d = min( da );
конкретизирует экземпляр min(), в котором Type заменено на double, а size на 6:
В качестве формальных параметров шаблона функции используются параметр-тип и параметр-константа. Для определения фактического типа и значения константы, которые надо подставить в шаблон, исследуются фактические аргументы, переданные при вызове функции. В нашем примере для идентификации аргументов шаблона при конкретизации используются тип ia (массив из пяти int) и da (массив из шести double). Процесс определения типов и значений аргументов шаблона по известным фактическим аргументам функции называется выведением (deduction) аргументов шаблона. (В следующем разделе мы расскажем об этом подробнее. А в разделе 10.4 речь пойдет о возможности явного задания аргументов.)
Шаблон конкретизируется либо при вызове, либо при взятии адреса функции. В следующем примере указатель pf инициализируется адресом конкретизированного экземпляра шаблона. Его аргументы определяются путем исследования типа параметра функции, на которую указывает pf:
template
Type min( Type (&p_array)[size] ) { /* ... */ }
// pf указывает на int min( int (&)[10] )
int (*pf)(int (&)[10]) = &min;
Тип pf – это указатель на функцию с параметром типа int(&)[10], который определяет тип аргумента шаблона Type и значение аргумента шаблона size при конкретизации min(). Аргумент шаблона Type будет иметь тип int, а значением аргумента шаблона size будет 10. Конкретизированная функция представляется как min(int(&)[10]), и указатель pf адресует именно ее.
Когда берется адрес шаблона функции, контекст должен быть таким, чтобы можно было однозначно определить типы и значения аргументов шаблона. Если сделать это не удается, компилятор выдает сообщение об ошибке:
template
Type min( Type (&r_array)[size] ) { /* ... */ }
typedef int (&rai)[10];
typedef double (&rad)[20];
void func( int (*)(rai) );
void func( double (*)(rad) );
int main() {
// ошибка: как конкретизировать min()?
func( &min );
}
Функция func() перегружена и тип ее параметра не позволяет однозначно определить ни аргумент шаблона Type, ни значение аргумента шаблона size. Результатом конкретизации вызова func() может быть любая из следующих функций:
min( int (*)(int(&)[10]) )
min( double (*)(double(&)[20]) )
Поскольку однозначно определить аргументы функции func() нельзя, взятие адреса конкретизированного шаблона в таком контексте приводит к ошибке компиляции.
Этого можно избежать, если использовать явное приведение типов для указания типа аргумента:
int main() {
// правильно: с помощью явного приведения указывается тип аргумента
func( static_cast< double(*)(rad) >(&min) );
}
Лучше, однако, применять явное задание аргументов шаблона, как будет показано в разделе 10.4.
10.3. Вывод аргументов шаблона А
При вызове шаблона функции типы и значения его аргументов определяются путем исследования типов фактических аргументов функции. Этот процесс называется выводом аргументов шаблона.
Параметром функции в шаблоне min() является ссылка на массив элементов типа Type:
template
Type min( Type (&r_array)[size] ) { /* ... */ }
Для сопоставления с формальным параметром функции фактический аргумент также должен быть l-значением, представляющим тип массива. Следующий вызов ошибочен, так как pval имеет тип int*, а не является l-значением типа “массив int”.
void f( int pval[9] ) {
// ошибка: Type (&)[] != int*
int jval = min( pval );
}
При выводе аргументов шаблона не принимается во внимание тип значения, возвращаемого конкретизированным шаблоном функции. Например, если вызов min() записан так:
double da[8] = { 10.3, 7.2, 14.0, 3.8, 25.7, 6.4, 5.5, 16.8 };
int i1 = min( da );
то конкретизированный экземпляр min() имеет параметр типа “указатель на массив из восьми double” и возвращает значение типа double. Перед инициализацией i1 это значение приводится к типу int. Однако тот факт, что результат вызова min() используется для инициализации объекта типа int, не влияет на вывод аргументов шаблона.
Чтобы процесс такого вывода завершился успешно, тип фактического аргумента функции не обязательно должен совпадать с типом соответствующего формального параметра. Допустимы три вида преобразований типа: трансформация l-значения, преобразование спецификаторов и приведение к базовому классу, конкретизированному из шаблона класса. Рассмотрим последовательно каждое из них.
Напомним, что трансформация l-значения – это либо преобразование l-значения в r-значение, либо преобразование массива в указатель, либо преобразование функции в указатель (все они рассматривались в разделе 9.3). Для иллюстрации влияния такой трансформации на вывод аргументов шаблона рассмотрим функцию min2() c одним параметром шаблона Type и двумя параметрами функции. Первый параметр min2() – это указатель на тип Type*. size теперь не является параметром шаблона, как в определении min(), вместо этого он стал параметром функции, а его значение должно быть явно передано при вызове:
template
// первый параметр имеет тип Type*
Type min2( 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];
return min_val;
}
min2() можно вызвать, передав в качестве первого аргумента массив из четырех int, как в следующем примере:
int ai[4] = { 12, 8, 73, 45 };
int main() {
int size = sizeof (ai) / sizeof (ai[0]);
// правильно: преобразование массива в указатель
min2( ai, size );
}
Фактический аргумент функции ai имеет тип “массив из четырех int” и не совпадает с типом соответствующего формального параметра Type*. Однако, поскольку преобразование массива в указатель допустимо, то аргумент ai приводится к типу int* еще до вывода аргумента шаблона Type, для которого затем выводится тип int, и шаблон конкретизирует функцию min2(int*, int).
Преобразование спецификаторов добавляет const или volatile к указателям (такие трансформации также рассматривались в разделе 9.3). Для иллюстрации влияния преобразования спецификаторов на вывод аргументов шаблона рассмотрим min3() с первым параметром функции типа const Type*:
template
// первый параметр имеет тип const Type*
Type min3( const Type* array, int size ) {
// ...
}
min3() можно вызвать, передав int* в качестве первого фактического аргумента, как в следующем примере:
int *pi = &ai;
// правильно: приведение спецификаторов к типу const int*
int i = min3( pi, 4 );
Фактический аргумент функции pi имеет тип “указатель на int” и не совпадает с типом формального параметра const Type*. Однако, поскольку преобразование спецификаторов допустимо, то он приводится к типу const int* еще до вывода аргумента шаблона Type, для которого затем выводится тип int, и шаблон конкретизирует функцию min3(const int*, int).
Теперь обратимся к преобразованию в базовый класс, конкретизированный из шаблона класса. Вывод аргументов шаблона можно выполнить, если тип формального параметра функции является таким шаблоном, а фактический аргумент – базовый класс, конкретизированный из него. Чтобы проиллюстрировать такое преобразование, рассмотрим новый шаблон функции min4() с параметром типа Array
template
class Array { /* ... */ }
template
Type min4( Array
{
Type min_val = array[0];
for ( int i = 1; i < array.size(); ++i )
if ( array[i] < min_val )
min_val = array[i];
return min_val;
}
min4() можно вызвать, передав в качестве первого аргумента ArrayRC
template
class ArrayRC : public Array
int main() {
ArrayRC
min4( ia_rc );
}
Фактический аргумент ia_rc имеет тип ArrayRC
В процессе вывода одного аргумента шаблона могут принимать участие несколько аргументов функции. Если параметр шаблона встречается в списке параметров функции более одного раза, то каждый выведенный тип должен точно соответствовать типу, выведенному для того же аргумента шаблона в первый раз:
template
unsigned int ui;
int main() {
// ошибка: нельзя конкретизировать min5( unsigned int, int )
// должно быть: min5( unsigned int, unsigned int ) или
// min5( int, int )
min5( ui, 1024 );
}
Оба фактических аргумента функции должны иметь один и тот же тип: либо int, либо unsigned int, поскольку в шаблоне они принадлежат к одному типу T. Аргумент шаблона T, выведенный из первого аргумента функции, – это int. Аргумент же шаблона T, выведенный из второго аргумента функции, – это unsigned int. Поскольку они оказались разными, процесс вывода завершается неудачей и при конкретизации шаблона выдается сообщение об ошибке. (Избежать ее можно, если явно задать аргументы шаблона при вызове функции min5(). В разделе 10.4 мы увидим, как это делается.)
Ограничение на допустимые типы преобразований относится только к тем фактическим параметрам функции, которые принимают участие в выводе аргументов шаблона. К остальным аргументам могут применяться любые трансформации. В следующем шаблоне функции sum() есть два формальных параметра. Фактический аргумент op1 для первого параметра участвует в выводе аргумента Type шаблона, а второй фактический аргумент op2 – нет.
template
Type sum( Type op1, int op2 ) { /* ... */ }
Поэтому при конкретизации шаблона функции sum() его можно подвергать любым трансформациям. (Преобразования типов, применимые к фактическим аргументам функции, описываются в разделе 9.3.) Например:
int ai[] = { ... };
double dd;
int main() {
// конкретизируется sum( int, int )
sum( ai[0], dd );
}
Тип второго фактического аргумента функции dd не соответствует типу формального параметра int. Но это не мешает конкретизировать шаблон функции sum(), поскольку тип второго аргумента фиксирован и не зависит от параметров шаблона. Для этого вызова конкретизируется функция sum(int,int). Аргумент dd приводится к типу int с помощью преобразования целого типа в тип с плавающей точкой.
Таким образом, общий алгоритм вывода аргументов шаблона можно сформулировать следующим образом:
По очереди исследуется каждый фактический аргумент функции, чтобы выяснить, присутствует ли в соответствующем формальном параметре какой-нибудь параметр шаблона.
Если параметр шаблона найден, то путем анализа типа фактического аргумента выводится соответствующий аргумент шаблона.
Тип фактического аргумента функции не обязан точно соответствовать типу формального параметра. Для приведения типов могут быть применены следующие преобразования:
трансформации l-значения
преобразования спецификаторов
приведение производного класса к базовому при условии, что формальный параметр функции имеет вид T
Если один и тот же параметр шаблона найден в нескольких формальных параметрах функций, то аргумент шаблона, выведенный по каждому из соответствующих фактических аргументов, должен быть одним и тем же.
Упражнение 10.4
Назовите два типа преобразований, которые можно применять к фактическим аргументам функций, участвующим в процессе вывода аргументов шаблона.
Упражнение 10.5
Пусть даны следующие определения шаблонов:
template
Type min3( const Type* array, int size ) { /* ... */ }
template
Type min5( Type p1, Type p2 ) { /* ... */ }
Какие из приведенных ниже вызовов ошибочны? Почему?
double dobj1, dobj2;
float fobj1, fobj2;
char cobj1, cobj2;
int ai[5] = { 511, 16, 8, 63, 34 };
(a) min5( cobj2, 'c' );
(b) min5( dobj1, fobj1 );
(c) min3( ai, cobj1 );
10.4. Явное задание аргументов шаблона A
В некоторых ситуациях автоматически вывести типы аргументов шаблона невозможно. Как мы видели на примере шаблона функции min5(), если процесс вывода дает два различных типа для одного и того же параметра шаблона, то компилятор сообщает об ошибке – неудачном выводе аргументов.
В таких ситуациях приходится подавлять механизм вывода и задавать аргументы явно, указывая их с помощью заключенного в угловые скобки списка разделенных запятыми значений, который следует после имени конкретизируемого шаблона функции. Например, если мы хотим задать тип unsigned int в качестве значения аргумента шаблона T в рассмотренном выше примере использования min5(), то нужно записать вызов конкретизируемого шаблона так:
// конкретизируется min5( unsigned int, unsigned int )
min5< unsigned int >( ui, 1024 );
В этом случае список аргументов шаблона
Обратите внимание, что при вызове функции min5() второй аргумент равен 1024, т.е. имеет тип int. Так как тип второго формального параметра функции при явном задании аргумента шаблона установлен в unsigned int, то второй фактический параметр функции приводится к типу unsigned int с помощью стандартного преобразования целых типов.
В предыдущем разделе мы говорили, что в процессе вывода аргументов шаблона к фактическим аргументам функции разрешается применять только ограниченное множество преобразований типов. Трансформация int в unsigned int в это множество не входит. Но если аргументы шаблона задаются явно, выполнять вывод типов не нужно, поскольку они уже зафиксированы. Следовательно, при явном задании аргументов шаблона для приведения типов фактических аргументов функции к типам формальных параметров можно применять любые стандартные преобразования.
Помимо разрешения любых преобразований фактических аргументов функции, явное задание аргументов шаблона помогает избежать и других проблем, встающих перед программистом. Рассмотрим следующую задачу. Мы хотим определить шаблон функции с именем sum() так, чтобы его конкретизация возвращала значения типа, достаточно большого для представления суммы двух значений любых двух типов, переданных в любом порядке. Как это сделать? Какой тип возвращаемого значения следует задать?
// каким должен быть тип возвращаемого значения: T или U
template
??? sum( T, U );
В нашем случае нельзя использовать ни тот, ни другой параметрический тип, иначе мы неизбежно допустим ошибку:
char ch; unsigned int ui;
// ни T, ни U нельзя использовать в качестве типа возвращаемого значения
sum( ch, ui ); // правильно: U sum( T, U );
sum( ui, ch ); // правильно: T sum( T, U );
Решение заключается в том, чтобы ввести в шаблон третий параметр для обозначения типа возвращаемого значения:
// T1 не появляется в списке параметров шаблона функции
template
T1 sum( T2, T3 );
Поскольку тип возвращаемого значения может отличаться от типов аргументов функции, T1 не упоминается в списке формальных параметров. Это потенциальная проблема, так как тип T1 не может быть выведен из фактических аргументов функции. Однако, если при конкретизации sum() мы зададим аргументы шаблона явно, то избегнем сообщения компилятора о невозможности вывести T1. Например:
typedef unsigned int ui_type;
ui_type calc( char ch, ui_type ui ) {
// ...
// ошибка: невозможно вывести T1
ui_type loc1 = sum( ch, ui );
// правильно: аргументы шаблона заданы явно
// T1 и T3 - это unsigned int, T2 - это char
ui_type loc2 = sum< ui_type, ui_type >( ch, ui );
}
Не хватает возможности явно задать T1, но не T2 и T3, поскольку их можно вывести из аргументов функции при вызове.
При явном задании аргументов шаблона необходимо перечислять только те, которые не могут быть выведены автоматически. Но, как и в случае аргументов функции со значениями по умолчанию, опускать можно исключительно “хвостовые”:
// правильно: T3 - это unsigned int
// T3 выведен из типа ui
ui_type loc3 = sum< ui_type, char >( ch, ui );
// правильно: T2 - это char, T3 - unsigned int
// T2 и T3 выведены из типа pf
ui_type (*pf)( char, ui_type ) = &sum< ui_type >;
// ошибка: опускать можно только “хвостовые” аргументы
ui_type loc4 = sum< ui_type, , ui_type >( ch, ui );
Встречаются ситуации, когда невозможно вывести аргументы шаблона в контексте, где конкретизируется шаблон функции; следовательно, необходимо их явно задать. Именно выявление таких ситуаций и необходимость решить проблему послужила причиной поддержки явного задания аргументов шаблона в стандартном C++.
В следующем примере берется адрес конкретизированной функции sum() и передается в качестве аргумента перегруженной функции manipulate(). Как мы показали в разделе 10.2, невозможно понять, как именно нужно конкретизировать sum(), если есть только списки параметров функций manipulate(). Имеется две разных функции sum(), и обе удовлетворяют условиям вызова. Следовательно, вызов manipulate() неоднозначен. Одним из способов разрешения такой неоднозначности является явное приведение типов. Однако лучше использовать явное задание аргументов шаблона: оно позволяет указать, как именно конкретизировать sum(), и, следовательно, выбрать нужный вариант перегруженной функции manipulate(). Например:
template
T1 sum( T2 op1, T3 op2 ) { /* ... */ }