Главная страница
Навигация по странице:

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

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

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница41 из 93
    1   ...   37   38   39   40   41   42   43   44   ...   93
    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
    На втором шаге процесса разрешения перегрузки функции компилятор идентифицирует и ранжирует преобразования, которые следует применить к каждому фактическому аргументу вызванной функции для приведения его к типу соответствующего формального параметра любой из устоявших функций. Ранжирование может дать один из трех возможных результатов:

    точное соответствие. Тип фактического аргумента точно соответствует типу формального параметра. Например, если в множестве перегруженных функций print()
    есть такие: void print( char ); void print( unsigned int ); void print( const char* );

    С++ для начинающих
    438
    то каждый из следующих трех вызовов дает точное соответствие: print( a ); // соответствует print( unsigned int );

    соответствие с преобразованием типа. Тип фактического аргумента не соответствует типу формального параметра, но может быть преобразован в него: ff( 0 ); // аргумент типа int приводится к типу char

    отсутствие соответствия. Тип фактического аргумента не может быть приведен к типу формального параметра в объявлении функции, поскольку необходимого преобразования не существует. Для каждого из следующих двух вызовов функции print() соответствия нет: print( si ); // ошибка: нет соответствия
    Для установления точного соответствия тип фактического аргумента необязательно должен совпадать с типом формального параметра. К аргументу могут быть применены некоторые тривиальные преобразования, а именно:

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

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

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

    преобразования спецификаторов.
    (Подробнее они рассмотрены ниже.)
    Категория соответствия с преобразованием типа является наиболее сложной. Необходимо рассмотреть несколько видов такого приведения: расширение типов (promotions),
    стандартные преобразования и определенные пользователем преобразования.
    (Расширения типов и стандартные преобразования изучаются в этой главе.
    Определенные пользователем преобразования будут представлены позднее, после детального рассмотрения классов; они выполняются конвертером, функцией-членом, которая позволяет определить в классе собственный набор “стандартных” трансформаций. В главе 15 мы познакомимся с такими конвертерами и с тем, как они влияют на разрешение перегрузки функций.) unsigned int a; print( 'a' ); // соответствует print( char ); print( "a" ); // соответствует print( const char* ); void ff( char );
    // функции print() объявлены так же, как и выше int *ip; class SmallInt { /* ... */ };
    SmallInt si; print( ip ); // ошибка: нет соответствия

    С++ для начинающих
    439
    При выборе лучшей из устоявших функций для данного вызова компилятор ищет функцию, для которой применяемые к фактическим аргументам преобразования являются “наилучшими”. Преобразования типов ранжируются следующим образом: точное соответствие лучше расширения типа, расширение типа лучше стандартного преобразования, а оно, в свою очередь, лучше определенного пользователем преобразования. Мы еще вернемся к ранжированию в разделе 9.4, а пока на простых примерах покажем, как оно помогает выбрать наиболее подходящую функцию.
    9.3.1.
    Подробнее о точном соответствии
    Самый простой случай возникает тогда, когда типы фактических аргументов совпадают с типами формальных параметров. Например, есть две показанные ниже перегруженные функции max(). Тогда каждый из вызовов max() точно соответствует одному из объявлений:
    }
    Перечислимый тип точно соответствует только определенным в нем элементам перечисления, а также объектам, которые объявлены как принадлежащие к этому типу:
    }
    Выше уже упоминалось, что фактический аргумент может точно соответствовать формальному параметру, даже если для приведения их типов необходимо некоторое тривиальное преобразование, первое из которых – преобразование l-значения в r- значение. Под l-значением понимается объект, удовлетворяющий следующим условиям:

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

    можно получить значение объекта; 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( Pass ); // точно соответствует ff( Stat ) ff( 0 ); // точно соответствует ff( int ) ff( curTok ); // точно соответствует ff( Tokens )
    // ...

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

    это значение легко модифицировать (если только в объявлении объекта нет спецификатора const).
    Напротив, r-значение – это выражение, значение которого вычисляется, или выражение, обозначающее временный объект, для которого нельзя получить адрес и значение которого нельзя модифицировать. Вот простой пример:
    }
    В первом операторе присваивания переменная lval – это l-значение, а литерал 5 – r- значение. Во втором операторе присваивания res – это l-значение, а временный объект, в котором хранится результат, возвращаемый функцией calc(), – это r-значение.
    В некоторых ситуациях в контексте, где ожидается значение, можно использовать выражение, представляющее собой l-значение:
    }
    Здесь obj1 и obj2 – это l-значения. Однако для выполнения сложения в функции main() из переменных obj1 и obj2 извлекаются их значения. Действие, состоящее в извлечении значения объекта, представленного выражением вида l-значение, называется преобразованием l-значения в r-значение.
    Когда функция ожидает аргумент, переданный по значению, то в случае, если аргумент является l-значением, выполняется его преобразование в r-значение:
    } int calc( int ); int main() { int lval, res; lval = 5; // lvalue: lval; rvalue: 5 res = calc( lval );
    // lvalue: res
    // rvalue: временный объект для хранения значения,
    // возвращаемого функцией calc() return 0; int obj1; int obj2; int main() {
    // ... int local = obj1 + obj2; return 0;
    #include string color( "purple" ); void print( string ); int main() { print( color ); // точное соответствие: преобразование lvalue
    // в rvalue return 0;

    С++ для начинающих
    441
    Так как аргумент в вызове print(color) передается по значению, то производится преобразование l-значения в r-значение для извлечения значения color и передачи его в функцию с прототипом print(string). Однако несмотря на то, что такое приведение имело место, считается, что фактический аргумент color точно соответствует объявлению print(string).
    При вызове функций не всегда требуется применять к аргументам подобное преобразование. Ссылка представляет собой l-значение; если у функции есть параметр- ссылка, то при вызове функция получает l-значение. Поэтому к фактическому аргументу, которому соответствует формальный параметр-ссылка, описанное преобразование не применяется. Например, пусть объявлена такая функция: void print( list & );
    В вызове ниже li – это l-значение, представляющее объект list, передаваемый функции print():
    }
    Сопоставление li с параметром-ссылкой считается точным соответствием.
    Второе преобразование, при котором все же фиксируется точное соответствие, – это преобразование массива в указатель. Как уже отмечалось в разделе 7.3, параметр функции никогда не имеет тип массива, трансформируясь вместо этого в указатель на его первый элемент. Аналогично фактический аргумент типа массива из NT (где N – число элементов в массиве, а T – тип каждого элемента) всегда приводится к типу указателя на
    T. Такое преобразование типа фактического аргумента и называется преобразованием массива в указатель. Несмотря на это, считается, что фактический аргумент точно соответствует формальному параметру типа “указатель на T”. Например:
    }
    Перед вызовом функции putValues() массив преобразуется в указатель, в результате чего фактический аргумент ai (массив из трех целых) приводится к указателю на int.
    #include list li(20); int main() {
    // ... print( li ); // точное соответствие: нет преобразования lvalue в
    // rvalue return 0; int ai[3]; void putValues(int *); int main() {
    // ... putValues(ai); // точное соответствие: преобразование массива в
    // указатель return 0;

    С++ для начинающих
    442
    Хотя формальным параметром функции putValues() является указатель и фактический аргумент при вызове преобразован, между ними устанавливается точное соответствие.
    При установлении точного соответствия допустимо также преобразование функции в указатель. (Оно упоминалось в разделе 7.9.) Как и параметр-массив, параметр-функция становится указателем на функцию. Фактический аргумент типа “функция” также автоматически приводится к типу указателя на функцию. Такое преобразование типа фактического аргумента и называется преобразованием функции в указатель. Хотя трансформация производится, считается, что фактический аргумент точно соответствует формальному параметру. Например:
    }
    Перед вызовом sort() применяется преобразование функции в указатель, которое приводит аргумент lexicoCompare от типа “функция” к типу “указатель на функцию”.
    Хотя формальным параметром функции является указатель, а фактическим – имя функции и, следовательно, было произведено преобразование функции в указатель, считается, что фактический аргумент точно третьему формальному параметру функции sort()
    Последнее из перечисленных выше – это преобразование спецификаторов. Оно относится только к указателям и заключается в добавлении спецификаторов const или volatile
    (или обоих) к типу, который адресует данный указатель:
    } 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; 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 или volatile, а фактический аргумент – нет.
    }
    Хотя формальный параметр функции takeCI() имеет тип const int, а вызывается она с аргументом ii типа int, преобразование спецификаторов не производится: есть точное соответствие между фактическим аргументом и формальным параметром.
    Все сказанное верно и для случая, когда аргумент является указателем, а спецификаторы const или volatile относятся к этому указателю:
    }
    Спецификатор const при формальном параметре функции init() относится к самому указателю, а не к типу, который он адресует. Поэтому компилятор при анализе преобразований, которые должны быть применены к фактическому аргументу, не учитывает этот спецификатор. К аргументу pi не применяется преобразование спецификатора: считается, что этот аргумент и формальный параметр точно соответствуют друг другу.
    Первые три из рассмотренных преобразований (l-значения в r-значение, массива в указатель и функции в указатель) часто называют трансформациями l-значений. (В разделе 9.4 мы увидим, что хотя и трансформации l-значений, и преобразования спецификаторов относятся к категории преобразований, не нарушающих точного соответствия, его степень считается выше в случае, когда необходима лишь первая трансформация. В следующем разделе мы поговорим об этом несколько подробнее.)
    Точное соответствие можно установить принудительно, воспользовавшись явным приведением типов. Например, если есть две перегруженные функции: extern void takeCI( const int ); int main() { int ii = ...; takeCI(ii); // преобразование спецификаторов не применяется return 0; extern void init( int *const ); extern int *pi; int main() {
    // ... init(pi); // преобразование спецификаторов не применяется return 0;

    С++ для начинающих
    444
    extern void ff(void *); то вызов ff( 0xffbc ); // вызывается ff(int) будет точно соответствовать ff(int), хотя литерал 0xffbc записан в виде шестнадцатеричной константы. Программист может заставить компилятор вызвать функцию ff(void *), если явно выполнит операцию приведения типа: ff( reinterpret_cast(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.
    Подобное расширение применяется, когда тип фактического аргумента совпадает с одним из только что перечисленных типов, а формальный параметр относится к соответствующему расширенному типу:
    }
    Символьный литерал имеет тип char. Он расширяется до int. Поскольку расширенный тип соответствует типу формального параметра функции manip(), мы говорим, что ее вызов требует расширения типа аргумента. extern void ff(int); extern void manip( int ); int main() { manip( 'a' ); // тип char расширяется до int return 0;

    С++ для начинающих
    445
    Рассмотрим следующий пример: print( uc ); // print( int ); для uc требуется только расширение типа
    Для аппаратной платформы, на которой unsigned char занимает один байт памяти, а int
    – четыре байта, расширение преобразует unsigned char в int, так как с его помощью можно представить все значения типа unsigned char. Для такой машинной архитектуры из приведенного в примере множества перегруженных функций наилучшее соответствие аргументу типа unsigned char обеспечивает print(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 расширяются до разных типов: extern void print( unsigned int ); extern void print( int ); extern void print( char ); unsigned char uc; enum Stat ( Fail, Pass ); extern void ff( int ); extern void ff( char ); int main() {
    // правильно: элемент перечисления Pass расширяется до типа int ff( Pass ); // ff( int ) ff( 0 ); // ff( int )

    С++ для начинающих
    1   ...   37   38   39   40   41   42   43   44   ...   93


    написать администратору сайта