<< Пред.           стр. 45 (из 121)           След. >>

Список литературы по разделу

 }
 
 // using-объявление
 using libs_R_us::max;
 
 void func()
 {
  char c1, c2;
  max( c1, c2 ); // вызывается libs_R_us::max( int, int )
 }
 Аргументы в вызове функции max() имеют тип char. Последовательность преобразований аргументов при вызове функции libs_R_us::max(int,int) следующая:
 1a. Так как аргументы передаются по значению, то с помощью преобразования l-значения в r-значение извлекаются значения аргументов c1 и c2.
 2a. С помощью расширения типа аргументы трансформируются из char в int.
 Последовательность преобразований аргументов при вызове функции libs_R_us::max(double,double) следующая:
 1b. С помощью преобразования l-значения в r-значение извлекаются значения аргументов c1 и c2.
 2b. Стандартное преобразование между целым и плавающим типом приводит аргументы от типа char к типу double.
 Ранг первой последовательности – расширение типа (самое худшее из примененных изменений), тогда как ранг второй – стандартное преобразование. Так как расширение типа лучше, чем преобразование, то в качестве наилучшей из устоявших для данного вызова выбирается функция libs_R_us::max(int,int).
 Если ранжирование последовательностей преобразований аргументов не может выявить единственной устоявшей функции, то вызов считается неоднозначным. В данном примере для обоих вызовов calc() требуется такая последовательность:
 Преобразование l-значения в r-значение для извлечения значений аргументов i и j.
 Стандартное преобразование для приведения типов фактических аргументов к типам соответствующих формальных параметров.
 Поскольку нельзя сказать, какая из этих последовательностей лучше другой, вызов неоднозначен:
 int i, j;
 extern long calc( long, long );
 extern double calc( double, double );
 
 void jj() {
 
  // ошибка: неоднозначность, нет наилучшего соответствия
  calc( i, j );
 }
 Преобразование спецификаторов (добавление спецификатора const или volatile к типу, который адресует указатель) имеет ранг точного соответствия. Однако, если две последовательности трансформаций отличаются только тем, что в конце одной из них есть дополнительное преобразование спецификаторов, то последовательность без него считается лучше. Например:
 void reset( int * );
 void reset( const int * );
 
 int* pi;
 
 int main() {
  reset( pi ); // без преобразования спецификаторов лучше:
  // выбирается reset( int * )
  return 0;
 }
 Последовательность стандартных преобразований, примененная к фактическому аргументу для первой функции-кандидата reset(int*), – это точное соответствие, требуется лишь переход от l-значения к r-значению, чтобы извлечь значение аргумента. Для второй функции-кандидата reset(const int *) также применяется трансформация l-значения в r-значение, но за ней следует еще и преобразование спецификаторов для приведения результирующего значения от типа “указатель на int” к типу “указатель на const int”. Обе последовательности представляют собой точное соответствие, но неоднозначности при этом не возникает. Так как вторая последовательность отличается от первой наличием трансформации спецификаторов в конце, то последовательность без такого преобразования считается лучшей. Поэтому наилучшей из устоявших функций будет reset(int*).
 Вот еще пример, в котором приведение спецификаторов влияет на то, какая последовательность будет выбрана:
 int extract( void * );
 int extract( const void * );
 
 int* pi;
 
 int main() {
  extract( pi ); // выбирается extract( void * )
  return 0;
 }
 Здесь для вызова есть две устоявших функции: extract(void*) и extract(const void*). Последовательность преобразований для функции extract(void*) состоит из трансформации l-значения в r-значение для извлечения значения аргумента, сопровождаемого стандартным преобразованием указателя: из указателя на int в указатель на void. Для функции extract(const void*) такая последовательность отличается от первой дополнительным преобразованием спецификаторов для приведения типа результата от указателя на void к указателю на const void. Поскольку последовательности различаются лишь этой трансформацией, то первая выбирается как более подходящая и, следовательно, наилучшей из устоявших будет функция extract(const void*).
 Спецификаторы const и volatile влияют также на ранжирование инициализации параметров-ссылок. Если две такие инициализации отличаются только добавлением спецификатора const и volatile, то инициализация без дополнительной спецификации считается лучшей при разрешении перегрузки:
 #include
 void manip( vector & );
 void manip( const vector & );
 
 vector f();
 extern vector vec;
 
 int main() {
  manip( vec ); // выбирается manip( vector & )
  manip( f() ); // выбирается manip( const vector & )
  return 0;
 }
 В первом вызове инициализация ссылок для вызова любой функции является точным соответствием. Но этот вызов все же не будет неоднозначным. Так как обе инициализации одинаковы во всем, кроме наличия дополнительной спецификации const во втором случае, то инициализация без такой спецификации считается лучше, поэтому перегрузка будет разрешена в пользу устоявшей функции manip(vector&).
 Для второго вызова существует только одна устоявшая функция manip(const vector&). Поскольку фактический аргумент является временной переменной, содержащей результат, возвращенный f(), то такой аргумент представляет собой r-значение, которое нельзя использовать для инициализации неконстантного формального параметра-ссылки функции manip(vector&). Поэтому наилучшей является единственная устоявшая manip(const vector&).
 Разумеется, у функций может быть несколько фактических аргументов. Выбор наилучшей из устоявших должен производиться с учетом ранжирования последовательностей преобразований всех аргументов. Рассмотрим пример:
 extern int ff( char*, int );
 extern int ff( int, int );
 
 int main() {
 
  ff( 0, 'a' ); // ff( int, int )
  return 0;
 }
 Функция ff(), принимающая два аргумента типа int, выбирается в качестве наилучшей из устоявших по следующим причинам:
 ее первый аргумент лучше. 0 дает точное соответствие с формальным параметром типа int, тогда как для установления соответствия с параметром типа char * требуется стандартное преобразование указателя;
 ее второй аргумент имеет тот же ранг. К аргументу 'a' типа char для установления соответствия со вторым формальным параметром любой из двух функций должна быть применена последовательность преобразований, имеющая ранг расширения типа.
 Вот еще один пример:
 int compute( const int&, short );
 int compute( int&, double );
 
 extern int iobj;
 int main() {
  compute( iobj, 'c' ); // compute( int&, double )
  return 0;
 }
 Обе функции compute( const int&, short ) и compute( int&, double ) устояли. Вторая выбирается в качестве наилучшей по следующим причинам:
 ее первый аргумент лучше. Инициализация ссылки для первой устоявшей функции хуже потому, что она требует добавления спецификатора const, не нужного для второй функции;
 ее второй аргумент имеет тот же ранг. К аргументу 'c' типа char для установления соответствия со вторым формальным параметром любой из двух функций должна быть применена последовательность трансформаций, имеющая ранг стандартного преобразования.
 9.4.4. Аргументы со значениями по умолчанию
 Наличие аргументов со значениями по умолчанию способно расширить множество устоявших функций. Устоявшими являются функции, которые вызываются с данным списком фактических аргументов. Но такая функция может иметь больше формальных параметров, чем задано фактических аргументов, в том случае, когда для каждого неуказанного параметра есть некое значение по умолчанию:
 extern void ff( int );
 extern void ff( long, int = 0 );
 
 int main() {
  ff( 2L ); // соответствует ff( long, 0 );
 
  ff( 0, 0 ); // соответствует ff( long, int );
  ff( 0 ); // соответствует ff( int );
  ff( 3.14 ); // ошибка: неоднозначность
 }
 Для первого и третьего вызовов функция ff() является устоявшей, хотя передан всего один фактический аргумент. Это обусловлено следующими причинами:
 для второго формального параметра есть значение по умолчанию;
 первый параметр типа long точно соответствует фактическому аргументу в первом вызове и может быть приведен к типу аргумента в третьем вызове за счет последовательности, имеющей ранг стандартного преобразования.
 Последний вызов является неоднозначным, поскольку обе устоявших функции могут быть выбраны, если применить стандартное преобразование к первому аргументу. Функции ff(int) не отдается предпочтение только потому, что у нее один параметр.
 Упражнение 9.9
 Объясните, что происходит при разрешении перегрузки для вызова функции compute() внутри main(). Какие функции являются кандидатами? Какие из них устоят после первого шага? Какие последовательности преобразований надо применить к фактическому аргументу, чтобы он соответствовал формальному параметру для каждой устоявшей функции? Какая функция будет наилучшей из устоявших?
 namespace primerLib {
  void compute();
  void compute( const void * );
 }
 
 using primerLib::compute;
 void compute( int );
 void compute( double, double = 3.4 );
 void compute( char*, char* = 0 );
 
 int main() {
  compute( 0 );
  return 0;
 }
 Что будет, если using-объявление поместить внутрь main() перед вызовом compute()? Ответьте на те же вопросы.
 10
 10. Шаблоны функций
 В этой главе рассказывается, что такое шаблон функции, как его определять и использовать. Это довольно просто, и многие программисты применяют шаблоны, определенные в стандартной библиотеке, даже не понимая, с чем они работают. Только пользователи, хорошо знающие язык С++, самостоятельно определяют и применяют шаблоны функций так, как здесь описано. Поэтому материал данной главы следует рассматривать как переход к более сложным аспектам C++. Мы начнем с рассказа о том, что такое шаблон функции и как его определять, затем на простом примере проиллюстрируем использование шаблонов. Далее мы перейдем к темам, требующим больших знаний. Сначала посмотрим на усложненные примеры применения шаблонов, затем подробно остановимся на выведении (deduction) их аргументов и покажем, как их можно задавать при конкретизации (instantiation) шаблона функции. После этого мы посмотрим, каким образом компилятор конкретизирует шаблоны и какие требования предъявляются в этой связи к организации наших программ, а также обсудим, как определить специализацию для такой конкретизации. Затем в данной главе будут изложены вопросы, представляющие интерес для проектировщиков шаблонов функций. Мы объясним, как можно перегружать шаблоны и как применительно к ним работает разрешение перегрузки. Мы также расскажем о разрешении имен в определениях шаблонов функций и покажем, как можно определять шаблоны в пространствах имен. Глава завершается развернутым примером.
 10.1. Определение шаблона функции
 Иногда может показаться, что сильно типизированный язык создает препятствия для реализации совсем простых функций. Например, хотя следующий алгоритм функции min() тривиален, сильная типизация требует, чтобы его разновидности были реализованы для всех типов, которые мы собираемся сравнивать:
 int min( int a, int b ) {
  return a < b ? a : b;
 }
 
 double min( double a, double b ) {
  return a < b ? a : b;
 }
 Заманчивую альтернативу явному определению каждого экземпляра функции min() представляет использование макросов, расширяемых препроцессором:
 #define min(a, b) ((a) < (b) ? (a) : (b))
 Но этот подход таит в себе потенциальную опасность. Определенный выше макрос правильно работает при простых обращениях к min(), например:
 min( 10, 20 );
 min( 10.0, 20.0 );
 но может преподнести сюрпризы в более сложных случаях: такой механизм ведет себя не как вызов функции, он лишь выполняет текстовую подстановку аргументов. В результате значения обоих аргументов оцениваются дважды: один раз при сравнении a и b, а второй – при вычислении возвращаемого макросом результата:
 #include
 
 #define min(a,b) ((a) < (b) ? (a) : (b))
 
 const int size = 10;
 int ia[size];
 
 int main() {
  int elem_cnt = 0;
  int *p = &ia[0];
 
  // подсчитать число элементов массива
  while ( min(p++,&ia[size]) != &ia[size] )
  ++elem_cnt;
 
  cout << "elem_cnt : " << elem_cnt
  << "\texpecting: " << size << endl;
  return 0;
 }
 На первый взгляд, эта программа подсчитывает количество элементов в массиве ia целых чисел. Но в этом случае макрос min() расширяется неверно, поскольку операция постинкремента применяется к аргументу-указателю дважды при каждой подстановке. В результате программа печатает строку, свидетельствующую о неправильных вычислениях:
 elem_cnt : 5 expecting: 10
 Шаблоны функций предоставляют в наше распоряжение механизм, с помощью которого можно сохранить семантику определений и вызовов функций (инкапсуляция фрагмента кода в одном месте программы и гарантированно однократное вычисление аргументов), не принося в жертву сильную типизацию языка C++, как в случае применения макросов.
 Шаблон дает алгоритм, используемый для автоматической генерации экземпляров функций с различными типами. Программист параметризует все или только некоторые типы в интерфейсе функции (т.е. типы формальных параметров и возвращаемого значения), оставляя ее тело неизменным. Функция хорошо подходит на роль шаблона, если ее реализация остается инвариантной на некотором множестве экземпляров, различающихся типами данных, как, скажем, в случае min().
 Так определяется шаблон функции min():
 template
  Type min2( Type a, Type b ) {
  return a < b ? a : b;
 }
 
 int main() {
  // правильно: min( int, int );
  min( 10, 20 );
 
  // правильно: min( double, double );
  min( 10.0, 20.0 );
  return 0;
 }
 Если вместо макроса препроцессора min() подставить в текст предыдущей программы этот шаблон, то результат будет правильным:
 elem_cnt : 10 expecting: 10
 (В стандартной библиотеке C++ есть шаблоны функций для многих часто используемых алгоритмов, например для min(). Эти алгоритмы описываются в главе 12. А в данной вводной главе мы приводим собственные упрощенные версии некоторых алгоритмов из стандартной библиотеки.)
 Как объявление, так и определение шаблона функции всегда должны начинаться с ключевого слова template, за которым следует список разделенных запятыми идентификаторов, заключенный в угловые скобки '<' и '>', – список параметров шаблона, обязательно непустой. У шаблона могут быть параметры-типы, представляющие некоторый тип, и параметры-константы, представляющие фиксированное константное выражение.
 Параметр-тип состоит из ключевого слова class или ключевого слова typename, за которым следует идентификатор. Эти слова всегда обозначают, что последующее имя относится к встроенному или определенному пользователем типу. Имя параметра шаблона выбирает программист. В приведенном примере мы использовали имя Type, но могли выбрать и любое другое:
 template
  Glorp min2( Glorp a, Glorp b ) {
  return a < b ? a : b;
 }
 При конкретизации (порождении конкретного экземпляра) шаблона вместо параметра-типа подставляется фактический встроенный или определенный пользователем тип. Любой из типов int, double, char*, vector или list является допустимым аргументом шаблона.
 Параметр-константа выглядит как обычное объявление. Он говорит о том, что вместо имени параметра должно быть подставлено значение константы из определения шаблона. Например, size – это параметр-константа, который представляет размер массива arr:
 template
  Type min( Type (&arr) [size] );
 Вслед за списком параметров шаблона идет объявление или определение функции. Если не обращать внимания на присутствие параметров в виде спецификаторов типа или констант, то определение шаблона функции выглядит точно так же, как и для обычных функций:
 template
 Type min( const 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;
 }
 В этом примере Type определяет тип значения, возвращаемого функцией min(), тип параметра r_array и тип локальной переменной min_val; size задает размер массива r_array. В ходе работы программы при использовании функции min() вместо Type могут быть подставлены любые встроенные и определенные пользователем типы, а вместо size – те или иные константные выражения. (Напомним, что работать с функцией можно двояко: вызвать ее или взять ее адрес).
 Процесс подстановки типов и значений вместо параметров называется конкретизацией шаблона. (Подробнее мы остановимся на этом в следующем разделе.)
 Список параметров нашей функции min() может показаться чересчур коротким. Как было сказано в разделе 7.3, когда параметром является массив, передается указатель на его первый элемент, первая же размерность фактического аргумента-массива внутри определения функции неизвестна. Чтобы обойти эту трудность, мы объявили первый параметр min() как ссылку на массив, а второй – как его размер. Недостаток подобного подхода в том, что при использовании шаблона с массивами одного и того же типа int, но разных размеров генерируются (или конкретизируются) различные экземпляры функции min().
 Имя параметра разрешено употреблять внутри объявления или определения шаблона. Параметр-тип служит спецификатором типа; его можно использовать точно так же, как спецификатор любого встроенного или пользовательского типа, например в объявлении переменных или в операциях приведения типов. Параметр-константа применяется как константное значение – там, где требуются константные выражения, например для задания размера в объявлении массива или в качестве начального значения элемента перечисления.
 // size определяет размер параметра-массива и инициализирует
 // переменную типа const int
 template
  Type min( const Type (&r_array)[size] )
 {
  const int loc_size = size;
  Type loc_array[loc_size];
  // ...
 }
 Если в глобальной области видимости объявлен объект, функция или тип с тем же именем, что у параметра шаблона, то глобальное имя оказывается скрытым. В следующем примере тип переменной tmp не double, а тот, что у параметра шаблона Type:
 typedef double Type;
 template
  Type min( Type a, Type b )
 {
  // tmp имеет тот же тип, что параметр шаблона Type, а не заданный
  // глобальным typedef
  Type tm = a < b ? a : b;
  return tmp;
 }
 Объект или тип, объявленные внутри определения шаблона функции, не могут иметь то же имя, что и какой-то из параметров:
 template
  Type min( Type a, Type b )
 {
  // ошибка: повторное объявление имени Type, совпадающего с именем
  // параметра шаблона
  typedef double Type;
  Type tmp = a < b ? a : b;
  return tmp;
 }
 Имя параметра-типа шаблона можно использовать для задания типа возвращаемого значения:
 // правильно: T1 представляет тип значения, возвращаемого min(),
 // а T2 и T3 – параметры-типы этой функции
 template
  T1 min( T2, T3 );
 В одном списке параметров некоторое имя разрешается употреблять только один раз. Например, следующее определение будет помечено как ошибка компиляции:
 // ошибка: неправильное повторное использование имени параметра Type
 template
  Type min( Type, Type );
 Однако одно и то же имя можно многократно применять внутри объявления или определения шаблона:
 // правильно: повторное использование имени Type внутри шаблона
 template
  Type min( Type, Type );
 template
  Type max( Type, Type );
 Имена параметров в объявлении и определении не обязаны совпадать. Так, все три объявления min() относятся к одному и тому же шаблону функции:
 // все три объявления min() относятся к одному и тому же шаблону функции
 
 // опережающие объявления шаблона
 template T min( T, T );
 template U min( U, U );
 
 // фактическое определение шаблона
 template
  Type min( Type a, Type b ) { /* ... */ }
 Количество появлений одного и того же параметра шаблона в списке параметров функции не ограничено. В следующем примере Type используется для представления двух разных параметров:
 #include
 // правильно: Type используется неоднократно в списке параметров шаблона
 template
  Type sum( const vector &, Type );
 Если шаблон функции имеет несколько параметров-типов, то каждому из них должно предшествовать ключевое слово class или typename:
 // правильно: ключевые слова typename и class могут перемежаться
 template
  T minus( T*, U );
 
 // ошибка: должно быть или
 //
 template
  T sum( T*, U );
 В списке параметров шаблона функции ключевые слова typename и class имеют одинаковый смысл и, следовательно, взаимозаменяемы. Любое из них может использоваться для объявления разных параметров-типов шаблона в одном и том же списке (как было продемонстрировано на примере шаблона функции minus()). Для обозначения параметра-типа более естественно, на первый взгляд, употреблять ключевое слово typename, а не class, ведь оно ясно указывает, что за ним следует имя типа. Однако это слово было добавлено в язык лишь недавно, как часть стандарта C++, поэтому в старых программах вы скорее всего встретите слово class. (Не говоря уже о том, что class короче, чем typename, а человек по природе своей ленив.)
 Ключевое слово typename упрощает разбор определений шаблонов. (Мы лишь кратко остановимся на том, зачем оно понадобилось. Желающим узнать об этом подробнее рекомендуем обратиться к книге Страуструпа “Design and Evolution of C++”.)
 При таком разборе компилятор должен отличать выражения-типы от тех, которые таковыми не являются; выявить это не всегда возможно. Например, если компилятор встречает в определении шаблона выражение Parm::name и если Parm – это параметр-тип, представляющий класс, то следует ли считать, что name представляет член-тип класса Parm?
 template
  Parm minus( Parm* array, U value )
 {
  Parm::name * p; // это объявление указателя или умножение?
  // На самом деле умножение
 }
 Компилятор не знает, является ли name типом, поскольку определение класса, представленного параметром Parm, недоступно до момента конкретизации шаблона. Чтобы такое определение шаблона можно было разобрать, пользователь должен подсказать компилятору, какие выражения включают типы. Для этого служит ключевое слово typename. Например, если мы хотим, чтобы выражение Parm::name в шаблоне функции minus() было именем типа и, следовательно, вся строка трактовалась как объявление указателя, то нужно модифицировать текст следующим образом:
 template
  Parm minus( Parm* array, U value )
 {
  typename Parm::name * p; // теперь это объявление указателя
 }
 Ключевое слово typename используется также в списке параметров шаблона для указания того, что параметр является типом.
 Шаблон функции можно объявлять как inline или extern – как и обычную функцию. Спецификатор помещается после списка параметров, а не перед словом template.
 // правильно: спецификатор после списка параметров
 template
  inline
  Type min( Type, Type );
 

<< Пред.           стр. 45 (из 121)           След. >>

Список литературы по разделу