9. Перегруженные функцииИтак, мы уже знаем, как объявлять, определять и использовать функции в программах. В этой главе речь пойдет об их специальном виде – перегруженных функциях. Две функции называются перегруженными, если они имеют одинаковое имя, объявлены в одной и той же области видимости, но имеют разные списки формальных параметров. Мы расскажем, как объявляются такие функции и почему они полезны. Затем мы рассмотрим вопрос об их разрешении, т.е. о том, какая именно из нескольких перегруженных функций вызывается во время выполнения программы. Эта проблема является одной из наиболее сложных в C++. Тем, кто хочет разобраться в деталях, будет интересно прочитать два раздела в конце главы, где тема преобразования типов аргументов и разрешения перегруженных функций раскрывается более подробно. 9.1. Объявления перегруженных функцийТеперь, научившись объявлять, определять и использовать функции в программах,
познакомимся с перегрузкой – еще одним аспектом в C++. Перегрузка позволяет
иметь несколько одноименных функций, выполняющих схожие операции над аргументами
разных типов. 1 + 3 вызывается операция целочисленного сложения, тогда как вычисление выражения 1.0 + 3.0 осуществляет сложение с плавающей точкой. Выбор той или иной операции производится
незаметно для пользователя. Операция сложения перегружена, чтобы обеспечить
работу с операндами разных типов. Ответственность за распознавание контекста
и применение операции, соответствующей типам операндов, возлагается на компилятор,
а не на программиста. 9.1.1. Зачем нужно перегружать имя функцииКак и в случае со встроенной операцией сложения, нам может понадобиться набор функций, выполняющих одно и то же действие, но над параметрами различных типов. Предположим, что мы хотим определить функции, возвращающие наибольшее из переданных значений параметров. Если бы не было перегрузки, пришлось бы каждой такой функции присвоить уникальное имя. Например, семейство функций max() могло бы выглядеть следующим образом: int i_max( int, int ); int vi_max( const vector<int> & ); int matrix_max( const matrix & ); Однако все они делают одно и то же: возвращают наибольшее из значений параметров.
С точки зрения пользователя, здесь лишь одна операция – вычисление максимума,
а детали ее реализации большого интереса не представляют. int ix = max( j, k ); vector<int> vec; //... int iy = max( vec ); Этот подход оказывается чрезвычайно полезным во многих ситуациях. 9.1.2. Как перегрузить имя функцииВ C++ двум или более функциям может быть дано одно и то же имя при условии, что их списки параметров различаются либо числом параметров, либо их типами. В данном примере мы объявляем перегруженную функцию max(): int max ( int, int ); int max( const vector<int> & ); int max( const matrix & ); Для каждого перегруженного объявления требуется отдельное определение функции
max() с соответствующим списком параметров.
Перегруженные функции не могут различаться лишь типами возвращаемого значения; если списки параметров двух функций разнятся только подразумеваемыми по умолчанию значениями аргументов, то второе объявление считается повторным: // объявления одной и той же функции int max ( int *ia, int sz ); int max ( int *ia, int = 10 ); Ключевое слово typedef создает альтернативное имя для существующего типа данных, новый тип при этом не создается. Поэтому если списки параметров двух функций различаются только тем, что в одном используется typedef, а в другом тип, для которого typedef служит псевдонимом, такие списки считаются одинаковыми, как, например, в следующих двух объявлениях функции calc(). В таком случае второе объявление даст ошибку компиляции, поскольку возвращаемое значение отличается от указанного раньше: // typedef не вводит нового типа typedef double DOLLAR; // ошибка: одинаковые списки параметров, но разные типы // возвращаемых значений extern DOLLAR calc( DOLLAR ); extern int calc( double ); Спецификаторы const или volatile при подобном сравнении не принимаются во внимание. Так, следующие два объявления считаются одинаковыми: // объявляют одну и ту же функцию void f( int ); void f( const int ); Спецификатор const важен только внутри определения функции: он показывает, что в теле функции запрещено изменять значение параметра. Однако аргумент, передаваемый по значению, можно использовать в теле функции как обычную инициированную переменную: вне функции изменения не видны. (Способы передачи аргументов, в частности передача по значению, обсуждаются в разделе 7.3.) Добавление спецификатора const к параметру, передаваемому по значению, не влияет на его интерпретацию. Функции, объявленной как f(int), может быть передано любое значение типа int, равно как и функции f(const int). Поскольку они обе принимают одно и то же множество значений аргумента, то приведенные объявления не считаются перегруженными. f() можно определить как void f( int i ) { } или как void f( const int i ) { } Наличие двух этих определений в одной программе – ошибка, так как одна и та
же функция определяется дважды. // объявляются разные функции void f( int* ); void f( const int* ); // и здесь объявляются разные функции 9.1.3. Когда не надо перегружать имя функцииВ каких случаях перегрузка имени не дает преимуществ? Например, тогда, когда присвоение функциям разных имен облегчает чтение программы. Вот несколько примеров. Следующие функции оперируют одним и тем же абстрактным типом даты. На первый взгляд, они являются подходящими кандидатами для перегрузки: void setDate( Date&, int, int, int ); Date &convertDate( const string & ); void printDate( const Date& ); Эти функции работают с одним типом данных – классом Date, но выполняют семантически различные действия. В этом случае лексическая сложность, связанная с употреблением различных имен, проистекает из принятого программистом соглашения об обеспечении набора операций над типом данных и именования функций в соответствии с семантикой этих операций. Правда, механизм классов C++ делает такое соглашение излишним. Следовало бы сделать такие функции членами класса Date, но при этом оставить разные имена, отражающие смысл операции: #include <string> class Date { public: set( int, int, int ); Date& convert( const string & ); void print(); // ... Приведем еще один пример. Следующие пять функций-членов Screen выполняют различные операции над экранным курсором, являющимся принадлежностью того же класса. Может показаться, что разумно перегрузить эти функции под общим названием move(): Screen& moveHome(); Screen& moveAbs( int, int ); Screen& moveRel( int, int, char *direction ); Screen& moveX( int ); Screen& moveY( int ); Впрочем, последние две функции перегрузить нельзя, так как у них одинаковые списки параметров. Чтобы сделать сигнатуру уникальной, объединим их в одну функцию: // функция, объединяющая moveX() и moveY() Screen& move( int, char xy ); Теперь у всех функций разные списки параметров, так что их можно перегрузить под именем move(). Однако этого делать не следует: разные имена несут информацию, без которой программу будет труднее понять. Так, выполняемые данными функциями операции перемещения курсора различны. Например, moveHome() осуществляет специальный вид перемещения в левый верхний угол экрана. Какой из двух приведенных ниже вызовов более понятен пользователю и легче запоминается? // какой вызов понятнее? myScreen.home(); // мы считаем, что этот! myScreen.move(); В некоторых случаях не нужно ни перегружать имя функции, ни назначать разные имена: применение подразумеваемых по умолчанию значений аргументов позволяет объединить несколько функций в одну. Например, функции управления курсором moveAbs(int, int); moveAbs(int, int, char*); различаются наличием третьего параметра типа char*. Если их реализации похожи и для третьего аргумента можно найти разумное значение по умолчанию, то обе функции можно заменить одной. В данном случае на роль значения по умолчанию подойдет указатель со значением 0: move( int, int, char* = 0 ); Применять те или иные возможности следует тогда, когда этого требует логика приложения. Вовсе не обязательно включать перегруженные функции в программу только потому, что они существуют. 9.1.4. Перегрузка и область видимости A
#include <string> void print( const string & ); void print( double ); // перегружает print() void fooBar( int ival ) Поскольку каждый класс определяет собственную область видимости, функции, являющиеся
членами двух разных классов, не перегружают друг друга. (Функции-члены класса
описываются в главе 13. Разрешение перегрузки для функций-членов класса рассматривается
в главе 15.) #include <string> namespace IBM { extern void print( const string & ); extern void print( double ); // перегружает print() } namespace Disney { // отдельная область видимости: // не перегружает функцию print() из пространства имен IBM extern void print( int ); } Использование using-объявлений и using-директив помогает сделать члены пространства
имен доступными в других областях видимости. Эти механизмы оказывают определенное
влияние на объявления перегруженных функций. (Using-объявления и using-директивы
рассматривались в разделе 8.6.) namespace libs_R_us { int max( int, int ); int max( double, double ); extern void print( int ); Первое using-объявление вводит обе функции libs_R_us::max в глобальную область видимости. Теперь любую из функций max() можно вызвать внутри func(). По типам аргументов определяется, какую именно функцию вызывать. Второе using-объявление – это ошибка: в нем нельзя задавать список параметров. Функция libs_R_us::print() объявляется только так: using libs_R_us::print; Using-объявление всегда делает доступными все перегруженные функции с указанным именем. Такое ограничение гарантирует, что интерфейс пространства имен libs_R_us не будет нарушен. Ясно, что в случае вызова print( 88 ); автор пространства имен ожидает, что будет вызвана функция libs_R_us::print(int).
Если разрешить пользователю избирательно включать в область видимости лишь одну
из нескольких перегруженных функций, то поведение программы становится непредсказуемым. #include <string> namespace libs_R_us { extern void print( int ); extern void print( double ); } extern void print( const string & ); // libs_R_us::print( int ) и libs_R_us::print( double ) Using-объявление добавляет в глобальную область видимости два объявления: для
print(int) и для print(double). Они являются псевдонимами в пространстве libs_R_us
и включаются в множество перегруженных функций с именем print, где уже находится
глобальная print(const string &). При разрешении перегрузки print в fooBar
рассматриваются все три функции. namespace libs_R_us { void print( int ); void print( double ); } void print( int ); using libs_R_us::print; // ошибка: повторное объявление print(int) void fooBar( int ival ) Мы показали, как связаны using-объявления и перегруженные функции. Теперь рассмотрим особенности применения using-директивы. Using-директива приводит к тому, что члены пространства имен выглядят объявленными вне этого пространства, добавляя их в новую область видимости. Если в этой области уже есть функция с тем же именем, то происходит перегрузка. Например: #include <string> namespace libs_R_us { extern void print( int ); extern void print( double ); } extern void print( const string & ); // using-директива Это верно и в том случае, когда есть несколько using-директив. Одноименные функции, являющиеся членами разных пространств, включаются в одно и то множество: namespace IBM { int print( int ); } namespace Disney { double print( double ); } // using-директива // формируется множество перегруженных функций из различных // пространств имен using namespace IBM; using namespace Disney; long double print(long double); int main() { Множество перегруженных функций с именем print в глобальной области видимости
включает функции print(int), print(double) и print(long double). Все они рассматриваются
в main() при разрешении перегрузки, хотя первоначально были определены в разных
пространствах имен. 9.1.5. Директива extern "C" и перегруженные функции AВ разделе 7.7 мы видели, что директиву связывания extern
"C" можно использовать в программе на C++ для того, чтобы указать,
что некоторый объект находится в части, написанной на языке C. Как эта директива
влияет на объявления перегруженных функций? Могут ли в одном и том же множестве
находиться функции, написанные как на C++, так и на C? // ошибка: для двух перегруженных функций указана директива extern "C" extern "C" void print( const char* ); extern "C" void print( int ); Приведенный ниже пример перегруженной функции calc() иллюстрирует типичное применение директивы extern "C": class SmallInt ( /* ... */ ); class BigNum ( /* ... */ ); // написанная на C функция может быть вызвана как из программы, Написанная на C функция calc() может быть вызвана как из C, так и из программы
на C++. Остальные две функции принимают в качестве параметра класс и, следовательно,
их допустимо использовать только в программе на C++. Порядок следования объявлений
несуществен. Smallint si = 8; int main() { calc( 34 ); // вызывается C-функция calc( double ) calc( si ); // вызывается функция C++ calc( const SmallInt & ) // ... return 0; } 9.1.6. Указатели на перегруженные функции AМожно объявить указатель на одну из множества перегруженных функций. Например: extern void ff( vector<double> ); extern void ff( unsigned int ); // на какую функцию указывает pf1? Поскольку функция ff() перегружена, одного инициализатора &ff недостаточно
для выбора правильного варианта. Чтобы понять, какая именно функция инициализирует
указатель, компилятор ищет в множестве всех перегруженных функций ту, которая
имеет тот же тип возвращаемого значения и список параметров, что и функция,
на которую ссылается указатель. В нашем случае будет выбрана функция ff(unsigned
int). extern void ff( vector<double> ); extern void ff( unsigned int ); // ошибка: соответствие не найдено: неверный список параметров void ( *pf2 )( int ) = &ff; // ошибка: соответствие не найдено: неверный тип возвращаемого значения double ( *pf3 )( vector<double> ) = &ff; Присваивание работает аналогично. Если значением указателя должен стать адрес перегруженной функции , то для выбора операнда в правой части оператора присваивания используется тип указателя на функцию. И если компилятор не находит функции, в точности соответствующей нужному типу, он выдает сообщение об ошибке. Таким образом, преобразование типов между указателями на функции никогда не производится. matrix calc( const matrix & ); int calc( int, int ); int ( *pc1 )( int, int ) = 0; 9.1.7. Безопасное связывание AПри использовании перегрузки складывается впечатление, что в программе можно
иметь несколько одноименных функций с разными списками параметров. Однако это
лексическое удобство существует только на уровне исходного текста. В большинстве
систем компиляции программы, обрабатывающие этот текст для получения исполняемого
кода, требуют, чтобы все имена были различны. Редакторы связей, как правило,
разрешают внешние ссылки лексически. Если такой редактор встречает имя print
два или более раз, он не может различить их путем анализа типов (к этому моменту
информация о типах обычно уже потеряна). Поэтому он просто печатает сообщение
о повторно определенном символе print и завершает работу. Упражнение 9.1Зачем может понадобиться объявлять перегруженные функции? Упражнение 9.2Как нужно объявить перегруженные варианты функции error(), чтобы были корректны следующие вызовы: int index; int upperBound; char selectVal; // ... error( "Array out of bounds: ", index, upperBound ); error( "Division by zero" ); error( "Invalid selection", selectVal ); Упражнение 9.3Объясните, к какому эффекту приводит второе объявление в каждом из приведенных примеров: (a) int calc( int, int ); int calc( const int, const int ); (b) int get(); double get(); (c) int *reset( int * ); double *reset( double * ): (d) extern "C" int compute( int *, int ); extern "C" double compute( double *, double ); Упражнение 9.4Какая из следующих инициализаций приводит к ошибке? Почему? (a) void reset( int * ); void (*pf)( void * ) = reset; (b) int calc( int, int ); int (*pf1)( int, int ) = calc; (c) extern "C" int compute( int *, int ); int (*pf3)( int*, int ) = compute; (d) void (*pf4)( const matrix & ) = 0; 9.2. Три шага разрешения перегрузкиРазрешением перегрузки функции называется процесс выбора той функции из множества перегруженных, которую следует вызвать. Этот процесс основывается на указанных при вызове аргументах. Рассмотрим пример: T t1, t2; void f( int, int ); void f( float, float ); int main() { Здесь в ходе процесса разрешения перегрузки в зависимости от типа T определяется,
будет ли при обработке выражения f(t1,t2) вызвана функция f(int,int) или f(float,float)
или зафиксируется ошибка. void f(); void f( int ); void f( double, double = 3.4 ); void f( char *, char * ); void main() { При разрешении перегрузки функции выполняются следующие шаги:
Рассмотрим последовательно каждый пункт.
Если после второго шага не нашлось устоявших функций, то вызов считается ошибочным.
В таких случаях мы говорим, что имеет место отсутствие соответствия. Упражнение 9.5Что происходит на последнем (третьем) шаге процесса разрешения перегрузки функции? 9.3. Преобразования типов аргументов AНа втором шаге процесса разрешения перегрузки функции компилятор идентифицирует и ранжирует преобразования, которые следует применить к каждому фактическому аргументу вызванной функции для приведения его к типу соответствующего формального параметра любой из устоявших функций. Ранжирование может дать один из трех возможных результатов:
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 ); // ошибка: нет соответствия Для установления точного соответствия тип фактического аргумента необязательно должен совпадать с типом формального параметра. К аргументу могут быть применены некоторые тривиальные преобразования, а именно:
(Подробнее они рассмотрены ниже.)Категория соответствия с преобразованием типа
является наиболее сложной. Необходимо рассмотреть несколько видов такого приведения:
расширение типов (promotions), стандартные преобразования и определенные пользователем
преобразования. (Расширения типов и стандартные преобразования изучаются в этой
главе. Определенные пользователем преобразования будут представлены позднее,
после детального рассмотрения классов; они выполняются конвертером, функцией-членом,
которая позволяет определить в классе собственный набор “стандартных” трансформаций.
В главе 15 мы познакомимся с такими конвертерами и с тем, как они влияют на
разрешение перегрузки функций.) 9.3.1. Подробнее о точном соответствииСамый простой случай возникает тогда, когда типы фактических аргументов совпадают с типами формальных параметров. Например, есть две показанные ниже перегруженные функции max(). Тогда каждый из вызовов max() точно соответствует одному из объявлений: int max( int, int ); double max( double, double ); int i1; void calc( double d1 ) { Перечислимый тип точно соответствует только определенным в нем элементам перечисления, а также объектам, которые объявлены как принадлежащие к этому типу: enum Tokens { INLINE = 128; VIRTUAL = 129; }; Tokens curTok = INLINE; enum Stat { Fail, Pass }; extern void ff( Tokens ); Выше уже упоминалось, что фактический аргумент может точно соответствовать формальному параметру, даже если для приведения их типов необходимо некоторое тривиальное преобразование, первое из которых – преобразование l-значения в r-значение. Под l-значением понимается объект, удовлетворяющий следующим условиям:
Напротив, r-значение – это выражение, значение которого вычисляется, или выражение, обозначающее временный объект, для которого нельзя получить адрес и значение которого нельзя модифицировать. Вот простой пример: int calc( int ); int main() { int lval, res; lval = 5; // lvalue: lval; rvalue: 5 В первом операторе присваивания переменная lval – это l-значение, а литерал
5 – r-значение. Во втором операторе присваивания res – это l-значение, а временный
объект, в котором хранится результат, возвращаемый функцией calc(), – это r-значение. int obj1; int obj2; int main() { Здесь obj1 и obj2 – это l-значения. Однако для выполнения сложения в функции
main() из переменных obj1 и obj2 извлекаются их значения. Действие, состоящее
в извлечении значения объекта, представленного выражением вида l-значение, называется
преобразованием l-значения в r-значение. #include <string> string color( "purple" ); void print( string ); int main() { Так как аргумент в вызове print(color) передается по значению, то производится
преобразование l-значения в r-значение для извлечения значения color и передачи
его в функцию с прототипом print(string). Однако несмотря на то, что такое приведение
имело место, считается, что фактический аргумент color точно соответствует объявлению
print(string). #include <list> void print( list<int> & ); В вызове ниже li – это l-значение, представляющее объект list<int>, передаваемый функции print(): list<int> li(20); int main() { Сопоставление li с параметром-ссылкой считается точным соответствием. int ai[3]; void putValues(int *); int main() { Перед вызовом функции putValues() массив преобразуется в указатель, в результате
чего фактический аргумент ai (массив из трех целых) приводится к указателю на
int. Хотя формальным параметром функции putValues() является указатель и фактический
аргумент при вызове преобразован, между ними устанавливается точное соответствие. int lexicoCompare( const string &, const string & ); typedef int (*PFI)( const string &, const string & ); Перед вызовом sort() применяется преобразование функции в указатель, которое
приводит аргумент lexicoCompare от типа “функция” к типу “указатель на функцию”.
Хотя формальным параметром функции является указатель, а фактическим – имя функции
и, следовательно, было произведено преобразование функции в указатель, считается,
что фактический аргумент точно третьему формальному параметру функции sort(). int a[5] = { 4454, 7864, 92, 421, 938 }; int *pi = a; bool is_equal( const int * , const int * ); void func( int *parm ) { // точное соответствие между pi и parm: преобразование спецификаторов Перед вызовом функции is_equal() фактические аргументы pi и parm преобразуются
из типа “указатель на int” в тип “указатель на const int”. Эта трансформация
заключается в добавлении спецификатора const к адресуемому типу, поэтому относится
к категории преобразований спецификаторов. Несмотря на то, что функция ожидает
получить два указателя на const int, а фактические аргументы являются указателями
на int, считается, что точное соответствие между формальными и фактическими
параметрами функции is_equal() установлено. extern void takeCI( const int ); int main() { Хотя формальный параметр функции takeCI() имеет тип const int, а вызывается
она с аргументом ii типа int, преобразование спецификаторов не производится:
есть точное соответствие между фактическим аргументом и формальным параметром. extern void init( int *const ); extern int *pi; int main() { Спецификатор const при формальном параметре функции init() относится к самому
указателю, а не к типу, который он адресует. Поэтому компилятор при анализе
преобразований, которые должны быть применены к фактическому аргументу, не учитывает
этот спецификатор. К аргументу pi не применяется преобразование спецификатора:
считается, что этот аргумент и формальный параметр точно соответствуют друг
другу. 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. Подробнее о расширении типовПод расширением типа понимается одно из следующих преобразований:
Подобное расширение применяется, когда тип фактического аргумента совпадает с одним из только что перечисленных типов, а формальный параметр относится к соответствующему расширенному типу: extern void manip( int ); int main() { Символьный литерал имеет тип char. Он расширяется до int. Поскольку расширенный
тип соответствует типу формального параметра функции manip(), мы говорим, что
ее вызов требует расширения типа аргумента. extern void print( unsigned int ); extern void print( int ); extern void print( char ); unsigned char uc; Для аппаратной платформы, на которой unsigned char занимает один байт памяти,
а int – четыре байта, расширение преобразует unsigned char в int, так как с
его помощью можно представить все значения типа unsigned char. Для такой машинной
архитектуры из приведенного в примере множества перегруженных функций наилучшее
соответствие аргументу типа unsigned char обеспечивает print(int). Для двух
других функций установление соответствия требует стандартного приведения. enum Stat ( Fail, Pass ); extern void 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. #include <string> string format( int ); При первом обращении к format() фактический аргумент расширяется до типа int, так как для представления типа e1 используется char, и, следовательно, вызывается перегруженная функция format(int). При втором обращении тип фактического аргумента e2 представлен типом unsigned int и аргумент расширяется до unsigned int, из-за чего вызывается перегруженная функция format(unsigned int). Поэтому следует помнить, что поведение двух перечислений по отношению к процессу разрешения перегрузки может быть различным и зависеть от значений элементов, определяющих, как происходит расширение типа. 9.3.3. Подробнее о стандартном преобразованииИмеется пять видов стандартных преобразований, а именно:
Вот несколько примеров: extern void print( void* ); extern void print( double ); int main() { Преобразования, относящиеся к группам 1, 2 и 3, потенциально опасны, так как целевой тип может и не обеспечивать представления всех значений исходного. Например, с помощью float нельзя адекватно представить все значения типа int. Именно по этой причине трансформации, входящие в эти группы, отнесены к категории стандартных преобразований, а не расширений типов. int i; void calc( float ); int main() { calc( i ); // стандартное преобразование между целым типом и типом с // плавающей точкой потенциально опасно в зависимости от // значения i return 0; } При вызове функции calc() применяется стандартное преобразование из целого
типа int в тип с плавающей точкой float. В зависимости от значения переменной
i может оказаться, что его нельзя сохранить в типе float без потери точности. 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<long>( 3.14 ) ); // manip( long ) либо используя суффикс, обозначающий, что константа принадлежит к типу float: manip ( 3.14F ) ); // manip( float ) Вот еще несколько примеров неоднозначных вызовов, которые помечаются как ошибки, поскольку соответствуют нескольким перегруженным функциям: extern void farith( unsigned int ); extern void farith( float ); int main() { Стандартные преобразования указателей иногда противоречат интуиции. В частности, значение 0 приводится к указателю на любой тип; полученный таким образом указатель называется нулевым. Значение 0 может быть представлено как константное выражение целого типа: void set(int*); int main() { Константное выражение 0L (значение 0 типа long int) и константное выражение
0x00 (шестнадцатеричное целое значение 0) имеют целый тип и потому могут быть
преобразованы в нулевой указатель типа int*. enum EN { zr = 0 }; set( zr ); // ошибка: zr нельзя преобразовать в тип int* Вызов функции set() является ошибкой, так как не существует преобразования
между значением zr элемента перечисления и формальным параметром типа int*,
хотя zr равно 0. void print( int ); void print( void * ); void set( const char * ); При вызове print(int) имеет место точное соответствие, тогда как для вызова
print(void*) необходимо приведение значения 0 к типу указателя. Поскольку соответствие
лучше преобразования, для разрешения этого вызова выбирается функция print(int).
Обращение к set() неоднозначно, так как 0 соответствует формальным параметрам
обеих перегруженных функций за счет применения стандартной трансформации. Раз
обе функции одинаково хороши, фиксируется неоднозначность. #include <string> extern void reset( void * ); void func( int *pi, string *ps ) { Только указатели на типы данных могут быть приведены к типу void* с помощью стандартного преобразования, с указателями на функции так поступать нельзя: typedef int (*PFV)(); extern PFV testCases[10]; // массив указателей на функции extern void reset( void * ); int main() { 9.3.4. СсылкиФактический аргумент или формальный параметр функции могут быть ссылками. Как
это влияет на правила преобразования типов? int i; int& ri = i; void print( int ); int main() { Фактический аргумент в обоих вызовах имеет тип int. Использование ссылки для
его передачи во втором вызове не влияет на сам тип аргумента. int i; int& ri = i; void calc( double ); int main() { А как влияет на преобразования, применяемые к фактическому аргументу, формальный параметр-ссылка? Сопоставление дает следующие результаты:
В обоих случаях мы видим, что если формальный параметр-ссылка имеет спецификатор const, то между ним и фактическим аргументом может быть установлено точное соответствие. Следует отметить, что и преобразование l-значения в r-значение, и инициализация ссылки считаются точными соответствиями. В данном примере первый вызов функции приводит к ошибке: void print( int ); void print( int& ); int iobj; Объект iobj – это аргумент, для которого может быть установлено соответствие
с обеими функциями print(), то есть вызов неоднозначен. То же относится и к
следующей строке, где ссылка ri обозначает объект, соответствующий обеим функциям
print(). С третьим вызовом, однако, все в порядке. Для него print(int&)
не является устоявшей. Целая константа – это r-значение, так что она не может
инициализировать параметр-ссылку. Единственной устоявшей функцией для вызова
print(86) является print(int), поэтому она и выбирается при разрешении перегрузки. Упражнение 9.6Назовите два тривиальных преобразования, допустимых при установлении точного соответствия. Упражнение 9.7Каков ранг каждого из преобразований аргументов в следующих вызовах функций: (a) void print( int *, int ); int arr[6]; print( arr, 6 ); // вызов функции (b) void manip( int, int ); manip( 'a', 'z' ); // вызов функции (c) int calc( int, int ); double dobj; double = calc( 55.4, dobj ) // вызов функции (d) void set( const int * ); int *pi; set( pi ); // вызов функции Упражнение 9.8Какие из данных вызовов ошибочны из-за того, что не существует преобразования между типом фактического аргумента и формального параметра: (a) enum Stat { Fail, Pass }; void test( Stat ); text( 0 ); // вызов функции (b) void reset( void *); reset( 0 ); // вызов функции (c) void set( void * ); int *pi; set( pi ); // вызов функции (d) #include <list> list<int> oper(); void print( oper() ); // вызов функции (e) void print( const int ); int iobj; print( iobj ); // вызов функции 9.4. Детали разрешения перегрузки функцийВ разделе 9.2 мы уже упоминали, что процесс разрешения перегрузки функций состоит из трех шагов:
Теперь мы готовы к тому, чтобы изучить эти шаги более детально. 9.4.1. Функции-кандидатыФункцией-кандидатом называется функция, имеющая то же имя, что и вызванная. Кандидаты отыскиваются двумя способами:
Таким образом, совокупность кандидатов является объединением множества функций,
видимых в точке вызова, и множества функций, объявленных в том же пространстве
имен, к которому принадлежат типы фактических аргументов. char* format( int ); void g() { char *format( double ); char* format( char* ); format(3); // вызывается format( double ) Так как format(int), объявленная в глобальной области видимости, скрыта, она
не включается в множество функций-кандидатов. namespace libs_R_us { int max( int, int ); double max( double, double ); } char max( char, char ); void func() Функции max(), определенные в пространстве имен libs_R_us, невидимы в точке вызова. Единственной видимой является функция max() из глобальной области; только она входит в множество функций-кандидатов и вызывается при каждом из трех обращений к func(). Мы можем воспользоваться using-объявлением, чтобы сделать видимыми функции max() из пространства имен libs_R_us. Куда поместить using-объявление? Если включить его в глобальную область видимости: char max( char, char ); using libs_R_us::max; // using-объявление то функции max() из libs_R_us добавляются в множество перегруженных функций, которое уже содержит max(), объявленную в глобальной области. Теперь все три функции видны внутри func() и становятся кандидатами. В этой ситуации вызовы func() разрешаются следующим образом: void func() { max( 87, 65 ); // вызывается libs_R_us::max( int, int ) max( 35.5, 76.6 ); // вызывается libs_R_us::max( double, double ) max( 'J', 'L' ); // вызывается ::max( char, char ) } Но что будет, если мы введем using-объявление в локальную область видимости функции func(), как показано в данном примере? void func() { // using-объявление using libs_R_us::max; // те же вызовы функций, что и выше Какие из функций max() будут включены в множество кандидатов? Напомним, что using-объявления вкладываются друг в друга. При наличии такого объявления в локальной области глобальная функция max(char, char) оказывается скрытой, так что в точке вызова видны только libs_R_us::max( int, int ); libs_R_us::max( double, double ); Они и являются кандидатами. Теперь вызовы func() разрешаются следующим образом: void func() { // using-объявление // глобальная функция max( char, char ) скрыта using libs_R_us::max; max( 87, 65 ); // вызывается libs_R_us::max( int, int ) Using-директивы также оказывают влияние на состав множества функций-кандидатов. Предположим, мы решили их использовать, чтобы сделать функции max() из пространства имен libs_R_us видимыми в func(). Если разместить следующую using-директиву в глобальной области видимости, то множество функций-кандидатов будет состоять из глобальной функции max(char, char) и функций max(int, int) и max(double, double), объявленных в libs_R_us: namespace libs_R_us { int max( int, int ); double max( double, double ); } char max( char, char ); Что будет, если поместить using-директиву в локальную область видимости, как в следующем примере? void func() { // using-директива using namespace libs_R_us; // те же вызовы функций, что и выше Какие из функций max() окажутся среди кандидатов? Напомним, что using-директива делает члены пространства имен видимыми, словно они были объявлены вне этого пространства, в той точке, где такая директива помещается. В нашем примере члены libs_R_us видимы в локальной области функции func(), как будто они объявлены вне пространства – в глобальной области. Отсюда следует, что множество перегруженных функций, видимых внутри func(), то же, что и раньше, т.е. включает в себя max( char, char ); libs_R_us::max( int, int ); libs_R_us::max( double, double ); В локальной или глобальной области видимости появляется using-директива, на разрешение вызовов функции func() не влияет: void func() { using namespace libs_R_us; max( 87, 65 ); // вызывается libs_R_us::max( int, int ) Итак, множество кандидатов состоит из функций, видимых в точке вызова, включая и те, которые введены using-объявлениями и using-директивами, а также из функций, объявленных в пространствах имен, ассоциированных с типами фактических аргументов. Например: namespace basicLib { int print( int ); double print( double ); } namespace matrixLib { class matrix { /* ... */ }; void print( const maxtrix & ); } void display() { using basicLib::print; matrixLib::matrix mObj; Кандидатами для print(mObj) являются введенные using-объявлением внутри display() функции basicLib::print(int) и basicLib::print(double), поскольку они видимы в точке вызова. Так как фактический аргумент функции имеет тип matrixLib::matrix, то функция print(), объявленная в пространстве имен matrixLib, также будет кандидатом. Каковы функции-кандидаты для print(87)? Только basicLib::print(int) и basicLib::print(double), видимые в точке вызова. Поскольку аргумент имеет тип int, дополнительное пространство имен в поисках других кандидатов не рассматривается. 9.4.2. Устоявшие функцииУстоявшая функция относится к числу кандидатов. В списке ее формальных параметров
либо то же самое число элементов, что и в списке фактических аргументов вызванной
функции, либо больше. В последнем случае для дополнительных параметров задаются
значения по умолчанию, иначе функцию нельзя будет вызвать с данным числом аргументов.
Чтобы функция считалась устоявшей, должно существовать преобразование каждого
фактического аргумента в тип соответствующего формального параметра. (Такие
преобразования были рассмотрены в разделе 9.3.) void f(); void f( int ); void f( double ); void f( char*, char* ); int main() { Функция f(int) устояла, так как она имеет всего один формальный параметр, что
соответствует числу фактических аргументов в вызове. Кроме того, существует
стандартное преобразование аргумента типа double в int. Функция f(double) также
устояла; она тоже имеет один параметр типа double, и он точно соответствует
фактическому аргументу. Функции-кандидаты f() и f(char*, char*) исключены из
списка устоявших, так как они не могут быть вызваны с одним аргументом. char* format( int ); void g() { // глобальная функция format( int ) скрыта char* format( double ); char* format( char* ); format(3); // есть только одна устоявшая функция: format( double ) } В следующем примере все три функции-кандидата оказываются устоявшими для вызова max() внутри func(). Все они могут быть вызваны с двумя аргументами. Поскольку фактические аргументы имеют тип int, они точно соответствуют формальным параметрам функции libs_R_us::max(int, int) и могут быть приведены к типам параметров функции libs_R_us::max(double, double) с помощью трансформации целых в плавающие, а также к типам параметров функции libs_R_us::max(char, char) посредством преобразования целых типов. namespace libs_R_us { int max( int, int ); double max( double, double ); } // using-объявление Обратите внимание, что функция-кандидат с несколькими параметрами исключается из числа устоявших, как только выясняется, что один из фактических аргументов не может быть приведен к типу соответствующего формального параметра, пусть даже для всех остальных аргументов такое преобразование существует. В следующем примере функция min(char *, int) исключается из множества устоявших, поскольку нет возможности трансформации типа первого аргумента int в тип соответствующего параметра char *. И это происходит несмотря на то, что второй аргумент точно соответствует второму параметру. extern double min( double, double ); extern double min( char*, int ); void func() Если после исключения из множества кандидатов всех функций с несоответствующим числом параметров и тех, для параметров которых не оказалось подходящего преобразования, не осталось устоявших, то обработка вызова функции заканчивается ошибкой компиляции. В таком случае говорят, что соответствия не найдено. void print( unsigned int ); void print( char* ); void print( char ); int *ip; 9.4.3. Наилучшая из устоявших функцияНаилучшей считается та из устоявших функций, формальные параметры которой наиболее точно соответствуют типам фактических аргументов. Для любой такой функции преобразования типов, применяемые к каждому аргументу, ранжируются для определения степени его соответствия параметру. (В разделе 6.2 описаны поддерживаемые преобразования типов.) Наилучшей из устоявших называют функцию, для которой одновременно выполняются два условия:
Может оказаться так, что для приведения фактического аргумента к типу соответствующего формального параметра нужно выполнить несколько преобразований. Так, в следующем примере int arr[3]; void putValues(const int *); int main() { для приведения аргумента arr от типа “массив из трех int” к типу “указатель на const int” применяется последовательность преобразований:
Поэтому было бы более правильно говорить, что для приведения фактического аргумента
к типу формального параметра устоявшей функции требуется последовательность
преобразований. Поскольку применяется не одна, а несколько трансформаций, то
на третьем шаге процесса разрешения перегрузки функции на самом деле ранжируются
последовательности преобразований. преобразование l-значения -> расширение типа или стандартное преобразование -> преобразование спецификаторов Термин преобразование l-значения относится к первым трем трансформациям из
категории точных соответствий, рассмотренных в разделе 9.2: преобразование l-значения
в r-значение, преобразование массива в указатель и преобразование функции в
указатель. Последовательность трансформаций состоит из нуля или одного преобразования
l-значения, за которым следует нуль или одно расширение типа или стандартное
преобразование, и наконец нуль или одно преобразование спецификаторов. Для приведения
фактического аргумента к типу формального параметра может быть применено только
одна трансформация каждого вида. namespace libs_R_us { int max( int, int ); double max( double, double ); } // using-объявление Аргументы в вызове функции max() имеют тип char. Последовательность преобразований
аргументов при вызове функции libs_R_us::max(int,int) следующая:
Поскольку нельзя сказать, какая из этих последовательностей лучше другой, вызов неоднозначен: int i, j; extern long calc( long, long ); extern double calc( double, double ); void jj() { // ошибка: неоднозначность, нет наилучшего соответствия Преобразование спецификаторов (добавление спецификатора const или volatile к типу, который адресует указатель) имеет ранг точного соответствия. Однако, если две последовательности трансформаций отличаются только тем, что в конце одной из них есть дополнительное преобразование спецификаторов, то последовательность без него считается лучше. Например: void reset( int * ); void reset( const int * ); int* pi; int main() { Последовательность стандартных преобразований, примененная к фактическому аргументу
для первой функции-кандидата reset(int*), – это точное соответствие, требуется
лишь переход от l-значения к r-значению, чтобы извлечь значение аргумента. Для
второй функции-кандидата reset(const int *) также применяется трансформация
l-значения в r-значение, но за ней следует еще и преобразование спецификаторов
для приведения результирующего значения от типа “указатель на int” к типу “указатель
на const int”. Обе последовательности представляют собой точное соответствие,
но неоднозначности при этом не возникает. Так как вторая последовательность
отличается от первой наличием трансформации спецификаторов в конце, то последовательность
без такого преобразования считается лучшей. Поэтому наилучшей из устоявших функций
будет reset(int*). int* pi; int main() { #include <vector> void manip( vector<int> & ); void manip( const vector<int> & ); vector<int> f(); В первом вызове инициализация ссылок для вызова любой функции является точным
соответствием. Но этот вызов все же не будет неоднозначным. Так как обе инициализации
одинаковы во всем, кроме наличия дополнительной спецификации const во втором
случае, то инициализация без такой спецификации считается лучше, поэтому перегрузка
будет разрешена в пользу устоявшей функции manip(vector<int>&). extern int ff( char*, int ); extern int ff( int, int ); int main() { ff( 0, 'a' ); // ff( int, int ) Функция ff(), принимающая два аргумента типа int, выбирается в качестве наилучшей из устоявших по следующим причинам:
Вот еще один пример: int compute( const int&, short ); int compute( int&, double ); extern int iobj; Обе функции compute( const int&, short ) и compute( int&, double ) устояли. Вторая выбирается в качестве наилучшей по следующим причинам:
9.4.4. Аргументы со значениями по умолчаниюНаличие аргументов со значениями по умолчанию способно расширить множество устоявших функций. Устоявшими являются функции, которые вызываются с данным списком фактических аргументов. Но такая функция может иметь больше формальных параметров, чем задано фактических аргументов, в том случае, когда для каждого неуказанного параметра есть некое значение по умолчанию: extern void ff( int ); extern void ff( long, int = 0 ); int main() { Для первого и третьего вызовов функция ff() является устоявшей, хотя передан всего один фактический аргумент. Это обусловлено следующими причинами:
Последний вызов является неоднозначным, поскольку обе устоявших функции могут быть выбраны, если применить стандартное преобразование к первому аргументу. Функции ff(int) не отдается предпочтение только потому, что у нее один параметр. Упражнение 9.9Объясните, что происходит при разрешении перегрузки для вызова функции compute() внутри main(). Какие функции являются кандидатами? Какие из них устоят после первого шага? Какие последовательности преобразований надо применить к фактическому аргументу, чтобы он соответствовал формальному параметру для каждой устоявшей функции? Какая функция будет наилучшей из устоявших? namespace primerLib { void compute(); void compute( const void * ); } using primerLib::compute; Что будет, если using-объявление поместить внутрь main() перед вызовом compute()? Ответьте на те же вопросы. Назад ВпередСодержание |
2016-01-21 17:05:38 димка фыв 2017-01-12 19:07:10 Дмитрий Спасибо. Нашёл что искал. 2019-04-29 23:56:47 Дмитрий Спасибо, громоздко, но полезно и полно. Оставить комментарий: |