<< Пред.           стр. 42 (из 121)           След. >>

Список литературы по разделу

 Первое 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). Если разрешить пользователю избирательно включать в область видимости лишь одну из нескольких перегруженных функций, то поведение программы становится непредсказуемым.
 Что происходит, если using-объявление вводит в область видимости функцию с уже существующим именем? Эти функции выглядят так, как будто они объявлены прямо в том месте, где встречается using-объявление. Поэтому введенные функции участвуют в процессе разрешения имен всех перегруженных функций, присутствующих в данной области видимости:
 #include
 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 )
 // перегружают print( const string & )
 using libs_R_us::print;
 
 void fooBar( int ival )
 {
  print( "Value: "); // вызывает глобальную функцию
  // print( const string & )
  print( ival ); // вызывает libs_R_us::print( int )
 }
 Using-объявление добавляет в глобальную область видимости два объявления: для print(int) и для print(double). Они являются псевдонимами в пространстве libs_R_us и включаются в множество перегруженных функций с именем print, где уже находится глобальная print(const string &). При разрешении перегрузки print в fooBar рассматриваются все три функции.
 Если using-объявление вводит некоторую функцию в область видимости, в которой уже имеется функция с таким же именем и таким же списком параметров, это считается ошибкой. С помощью using-объявления нельзя задать псевдоним для функции print(int) в пространстве имен libs_R_us, если в глобальной области видимости уже есть print(int). Например:
 namespace libs_R_us {
  void print( int );
  void print( double );
 }
 
 void print( int );
 
 using libs_R_us::print; // ошибка: повторное объявление print(int)
 
 void fooBar( int ival )
 {
  print( ival ); // какая print? ::print или libs_R_us::print
 }
 Мы показали, как связаны using-объявления и перегруженные функции. Теперь рассмотрим особенности применения using-директивы. Using-директива приводит к тому, что члены пространства имен выглядят объявленными вне этого пространства, добавляя их в новую область видимости. Если в этой области уже есть функция с тем же именем, то происходит перегрузка. Например:
 #include
 namespace libs_R_us {
  extern void print( int );
  extern void print( double );
 }
 
 extern void print( const string & );
 
 // using-директива
 // print(int), print(double) и print(const string &) - элементы
 // одного и того же множества перегруженных функций
 using namespace libs_R_us;
 
 void fooBar( int ival )
 {
  print( "Value: "); // вызывает глобальную функцию
  // print( const string & )
  print( ival ); // вызывает libs_R_us::print( int )
 }
 Это верно и в том случае, когда есть несколько 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(1); // вызывается IBM::print(int)
  print(3.1); // вызывается Disney::print(double)
  return 0;
 }
 Множество перегруженных функций с именем print в глобальной области видимости включает функции print(int), print(double) и print(long double). Все они рассматриваются в main() при разрешении перегрузки, хотя первоначально были определены в разных пространствах имен.
 Итак, повторим, что перегруженные функции находятся в одной и той же области видимости. В частности, они оказываются там в результате применения using-объявлений и using-директив, делающих доступными имена из других областей.
 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, так и из программы, написанной на C++.
 // функции C++ обрабатывают параметры, являющиеся классами
 extern "C" double calc( double );
 extern SmallInt calc( const SmallInt& );
 extern BigNum calc( const BigNum& );
 Написанная на 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 );
 extern void ff( unsigned int );
 
 // на какую функцию указывает pf1?
 void ( *pf1 )( unsigned int ) = &ff;
 Поскольку функция ff() перегружена, одного инициализатора &ff недостаточно для выбора правильного варианта. Чтобы понять, какая именно функция инициализирует указатель, компилятор ищет в множестве всех перегруженных функций ту, которая имеет тот же тип возвращаемого значения и список параметров, что и функция, на которую ссылается указатель. В нашем случае будет выбрана функция ff(unsigned int).
 А что если не найдется функции, в точности соответствующей типу указателя? Тогда компилятор выдаст сообщение об ошибке:
 extern void ff( vector );
 extern void ff( unsigned int );
 
 // ошибка: соответствие не найдено: неверный список параметров
 void ( *pf2 )( int ) = &ff;
 // ошибка: соответствие не найдено: неверный тип возвращаемого значения
 double ( *pf3 )( vector ) = &ff;
 Присваивание работает аналогично. Если значением указателя должен стать адрес перегруженной функции , то для выбора операнда в правой части оператора присваивания используется тип указателя на функцию. И если компилятор не находит функции, в точности соответствующей нужному типу, он выдает сообщение об ошибке. Таким образом, преобразование типов между указателями на функции никогда не производится.
 matrix calc( const matrix & );
 int calc( int, int );
 
 int ( *pc1 )( int, int ) = 0;
 int ( *pc2 )( int, double ) = 0;
 
 // ...
 // правильно: выбирается функция calc( int, int )
 pc1 = &calc;
 
 // ошибка: нет соответствия: неверный тип второго параметра
 pc2 = &calc;
 9.1.7. Безопасное связывание A
 При использовании перегрузки складывается впечатление, что в программе можно иметь несколько одноименных функций с разными списками параметров. Однако это лексическое удобство существует только на уровне исходного текста. В большинстве систем компиляции программы, обрабатывающие этот текст для получения исполняемого кода, требуют, чтобы все имена были различны. Редакторы связей, как правило, разрешают внешние ссылки лексически. Если такой редактор встречает имя print два или более раз, он не может различить их путем анализа типов (к этому моменту информация о типах обычно уже потеряна). Поэтому он просто печатает сообщение о повторно определенном символе print и завершает работу.
 Чтобы разрешить эту проблему, имя функции вместе с ее списком параметров декорируется так, чтобы получилось уникальное внутреннее имя. Вызываемые после компилятора программы видят только это внутреннее имя. Как именно производится такое преобразование имен, зависит от реализации. Общая идея заключается в том, чтобы представить число и типы параметров в виде строки символов и дописать ее к имени функции.
 Как было сказано в разделе 8.2, такое кодирование гарантирует, в частности, что два объявления одноименных функций с разными списками параметров, находящиеся в разных файлах, не воспринимаются редактором связей как объявления одной и той же функции. Поскольку этот способ помогает различить перегруженные функции на фазе редактирования связей, мы говорим о безопасном связывании.
 Декорирование имен не применяется к функциям, объявленным с помощью директивы extern "C", так как лишь одна из множества перегруженных функций может быть написана на чистом С. Две функции с различными списками параметров, объявленные как extern "C", редактор связей воспринимает как один и тот же символ.
 Упражнение 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() {
  f( t1, t2 );
  return 0;
 }
 Здесь в ходе процесса разрешения перегрузки в зависимости от типа T определяется, будет ли при обработке выражения f(t1,t2) вызвана функция f(int,int) или f(float,float) или зафиксируется ошибка.
 Разрешение перегрузки функции – один и самых сложных аспектов языка C++. Пытаясь разобраться во всех деталях, начинающие программисты столкнутся с серьезными трудностями. Поэтому в данном разделе мы представим лишь краткий обзор того, как происходит разрешение перегрузки, чтобы у вас составилось хоть какое-то впечатление об этом процессе. Для тех, кто хочет узнать больше, в следующих двух разделах приводится более подробное описание.
 Процесс разрешения перегрузки функции состоит из трех шагов, которые мы покажем на следующем примере:
 void f();
 void f( int );
 void f( double, double = 3.4 );
 void f( char *, char * );
 
 void main() {
  f( 5.6 );
  return 0;
 }
 При разрешении перегрузки функции выполняются следующие шаги:
 Выделяется множество перегруженных функций для данного вызова, а также свойства списка аргументов, переданных функции.
 Выбираются те из перегруженных функций, которые могут быть вызваны с данными аргументами, с учетом их количества и типов.
 Находится функция, которая лучше всего соответствует вызову.
 Рассмотрим последовательно каждый пункт.
 На первом шаге необходимо идентифицировать множество перегруженных функций, которые будут рассматриваться при данном вызове. Вошедшие в это множество функции называются кандидатами. Функция-кандидат – это функция с тем же именем, что и вызванная, причем ее объявление видимо в точке вызова. В нашем примере есть четыре таких кандидата: f(), f(int), f(double, double) и f(char*, char*).
 После этого идентифицируются свойства списка переданных аргументов, т.е. их количество и типы. В нашем примере список состоит из двух аргументов типа double.
 На втором шаге среди множества кандидатов отбираются устоявшие (viable) – такие, которые могут быть вызваны с данными аргументами, Устоявшая функция либо имеет столько же формальных параметров, сколько фактических аргументов передано вызванной функции, либо больше, но тогда для каждого дополнительного параметра должно быть задано значение по умолчанию. Чтобы функция считалась устоявшей, для любого фактического аргумента, переданного при вызове, обязано существовать преобразование к типу формального параметра, указанного в объявлении.
 В нашем примере есть две устоявших функции, которые могут быть вызваны с приведенными аргументами:
 функция f(int) устояла, потому что у нее есть всего один параметр и существует преобразование фактического аргумента типа double к формальному параметру типа int;
 функция f(double,double) устояла, потому что для второго аргумента есть значение по умолчанию, а первый формальный параметр имеет тип double, что в точности соответствует типу фактического аргумента.
 Если после второго шага не нашлось устоявших функций, то вызов считается ошибочным. В таких случаях мы говорим, что имеет место отсутствие соответствия.
 Третий шаг заключается в выборе функции, лучше всего отвечающей контексту вызова. Такая функция называется наилучшей из устоявших (или наиболее подходящей). На этом шаге производится ранжирование преобразований, использованных для приведения типов фактических аргументов к типам формальных параметров устоявшей функции. Наиболее подходящей считается функция, для которой выполняются следующие условия:
 преобразования, примененные к фактическим аргументам, не хуже преобразований, необходимых для вызова любой другой устоявшей функции;
 для некоторых аргументов примененные преобразования лучше, чем преобразования, необходимые для приведения тех же аргументов в вызове других устоявших функций.
 Преобразования типов и их ранжирование более подробно обсуждаются в разделе 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( unsigned int );
 void print( const char* );
 void print( char );
 то каждый из следующих трех вызовов дает точное соответствие:
 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() соответствия нет:
 // функции print() объявлены так же, как и выше
 int *ip;
 class SmallInt { /* ... */ };
 SmallInt si;
 
 print( ip ); // ошибка: нет соответствия
 print( si ); // ошибка: нет соответствия
 Для установления точного соответствия тип фактического аргумента необязательно должен совпадать с типом формального параметра. К аргументу могут быть применены некоторые тривиальные преобразования, а именно:
 преобразование l-значения в r-значение;
 преобразование массива в указатель;
 преобразование функции в указатель;
 преобразования спецификаторов.
 (Подробнее они рассмотрены ниже.)
 Категория соответствия с преобразованием типа является наиболее сложной. Необходимо рассмотреть несколько видов такого приведения: расширение типов (promotions), стандартные преобразования и определенные пользователем преобразования. (Расширения типов и стандартные преобразования изучаются в этой главе. Определенные пользователем преобразования будут представлены позднее, после детального рассмотрения классов; они выполняются конвертером, функцией-членом, которая позволяет определить в классе собственный набор “стандартных” трансформаций. В главе 15 мы познакомимся с такими конвертерами и с тем, как они влияют на разрешение перегрузки функций.)
 При выборе лучшей из устоявших функций для данного вызова компилятор ищет функцию, для которой применяемые к фактическим аргументам преобразования являются “наилучшими”. Преобразования типов ранжируются следующим образом: точное соответствие лучше расширения типа, расширение типа лучше стандартного преобразования, а оно, в свою очередь, лучше определенного пользователем преобразования. Мы еще вернемся к ранжированию в разделе 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( Pass ); // точно соответствует ff( Stat )
  ff( 0 ); // точно соответствует ff( int )
  ff( curTok ); // точно соответствует ff( Tokens )
  // ...
 }
 Выше уже упоминалось, что фактический аргумент может точно соответствовать формальному параметру, даже если для приведения их типов необходимо некоторое тривиальное преобразование, первое из которых – преобразование l-значения в r-значение. Под l-значением понимается объект, удовлетворяющий следующим условиям:
 можно получить адрес объекта;
 можно получить значение объекта;
 это значение легко модифицировать (если только в объявлении объекта нет спецификатора 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-значение.
 В некоторых ситуациях в контексте, где ожидается значение, можно использовать выражение, представляющее собой l-значение:
 int obj1;
 int obj2;
 
 int main() {

<< Пред.           стр. 42 (из 121)           След. >>

Список литературы по разделу