Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

462

namespace libs_R_us { int max( int, int );

double max( double, double );

}

// 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() требуется такая последовательность:

1.Преобразование l-значения в r-значение для извлечения значений аргументов i и j.

2.Стандартное преобразование для приведения типов фактических аргументов к типам соответствующих формальных параметров.

Поскольку нельзя сказать, какая из этих последовательностей лучше другой, вызов неоднозначен:

С++ для начинающих

463

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;

последовательность будет выбрана:

}

С++ для начинающих

464

Здесь для вызова есть две устоявших функции: 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 <vector>

void manip( vector<int> & );

void manip( const vector<int> & );

vector<int> f(); extern vector<int> vec;

int main() {

// выбирается manip( vector<int> & )

manip( vec );

manip( f() );

// выбирается manip( const vector<int> & )

return 0;

 

считается лучшей при разрешении перегрузки:

}

В первом вызове инициализация ссылок для вызова любой функции является точным соответствием. Но этот вызов все же не будет неоднозначным. Так как обе инициализации одинаковы во всем, кроме наличия дополнительной спецификации const во втором случае, то инициализация без такой спецификации считается лучше, поэтому перегрузка будет разрешена в пользу устоявшей функции manip(vector<int>&).

Для второго вызова существует только одна устоявшая функция manip(const vector<int>&). Поскольку фактический аргумент является временной переменной, содержащей результат, возвращенный f(), то такой аргумент представляет собой r- значение, которое нельзя использовать для инициализации неконстантного формального параметра-ссылки функции manip(vector<int>&). Поэтому наилучшей является единственная устоявшая manip(const vector<int>&).

Разумеется, у функций может быть несколько фактических аргументов. Выбор

наилучшей из устоявших должен производиться с учетом ранжирования

extern int ff( char*, int ); extern int ff( int, int );

int main() {

ff( 0, 'a' ); // ff( int, int ) return 0;

последовательностей преобразований всех аргументов. Рассмотрим пример:

С++ для начинающих

465

}

Функция ff(), принимающая два аргумента типа int, выбирается в качестве наилучшей из устоявших по следующим причинам:

1.ее первый аргумент лучше. 0 дает точное соответствие с формальным параметром типа int, тогда как для установления соответствия с параметром типа char * требуется стандартное преобразование указателя;

2.ее второй аргумент имеет тот же ранг. К аргументу '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 ) устояли.

Вторая выбирается в качестве наилучшей по следующим причинам:

1.ее первый аргумент лучше. Инициализация ссылки для первой устоявшей функции хуже потому, что она требует добавления спецификатора const, не нужного для второй функции;

2.ее второй аргумент имеет тот же ранг. К аргументу 'c' типа char для установления

соответствия со вторым формальным параметром любой из двух функций должна быть применена последовательность трансформаций, имеющая ранг стандартного преобразования.

9.4.4. Аргументы со значениями по умолчанию

Наличие аргументов со значениями по умолчанию способно расширить множество устоявших функций. Устоявшими являются функции, которые вызываются с данным списком фактических аргументов. Но такая функция может иметь больше формальных параметров, чем задано фактических аргументов, в том случае, когда для каждого неуказанного параметра есть некое значение по умолчанию:

С++ для начинающих

466

extern void ff( int );

 

extern void ff( long, int = 0 );

 

int main() {

// соответствует ff( long, 0 );

 

ff( 2L );

 

ff( 0, 0 ); // соответствует ff( long, int ); ff( 0 ); // соответствует ff( int );

ff( 3.14 ); // ошибка: неоднозначность

}

Для первого и третьего вызовов функция ff() является устоявшей, хотя передан всего один фактический аргумент. Это обусловлено следующими причинами:

1.для второго формального параметра есть значение по умолчанию;

2.первый параметр типа 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()? Ответьте на те же вопросы.

С++ для начинающих

467

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(), например:

С++ для начинающих

468

min( 10, 20 );

min( 10.0, 20.0 );

но может преподнести сюрпризы в более сложных случаях: такой механизм ведет себя не как вызов функции, он лишь выполняет текстовую подстановку аргументов. В результате значения обоих аргументов оцениваются дважды: один раз при сравнении a и b, а

#include <iostream>

#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():

С++ для начинающих

469

template <class Type>

 

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 <class Glorp>

Glorp min2( Glorp a, Glorp b ) { return a < b ? a : b;

могли выбрать и любое другое:

}

При конкретизации (порождении конкретного экземпляра) шаблона вместо параметра- типа подставляется фактический встроенный или определенный пользователем тип.

Любой из типов int, double, char*, vector<int> или list<double> является допустимым аргументом шаблона.

Параметр-константа выглядит как обычное объявление. Он говорит о том, что вместо имени параметра должно быть подставлено значение константы из определения шаблона. Например, size это параметр-константа, который представляет размер массива arr:

С++ для начинающих

470

template <class Type, int size>

Type min( Type (&arr) [size] );

Вслед за списком параметров шаблона идет объявление или определение функции. Если

не обращать внимания на присутствие параметров в виде спецификаторов типа или констант, то определение шаблона функции выглядит точно так же, как и для обычных

template <class Type, int size>

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().

Имя параметра разрешено употреблять внутри объявления или определения шаблона. Параметр-тип служит спецификатором типа; его можно использовать точно так же, как спецификатор любого встроенного или пользовательского типа, например в объявлении переменных или в операциях приведения типов. Параметр-константа применяется как константное значение там, где требуются константные выражения, например для

задания размера в объявлении массива или в качестве начального значения элемента перечисления.

С++ для начинающих

471

//size определяет размер параметра-массива и инициализирует

//переменную типа const int

template <class Type, int size>

Type min( const Type (&r_array)[size] )

{

const int loc_size = size; Type loc_array[loc_size]; // ...

}

Если в глобальной области видимости объявлен объект, функция или тип с тем же именем, что у параметра шаблона, то глобальное имя оказывается скрытым. В следующем примере тип переменной tmp не double, а тот, что у параметра шаблона

typedef double Type; template <class Type>

Type min( Type a, Type b )

{

//tmp имеет тот же тип, что параметр шаблона Type, а не заданный

//глобальным typedef

Type tm = a < b ? a : b; return tmp;

Type:

}

Объект или тип, объявленные внутри определения шаблона функции, не могут иметь то

template <class Type>

Type min( Type a, Type b )

{

//ошибка: повторное объявление имени Type, совпадающего с именем

//параметра шаблона

typedef double Type; Type tmp = a < b ? a : b; return tmp;

же имя, что и какой-то из параметров:

}

Имя параметра-типа шаблона можно использовать для задания типа возвращаемого

//правильно: T1 представляет тип значения, возвращаемого min(),

//а T2 и T3 – параметры-типы этой функции

template <class T1, class T2, class T3>

значения:

T1 min( T2, T3 );

В одном списке параметров некоторое имя разрешается употреблять только один раз. Например, следующее определение будет помечено как ошибка компиляции: