<< Пред. стр. 43 (из 121) След. >>
// ...int local = obj1 + obj2;
return 0;
}
Здесь obj1 и obj2 – это l-значения. Однако для выполнения сложения в функции main() из переменных obj1 и obj2 извлекаются их значения. Действие, состоящее в извлечении значения объекта, представленного выражением вида l-значение, называется преобразованием l-значения в r-значение.
Когда функция ожидает аргумент, переданный по значению, то в случае, если аргумент является l-значением, выполняется его преобразование в r-значение:
#include
string color( "purple" );
void print( string );
int main() {
print( color ); // точное соответствие: преобразование lvalue
// в rvalue
return 0;
}
Так как аргумент в вызове print(color) передается по значению, то производится преобразование l-значения в r-значение для извлечения значения color и передачи его в функцию с прототипом print(string). Однако несмотря на то, что такое приведение имело место, считается, что фактический аргумент color точно соответствует объявлению print(string).
При вызове функций не всегда требуется применять к аргументам подобное преобразование. Ссылка представляет собой l-значение; если у функции есть параметр-ссылка, то при вызове функция получает l-значение. Поэтому к фактическому аргументу, которому соответствует формальный параметр-ссылка, описанное преобразование не применяется. Например, пусть объявлена такая функция:
#include
void print( list
В вызове ниже li – это l-значение, представляющее объект list
list
int main() {
// ...
print( li ); // точное соответствие: нет преобразования lvalue в
// rvalue
return 0;
}
Сопоставление li с параметром-ссылкой считается точным соответствием.
Второе преобразование, при котором все же фиксируется точное соответствие, – это преобразование массива в указатель. Как уже отмечалось в разделе 7.3, параметр функции никогда не имеет тип массива, трансформируясь вместо этого в указатель на его первый элемент. Аналогично фактический аргумент типа массива из NT (где N – число элементов в массиве, а T – тип каждого элемента) всегда приводится к типу указателя на T. Такое преобразование типа фактического аргумента и называется преобразованием массива в указатель. Несмотря на это, считается, что фактический аргумент точно соответствует формальному параметру типа “указатель на T”. Например:
int ai[3];
void putValues(int *);
int main() {
// ...
putValues(ai); // точное соответствие: преобразование массива в
// указатель
return 0;
}
Перед вызовом функции putValues() массив преобразуется в указатель, в результате чего фактический аргумент ai (массив из трех целых) приводится к указателю на int. Хотя формальным параметром функции 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;
}
Перед вызовом функции is_equal() фактические аргументы pi и parm преобразуются из типа “указатель на int” в тип “указатель на const int”. Эта трансформация заключается в добавлении спецификатора const к адресуемому типу, поэтому относится к категории преобразований спецификаторов. Несмотря на то, что функция ожидает получить два указателя на const int, а фактические аргументы являются указателями на int, считается, что точное соответствие между формальными и фактическими параметрами функции is_equal() установлено.
Преобразование спецификаторов применимо только к типу, который адресует указатель. Оно не употребляется в случае, когда формальный параметр имеет спецификатор const или volatile, а фактический аргумент – нет.
extern void takeCI( const int );
int main() {
int ii = ...;
takeCI(ii); // преобразование спецификаторов не применяется
return 0;
}
Хотя формальный параметр функции takeCI() имеет тип const int, а вызывается она с аргументом ii типа int, преобразование спецификаторов не производится: есть точное соответствие между фактическим аргументом и формальным параметром.
Все сказанное верно и для случая, когда аргумент является указателем, а спецификаторы const или volatile относятся к этому указателю:
extern void init( int *const );
extern int *pi;
int main() {
// ...
init(pi); // преобразование спецификаторов не применяется
return 0;
}
Спецификатор const при формальном параметре функции init() относится к самому указателю, а не к типу, который он адресует. Поэтому компилятор при анализе преобразований, которые должны быть применены к фактическому аргументу, не учитывает этот спецификатор. К аргументу pi не применяется преобразование спецификатора: считается, что этот аргумент и формальный параметр точно соответствуют друг другу.
Первые три из рассмотренных преобразований (l-значения в r-значение, массива в указатель и функции в указатель) часто называют трансформациями l-значений. (В разделе 9.4 мы увидим, что хотя и трансформации l-значений, и преобразования спецификаторов относятся к категории преобразований, не нарушающих точного соответствия, его степень считается выше в случае, когда необходима лишь первая трансформация. В следующем разделе мы поговорим об этом несколько подробнее.)
Точное соответствие можно установить принудительно, воспользовавшись явным приведением типов. Например, если есть две перегруженные функции:
extern void ff(int);
extern void ff(void *);
то вызов
ff( 0xffbc ); // вызывается ff(int)
будет точно соответствовать ff(int), хотя литерал 0xffbc записан в виде шестнадцатеричной константы. Программист может заставить компилятор вызвать функцию ff(void *), если явно выполнит операцию приведения типа:
ff( reinterpret_cast
Если к фактическому аргументу применяется такое приведение, то он приобретает тип, в который преобразуется. Явные приведения типов помогают в управлении процессом разрешения перегрузки. Например, если при разрешении перегрузки получается неоднозначный результат (фактические аргументы одинаково хорошо соответствуют двум или более устоявшим функциям), то для устранения неоднозначности можно применить явное приведение типа, заставив компилятор выбрать конкретную функцию.
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() {
manip( 'a' ); // тип char расширяется до int
return 0;
}
Символьный литерал имеет тип char. Он расширяется до int. Поскольку расширенный тип соответствует типу формального параметра функции manip(), мы говорим, что ее вызов требует расширения типа аргумента.
Рассмотрим следующий пример:
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 расширяются до разных типов:
#include
string format( int );
string format( unsigned int );
int main() {
format(a1); // вызывается format( int )
format(a2); // вызывается format( unsigned int )
return 0;
}
При первом обращении к format() фактический аргумент расширяется до типа int, так как для представления типа e1 используется char, и, следовательно, вызывается перегруженная функция format(int). При втором обращении тип фактического аргумента e2 представлен типом unsigned int и аргумент расширяется до unsigned int, из-за чего вызывается перегруженная функция format(unsigned int). Поэтому следует помнить, что поведение двух перечислений по отношению к процессу разрешения перегрузки может быть различным и зависеть от значений элементов, определяющих, как происходит расширение типа.
9.3.3. Подробнее о стандартном преобразовании
Имеется пять видов стандартных преобразований, а именно:
преобразования целых типов: приведение от целого типа или перечисления к любому другому целому типу (исключая трансформации, которые выше были отнесены к категории расширения типов);
преобразования типов с плавающей точкой: приведение от любого типа с плавающей точкой к любому другому типу с плавающей точкой (исключая трансформации, которые выше были отнесены к категории расширения типов);
преобразования между целым типом и типом с плавающей точкой: приведение от любого типа с плавающей точкой к любому целому типу или наоборот;
преобразования указателей: приведение целого значения 0 к типу указателя или трансформация указателя любого типа в тип void*;
преобразования в тип bool: приведение от любого целого типа, типа с плавающей точкой, перечислимого типа или указательного типа к типу bool.
Вот несколько примеров:
extern void print( void* );
extern void print( double );
int main() {
int i;
print( i ); // соответствует print( double );
// i подвергается стандартному преобразованию из int в double
print( &i ); // соответствует print( void* );
// &i подвергается стандартному преобразованию
// из int* в void*
return 0;
}
Преобразования, относящиеся к группам 1, 2 и 3, потенциально опасны, так как целевой тип может и не обеспечивать представления всех значений исходного. Например, с помощью float нельзя адекватно представить все значения типа int. Именно по этой причине трансформации, входящие в эти группы, отнесены к категории стандартных преобразований, а не расширений типов.
int i;
void calc( float );
int main() {
calc( i ); // стандартное преобразование между целым типом и типом с
// плавающей точкой потенциально опасно в зависимости от
// значения i
return 0;
}
При вызове функции calc() применяется стандартное преобразование из целого типа int в тип с плавающей точкой float. В зависимости от значения переменной i может оказаться, что его нельзя сохранить в типе float без потери точности.
Предполагается, что все стандартные изменения требуют одного объема работы. Например, преобразование из char в unsigned char не более приоритетно, чем из char в double. Близость типов не принимается во внимание. Если две устоявших функции требуют для установления соответствия стандартной трансформации фактического аргумента, то вызов считается неоднозначным и помечается компилятором как ошибка. Например, если даны две перегруженные функции:
extern void manip( long );
extern void manip( float );
то следующий вызов неоднозначен:
int main() {
manip( 3.14 ); // ошибка: неоднозначность
// manip( float ) не лучше, чем manip( int )
return 0;
}
Константа 3.14 имеет тип double. С помощью того или иного стандартного преобразования соответствие может быть установлено с любой из перегруженных функций. Поскольку есть две трансформации, приводящие к цели, вызов считается неоднозначным. Ни одно преобразование не имеет преимущества над другим. Программист может разрешить неоднозначность либо путем явного приведения типа:
manip ( static_cast
либо используя суффикс, обозначающий, что константа принадлежит к типу float:
manip ( 3.14F ) ); // manip( float )
Вот еще несколько примеров неоднозначных вызовов, которые помечаются как ошибки, поскольку соответствуют нескольким перегруженным функциям:
extern void farith( unsigned int );
extern void farith( float );
int main() {
// каждый из последующих вызовов неоднозначен
farith( 'a' ); // аргумент имеет тип char
farith( 0 ); // аргумент имеет тип int
farith( 2uL ); // аргумент имеет тип unsigned long
farith( 3.14159 ); // аргумент имеет тип double
farith( true ); // аргумент имеет тип bool
}
Стандартные преобразования указателей иногда противоречат интуиции. В частности, значение 0 приводится к указателю на любой тип; полученный таким образом указатель называется нулевым. Значение 0 может быть представлено как константное выражение целого типа:
void set(int*);
int main() {
// преобразование указателя из 0 в int* применяется к аргументам
// в обоих вызовах
set( 0L );
set( 0x00 );
return 0;
}
Константное выражение 0L (значение 0 типа long int) и константное выражение 0x00 (шестнадцатеричное целое значение 0) имеют целый тип и потому могут быть преобразованы в нулевой указатель типа int*.
Но поскольку перечисления не относятся к целым типам, элемент, равный 0, не приводим к типу указателя:
enum EN { zr = 0 };
set( zr ); // ошибка: zr нельзя преобразовать в тип int*
Вызов функции set() является ошибкой, так как не существует преобразования между значением zr элемента перечисления и формальным параметром типа int*, хотя zr равно 0.
Следует отметить, что константное выражение 0 имеет тип int. Для его приведения к типу указателя требуется стандартное преобразование. Если в множестве перегруженных функций есть функция с формальным параметром типа int, то именно в ее пользу будет разрешена перегрузка в случае, когда фактический аргумент равен 0:
void print( int );
void print( void * );
void set( const char * );
void set( char * );
int main () {
print( 0 ); // вызывается print( int );
set( 0 ); // неоднозначность
return 0;
}
При вызове print(int) имеет место точное соответствие, тогда как для вызова print(void*) необходимо приведение значения 0 к типу указателя. Поскольку соответствие лучше преобразования, для разрешения этого вызова выбирается функция print(int). Обращение к set() неоднозначно, так как 0 соответствует формальным параметрам обеих перегруженных функций за счет применения стандартной трансформации. Раз обе функции одинаково хороши, фиксируется неоднозначность.
Последнее из возможных преобразований указателя позволяет привести указатель любого типа к типу void*, поскольку void* – это родовой указатель на любой тип данных. Вот несколько примеров:
#include
extern void reset( void * );
void func( int *pi, string *ps ) {
// ...
reset( pi ); // преобразование указателя: int* в void*
/// ...
reset( ps ); // преобразование указателя: string* в void*
}
Только указатели на типы данных могут быть приведены к типу void* с помощью стандартного преобразования, с указателями на функции так поступать нельзя:
typedef int (*PFV)();
extern PFV testCases[10]; // массив указателей на функции
extern void reset( void * );
int main() {
// ...
reset( textCases[0] ); // ошибка: нет стандартного преобразования
// между int(*)() и void*
return 0;
}
9.3.4. Ссылки
Фактический аргумент или формальный параметр функции могут быть ссылками. Как это влияет на правила преобразования типов?
Рассмотрим, что происходит, когда ссылкой является фактический аргумент. Его тип никогда не бывает ссылочным. Аргумент-ссылка трактуется как l-значение, тип которого совпадает с типом соответствующего объекта:
int i;
int& ri = i;
void print( int );
int main() {
print( i ); // аргумент - это lvalue типа int
print( ri ); // то же самое
return 0;
}
Фактический аргумент в обоих вызовах имеет тип int. Использование ссылки для его передачи во втором вызове не влияет на сам тип аргумента.
Стандартные преобразования и расширения типов, рассматриваемые компилятором, одинаковы для случаев, когда фактический аргумент является ссылкой на тип T и когда он сам имеет такой тип. Например:
int i;
int& ri = i;
void calc( double );
int main() {
calc( i ); // стандартное преобразование между целым типом
// и типом с плавающей точкой
calc( ri ); // то же самое
return 0;
}
А как влияет на преобразования, применяемые к фактическому аргументу, формальный параметр-ссылка? Сопоставление дает следующие результаты:
фактический аргумент подходит в качестве инициализатора параметра-ссылки. В таком случае мы говорим, что между ними есть точное соответствие:
void swap( int &, int & );
void manip( int i1, int i2 ) {
// ...
swap( i1, i2 ); // правильно: вызывается swap( int &, int & )
// ...
return 0;
}
фактический аргумент не может инициализировать параметр-ссылку. В такой ситуации точного соответствия нет, и аргумент нельзя использовать для вызова функции. Например:
int obj;
void frd( double & );
int main() {
frd( obj ); // ошибка: параметр должен иметь иметь тип const double &
return 0;
}
Вызов функции frd() является ошибкой. Фактический аргумент имеет тип int и должен быть преобразован в тип double, чтобы соответствовать формальному параметру-ссылке. Результатом такой трансформации является временная переменная. Поскольку ссылка не имеет спецификатора const, то для ее инициализации такие переменные использовать нельзя.
Вот еще один пример, в котором между формальным параметром-ссылкой и фактическим аргументом нет соответствия: