Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
86
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

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

437

Третий шаг заключается в выборе функции, лучше всего отвечающей контексту вызова.

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

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

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

Преобразования типов и их ранжирование более подробно обсуждаются в разделе 9.3. Здесь мы лишь кратко рассмотрим ранжирование преобразований для нашего примера.

Для устоявшей функции f(int) должно быть применено приведение фактического аргумента типа double к типу int, относящееся к числу стандартных. Для устоявшей

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

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

(Более подробно все шаги разрешения перегрузки функции обсуждаются в разделе 9.4. Процесс разрешения используется также при вызовах перегруженной функции-члена класса и перегруженного оператора. В разделе 15.10 рассматриваются правила разрешения перегрузки, применяемые к функциям-членам класса, а в разделе 15.11 – правила для перегруженных операторов. При разрешении перегрузки следует также принимать во внимание функции, конкретизированные из шаблонов. В разделе 10.8 обсуждается, как шаблоны влияют на такое разрешение.)

Упражнение 9.5 Что происходит на последнем (третьем) шаге процесса разрешения перегрузки функции?

9.3. Преобразования типов аргументов A

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

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

точное соответствие. Тип фактического аргумента точно соответствует типу формального параметра. Например, если в множестве перегруженных функций

void print( unsigned int ); void print( const char* );

print() есть такие: void print( char );

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

438

unsigned int a;

 

print( 'a' );

// соответствует print( char );

print( "a" );

// соответствует print( const char* );

то каждый из следующих трех вызовов дает точное соответствие:

print( a );

// соответствует print( unsigned int );

соответствие с преобразованием типа. Тип фактического аргумента не

void ff( char );

соответствует типу формального параметра, но может быть преобразован в него:

ff( 0 );

// аргумент типа int приводится к типу char

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

// функции print() объявлены так же, как и выше int *ip;

class SmallInt { /* ... */ }; SmallInt si;

print( ip ); // ошибка: нет соответствия

вызовов функции print() соответствия нет: print( si ); // ошибка: нет соответствия

Для установления точного соответствия тип фактического аргумента необязательно должен совпадать с типом формального параметра. К аргументу могут быть применены некоторые тривиальные преобразования, а именно:

преобразование l-значения в r-значение;

преобразование массива в указатель;

преобразование функции в указатель;

преобразования спецификаторов.

(Подробнее они рассмотрены ниже.)

Категория соответствия с преобразованием типа является наиболее сложной. Необходимо рассмотреть несколько видов такого приведения: расширение типов (promotions),

стандартные преобразования и определенные пользователем преобразования. (Расширения типов и стандартные преобразования изучаются в этой главе. Определенные пользователем преобразования будут представлены позднее, после детального рассмотрения классов; они выполняются конвертером, функцией-членом, которая позволяет определить в классе собственный набор стандартныхтрансформаций. В главе 15 мы познакомимся с такими конвертерами и с тем, как они влияют на разрешение перегрузки функций.)

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

439

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

9.3.1. Подробнее о точном соответствии

Самый простой случай возникает тогда, когда типы фактических аргументов совпадают с типами формальных параметров. Например, есть две показанные ниже перегруженные функции max(). Тогда каждый из вызовов max() точно соответствует одному из

int max( int, int );

double max( double, double );

 

 

int i1;

 

 

 

void calc( double d1

) {

 

 

max( 56, i1 );

// точно соответствует max( int, int );

 

 

max( d1, 66.9 );

// точно соответствует max( double, double );

объявлений:

 

 

 

}

 

 

 

 

 

 

 

Перечислимый тип точно соответствует только определенным в нем элементам

enum Tokens { INLINE = 128; VIRTUAL = 129; }; Tokens curTok = INLINE;

enum Stat { Fail, Pass };

extern void ff( Tokens ); extern void ff( Stat ); extern void ff( int );

int main() {

// точно соответствует ff( Stat )

ff( Pass );

ff( 0 );

// точно соответствует ff( int )

ff( curTok );

// точно соответствует ff( Tokens )

// ...

 

перечисления, а также объектам, которые объявлены как принадлежащие к этому типу:

}

Выше уже упоминалось, что фактический аргумент может точно соответствовать формальному параметру, даже если для приведения их типов необходимо некоторое тривиальное преобразование, первое из которых преобразование l-значения в r- значение. Под l-значением понимается объект, удовлетворяющий следующим условиям:

можно получить адрес объекта;

можно получить значение объекта;

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

440

∙ это значение легко модифицировать (если только в объявлении объекта нет спецификатора const).

Напротив, r-значение это выражение, значение которого вычисляется, или выражение, обозначающее временный объект, для которого нельзя получить адрес и значение

int calc( int );

int main() {

int lval, res;

lval = 5; // lvalue: lval; rvalue: 5 res = calc( lval );

//lvalue: res

//rvalue: временный объект для хранения значения,

//возвращаемого функцией calc()

return 0;

которого нельзя модифицировать. Вот простой пример:

}

Впервом операторе присваивания переменная lval это l-значение, а литерал 5 – r- значение. Во втором операторе присваивания res это l-значение, а временный объект, в котором хранится результат, возвращаемый функцией calc(), – это r-значение.

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

int obj1; int obj2;

int main() {

// ...

int local = obj1 + obj2; return 0;

выражение, представляющее собой l-значение:

}

Здесь obj1 и obj2 это l-значения. Однако для выполнения сложения в функции main() из переменных obj1 и obj2 извлекаются их значения. Действие, состоящее в извлечении значения объекта, представленного выражением вида l-значение, называется преобразованием l-значения в r-значение.

Когда функция ожидает аргумент, переданный по значению, то в случае, если аргумент

 

 

#include <string>

 

 

 

string color( "purple" );

 

 

void print( string );

 

 

 

int main() {

// точное соответствие: преобразование lvalue

 

 

print( color );

 

 

return 0;

// в rvalue

 

 

 

является l-значением, выполняется его преобразование в r-значение:

 

 

}

 

 

 

 

 

 

 

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

441

Так как аргумент в вызове print(color) передается по значению, то производится преобразование l-значения в r-значение для извлечения значения color и передачи его в функцию с прототипом print(string). Однако несмотря на то, что такое приведение имело место, считается, что фактический аргумент color точно соответствует объявлению print(string).

При вызове функций не всегда требуется применять к аргументам подобное преобразование. Ссылка представляет собой l-значение; если у функции есть параметр- ссылка, то при вызове функция получает l-значение. Поэтому к фактическому аргументу, которому соответствует формальный параметр-ссылка, описанное преобразование не

#include <list>

применяется. Например, пусть объявлена такая функция: void print( list<int> & );

В вызове ниже li это l-значение, представляющее объект list<int>, передаваемый

list<int> li(20);

int main() { // ...

print( li ); // точное соответствие: нет преобразования lvalue в // rvalue

return 0;

функции print():

}

Сопоставление li с параметром-ссылкой считается точным соответствием.

Второе преобразование, при котором все же фиксируется точное соответствие, – это преобразование массива в указатель. Как уже отмечалось в разделе 7.3, параметр функции никогда не имеет тип массива, трансформируясь вместо этого в указатель на его первый элемент. Аналогично фактический аргумент типа массива из NT (где N – число элементов в массиве, а T – тип каждого элемента) всегда приводится к типу указателя на T. Такое преобразование типа фактического аргумента и называется преобразованием массива в указатель. Несмотря на это, считается, что фактический аргумент точно

int ai[3];

void putValues(int *);

int main() { // ...

putValues(ai); // точное соответствие: преобразование массива в // указатель

return 0;

соответствует формальному параметру типа указатель на T”. Например:

}

Перед вызовом функции putValues() массив преобразуется в указатель, в результате чего фактический аргумент ai (массив из трех целых) приводится к указателю на int.

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

442

Хотя формальным параметром функции putValues() является указатель и фактический аргумент при вызове преобразован, между ними устанавливается точное соответствие.

При установлении точного соответствия допустимо также преобразование функции в указатель. (Оно упоминалось в разделе 7.9.) Как и параметр-массив, параметр-функция становится указателем на функцию. Фактический аргумент типа функциятакже автоматически приводится к типу указателя на функцию. Такое преобразование типа фактического аргумента и называется преобразованием функции в указатель. Хотя трансформация производится, считается, что фактический аргумент точно соответствует

int lexicoCompare( const string &, const string & );

typedef int (*PFI)( const string &, const string & ); void sort( string *, string *, PFI );

string as[10];

int main()

{

// ...

sort( as,

as + sizeof(as)/sizeof(as[0] - 1 ), lexicoCompare // точное соответствие

// преобразование функции в указатель

);

return 0;

формальному параметру. Например:

}

Перед вызовом sort() применяется преобразование функции в указатель, которое приводит аргумент lexicoCompare от типа функцияк типу указатель на функцию”. Хотя формальным параметром функции является указатель, а фактическим имя функции и, следовательно, было произведено преобразование функции в указатель, считается, что фактический аргумент точно третьему формальному параметру функции sort().

Последнее из перечисленных выше это преобразование спецификаторов. Оно относится

только к указателям и заключается в добавлении спецификаторов const или volatile

int a[5] = { 4454, 7864, 92, 421, 938 }; int *pi = a;

bool is_equal( const int * , const int * );

void func( int *parm ) {

// точное соответствие между pi и parm: преобразование спецификаторов if ( is_equal( pi, parm ) )

// ...

return 0;

(или обоих) к типу, который адресует данный указатель:

}

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

443

Перед вызовом функции is_equal() фактические аргументы pi и parm преобразуются из типа указатель на intв тип указатель на const int”. Эта трансформация заключается в добавлении спецификатора const к адресуемому типу, поэтому относится к категории преобразований спецификаторов. Несмотря на то, что функция ожидает получить два указателя на const int, а фактические аргументы являются указателями на int, считается, что точное соответствие между формальными и фактическими параметрами функции is_equal() установлено.

Преобразование спецификаторов применимо только к типу, который адресует указатель. Оно не употребляется в случае, когда формальный параметр имеет спецификатор const

extern void takeCI( const int );

int main() {

 

int ii = ...;

// преобразование спецификаторов не применяется

takeCI(ii);

return 0;

 

или volatile, а фактический аргумент нет.

}

Хотя формальный параметр функции takeCI() имеет тип const int, а вызывается она с аргументом ii типа int, преобразование спецификаторов не производится: есть точное соответствие между фактическим аргументом и формальным параметром.

Все сказанное верно и для случая, когда аргумент является указателем, а спецификаторы

extern void init( int *const ); extern int *pi;

int main() { // ...

init(pi); // преобразование спецификаторов не применяется return 0;

const или volatile относятся к этому указателю:

}

Спецификатор const при формальном параметре функции init() относится к самому указателю, а не к типу, который он адресует. Поэтому компилятор при анализе преобразований, которые должны быть применены к фактическому аргументу, не учитывает этот спецификатор. К аргументу pi не применяется преобразование спецификатора: считается, что этот аргумент и формальный параметр точно соответствуют друг другу.

Первые три из рассмотренных преобразований (l-значения в r-значение, массива в указатель и функции в указатель) часто называют трансформациями l-значений. (В разделе 9.4 мы увидим, что хотя и трансформации l-значений, и преобразования спецификаторов относятся к категории преобразований, не нарушающих точного соответствия, его степень считается выше в случае, когда необходима лишь первая трансформация. В следующем разделе мы поговорим об этом несколько подробнее.)

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

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

444

extern void ff(int); extern void ff(void *);

то вызов

ff( 0xffbc ); // вызывается ff(int)

будет точно соответствовать ff(int), хотя литерал 0xffbc записан в виде шестнадцатеричной константы. Программист может заставить компилятор вызвать функцию ff(void *), если явно выполнит операцию приведения типа:

ff( reinterpret_cast<void *>(0xffbc) );

// вызывается ff(void*)

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

9.3.2. Подробнее о расширении типов

Под расширением типа понимается одно из следующих преобразований:

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

аргумент типа float расширяется до типа double;

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

unsigned int, long, unsigned long;

аргумент типа bool расширяется до типа int.

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

extern void manip( int );

int main() {

// тип char расширяется до int

manip( 'a' );

return 0;

 

соответствующему расширенному типу:

}

Символьный литерал имеет тип char. Он расширяется до int. Поскольку расширенный тип соответствует типу формального параметра функции manip(), мы говорим, что ее вызов требует расширения типа аргумента.

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

445

extern void print( unsigned int ); extern void print( int );

extern void print( char );

unsigned char uc;

Рассмотрим следующий пример:

print( uc ); // print( int ); для uc требуется только расширение типа

Для аппаратной платформы, на которой unsigned char занимает один байт памяти, а int четыре байта, расширение преобразует unsigned char в int, так как с его помощью можно представить все значения типа unsigned char. Для такой машинной

архитектуры из приведенного в примере множества перегруженных функций наилучшее соответствие аргументу типа unsigned char обеспечивает print(int). Для двух других функций установление соответствия требует стандартного приведения.

Следующий пример иллюстрирует расширение фактического аргумента перечислимого

enum Stat ( Fail, Pass );

extern void ff( int ); extern void ff( char );

int main() {

 

// правильно: элемент перечисления Pass расширяется до типа int

ff( Pass );

// ff( int )

ff( 0 );

// ff( int )

типа:

}

Иногда расширение перечислений преподносит сюрпризы. Компиляторы часто выбирают представление перечисления в зависимости от значений его элементов. Предположим, что в вышеупомянутой архитектуре (один байт для char и четыре байта для int) определено такое перечисление:

enum e1 { a1, b1, c1 };

Поскольку есть всего три элемента: a1, b1 и c1 со значениями 0, 1 и 2 соответственно и поскольку все эти значения можно представить типом char, то компилятор, как правило, и выбирает char для представления типа e1. Рассмотрим, однако, перечисление e2 со следующим множеством элементов:

enum e2 { a2, b2, c2=0x80000000 };

Так как одна из констант имеет значение 0x80000000, то компилятор обязан выбрать для представления e2 такой тип, который достаточен для хранения значения 0x80000000, то есть unsigned int.

Итак, хотя и e1, и e2 являются перечислениями, их представления различаются. Из-за этого e1 и e2 расширяются до разных типов: