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

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

 При разрешении имени h() в определении функции-члена mf() сначала просматриваются функции-члены myClass. Поскольку функции-члена с таким именем в области видимости этого класса нет, то далее поиск идет в пространстве имен NS. Функции h()нет и там, поэтому мы переходим в глобальную область видимости. Результат – глобальная функция h(char), единственная функция-кандидат, видимая в точке вызова.
 Как только найдено подходящее объявление, поиск прекращается. Следовательно, множество содержит только те функции, объявления которых находятся в областях видимости, где разрешение имени завершилось успешно. Это можно наблюдать на примере построения множества кандидатов для вызова
 k( 4 );
 Сначала поиск ведется в области видимости класса myClass. При этом найдены две функции-члена k(int) и k(char*). Поскольку множество кандидатов содержит лишь функции, объявленные в той области, где разрешение успешно завершилось, то пространство имен NS не просматривается и функция k(double) в данное множество не включается.
 Если обнаруживается, что вызов неоднозначен, поскольку в множестве нет наиболее подходящей функции, то компилятор выдает сообщение об ошибке. Поиск кандидатов, лучше соответствующих фактическим аргументам, в объемлющих областях видимости не производится.
 15.10.4. Ранжирование последовательностей определенных пользователем преобразований
 Фактический аргумент функции может быть неявно приведен к типу формального параметра с помощью последовательности определенных пользователем преобразований. Как это влияет на разрешение перегрузки? Например, если имеется следующий вызов calc(), то какая функция будет вызвана?
 class SmallInt {
 public:
  SmallInt( int );
 };
 
 extern void calc( double );
 extern void calc( SmallInt );
 int ival;
 
 int main() {
  calc( ival ); // какая calc() вызывается?
 }
 Выбирается функция, формальные параметры которой лучше всего соответствуют типам фактических аргументов. Она называется лучшим соответствием или наилучшей из устоявших функций. Для выбора такой функции неявные преобразования, примененные к фактическим аргументам, подвергаются ранжированию. Лучшей из устоявших считается та, для которой примененные к аргументам изменения не хуже, чем для любой другой устоявшей, а хотя бы для одного аргумента они лучше, чем для всех остальных функций.
 Последовательность стандартных преобразований всегда лучше последовательности определенных пользователем преобразований. Так, при вызове calc() из примера выше обе функции calc() являются устоявшими. calc(double) устояла потому, что существует стандартное преобразование типа фактического аргумента int в тип формального параметра double, а calc(SmallInt) – потому, что имеется определенное пользователем преобразование из int в SmallInt, которое использует конструктор SmallInt(int). Следовательно, наилучшей из устоявших функций будет calc(double).
 А как сравниваются две последовательности определенных пользователем преобразований? Если в них используются разные конвертеры или разные конструкторы, то обе такие последовательности считаются одинаково хорошими:
 class Number {
 public:
  operator SmallInt();
  operator int();
  // ...
 };
 
 extern void calc( int );
 extern void calc( SmallInt );
 extern Number num;
 
 calc( num ); // ошибка: неоднозначность
 Устоявшими окажутся и calc(int), и calc(SmallInt); первая – поскольку конвертер Number::operator int()преобразует фактический аргумент типа Number в формальный параметр типа int, а вторая потому, что конвертер Number::operator SmallInt() преобразует фактический аргумент типа Number в формальный параметр типа SmallInt. Так как последовательности определенных пользователем преобразований всегда имеют одинаковый ранг, то компилятор не может выбрать, какая из них лучше. Таким образом, этот вызов функции неоднозначен и приводит к ошибке компиляции.
 Есть способ разрешить неоднозначность, указав преобразование явно:
 // явное указание преобразования устраняет неоднозначность
 calc( static_cast< int >( num ) );
 Явное приведение типов заставляет компилятор преобразовать аргумент num в тип int с помощью конвертера Number::operator int(). Фактический аргумент тогда будет иметь тип int, что точно соответствует функции calc(int), которая и выбирается в качестве наилучшей.
 Допустим, в классе Number не определен конвертер Number::operator int(). Будет ли тогда вызов
 // определен только Number::operator SmallInt()
 calc( num ); // по-прежнему неоднозначен?
 по-прежнему неоднозначен? Вспомните, что в SmallInt также есть конвертер, способный преобразовать значение типа SmallInt в int.
 class SmallInt {
 public:
  operator int();
  // ...
 };
 Можно предположить, что функция calc() вызывается, если сначала преобразовать фактический аргумент num из типа Number в тип SmallInt с помощью конвертера Number::operator SmallInt(), а затем результат привести к типу int с помощью SmallInt::operator SmallInt(). Однако это не так. Напомним, что в последовательность определенных пользователем преобразований может входит несколько стандартных преобразований, но лишь одно пользовательское. Если конвертер Number::operator int() не определен, то функция calc(int) не считается устоявшей, поскольку не существует неявного преобразования из типа фактического аргумента num в тип формального параметра int.
 Поэтому в отсутствие конвертера Number::operator int() единственной устоявшей функцией будет calc(SmallInt), в пользу которой и разрешается вызов.
 Если в двух последовательностях определенных пользователем преобразований употребляется один и тот же конвертер, то выбор наилучшей зависит от последовательности стандартных преобразований, выполняемых после его вызова:
 class SmallInt {
 public:
  operator int();
  // ...
 };
 void manip( int );
 void manip( char );
 
 SmallInt si ( 68 );
 
 main() {
  manip( si ); // вызывается manip( int )
 }
 Как manip(int), так и manip(char) являются устоявшими функциями; первая – потому, что конвертер SmallInt::operator int() преобразует фактический аргумент типа SmallInt в тип формального параметра int, а вторая – потому, что тот же конвертер преобразует SmallInt в int, после чего результат с помощью стандартного преобразования приводится к типу char. Последовательности определенных пользователем преобразований выглядят так:
 manip(int) : operator int()->точное соответствие
 manip(int) : operator int()->стандартное преобразование
 Поскольку в обеих последовательностях используется один и тот же конвертер, то для определения лучшей из них анализируется ранг последовательности стандартных преобразований. Так как точное соответствие лучше преобразования, то наилучшей из устоявших будет функция manip(int).
 Подчеркнем, что такой критерий выбора принимается только тогда, когда в обеих последовательностях определенных пользователем преобразований применяется один и тот же конвертер. Этим наш пример отличается от приведенных в конце раздела 15.9, где мы показывали, как компилятор выбирает пользовательское преобразование некоторого значения в данный целевой тип: исходный и целевой типы были фиксированы, и компилятору приходилось выбирать между различными определенными пользователем преобразованиями одного типа в другой. Здесь же рассматриваются две разных функции с разными типами формальных параметров, и целевые типы отличаются. Если для двух разных типов параметров нужны различные определенные пользователем преобразования, то предпочесть один тип другому возможно только в том случае, когда в обеих последовательностях используется один и тот же конвертер. Если это не так, то для выбора наилучшего целевого типа оцениваются стандартные преобразования, следующие за применением конвертера. Например:
 class SmallInt {
 public:
  operator int();
  operator float();
  // ...
 };
 void compute( float );
 void compute( char );
 
 SmallInt si ( 68 );
 
 main() {
  compute( si ); // неоднозначность
 }
 И compute(float), и compute(int) – устоявшие функции. compute(float) – потому, что конвертер SmallInt::operator float()преобразует аргумент типа SmallInt в тип параметра float, а compute(char) – потому, что SmallInt::operator int() преобразует аргумент типа SmallInt в тип int, после чего результат стандартно приводится к типу char. Таким образом, имеются последовательности:
 compute(float) : operator float()->точное соответствие
 compute(char) : operator char()->стандартное преобразование
 Поскольку в них применяются разные конвертеры, то невозможно определить, у какой функции формальные параметры лучше соответствуют вызову. Для выбора лучшей из двух ранг последовательности стандартных преобразований не используется. Вызов помечается компилятором как неоднозначный.
 Упражнение 15.12
 В классах стандартной библиотеки C++ нет определений конвертеров, а большинство конструкторов, принимающих один параметр, объявлены явными. Однако определено множество перегруженных операторов. Как вы думаете, почему при проектировании было принято такое решение?
 Упражнение 15.13
 Почему перегруженный оператор ввода для класса SmallInt, определенный в начале этого раздела, реализован не так:
 istream& operator>>( istream &is, SmallInt &si )
 {
  return ( is >> is.value );
 }
 Упражнение 15.14
 Приведите возможные последовательности определенных пользователем преобразований для следующих инициализаций. Каким будет результат каждой инициализации?
 class LongDouble {
  operator double();
  operator float();
 };
 
 extern LongDouble ldObj;
 (a) int ex1 = ldObj;
 (b) float ex2 = ldObj;
 Упражнение 15.15
 Назовите три множества функций-кандидатов, рассматриваемых при разрешении перегрузки функции в случае, когда хотя бы один ее аргумент имеет тип класса.
 Упражнение 15.16
 Какая из функций calc() выбирается в качестве наилучшей из устоявших в данном случае? Покажите последовательности преобразований, необходимых для вызова каждой функции, и объясните, почему одна из них лучше другой.
 class LongDouble {
 public:
  LongDouble( double );
  // ...
 };
 
 extern void calc( int );
 extern void calc( LongDouble );
 double dval;
 
 int main() {
  calc( dval ); // какая функция?
 }
 15.11. Разрешение перегрузки и функции-члены A
 Функции-члены также могут быть перегружены, и в этом случае тоже применяется процедура разрешения перегрузки для выбора наилучшей из устоявших. Такое разрешение очень похоже на аналогичную процедуру для обычных функций и состоит из тех же трех шагов:
 Отбор функций-кандидатов.
 Отбор устоявших функций.
 Выбор наилучшей из устоявших функции.
 Однако есть небольшие различия в алгоритмах формирования множества кандидатов и отбора устоявших функций-членов. Эти различия мы и рассмотрим в настоящем разделе.
 15.11.1. Объявления перегруженных функций-членов
 Функции-члены класса можно перегружать:
 class myClass {
 public:
  void f( double );
 char f( char, char ); // перегружает myClass::f( double )
  // ...
 };
 Как и в случае функций, объявленных в пространстве имен, функции-члены могут иметь одинаковые имена при условии, что списки их параметров различны либо по числу параметров, либо по их типам. Если же объявления двух функций-членов отличаются только типом возвращаемого значения, то второе объявление считается ошибкой компиляции:
 class myClass {
 public:
  void mf();
  double mf(); // ошибка: так перегружать нельзя
  // ...
 };
 В отличие от функций в пространствах имен, функции-члены должны быть объявлены только один раз. Если даже тип возвращаемого значения и списки параметров двух функций-членов совпадают, то второе объявление компилятор трактует как неверное повторное объявление:
 class myClass {
 public:
  void mf();
  void mf(); // ошибка: повторное объявление
  // ...
 };
 Все функции из множества перегруженных должны быть объявлены в одной и той же области видимости. Поэтому функции-члены никогда не перегружают функций, объявленных в пространстве имен. Кроме того, поскольку у каждого класса своя область видимости, функции, являющиеся членами разных классов, не перегружают друг друга.
 Множество перегруженных функций-членов может содержать как статические, так и нестатические функции:
 class myClass {
 public:
  void mcf( double );
  static void mcf( int* ); // перегружает myClass::mcf( double )
  // ...
 };
 Какая из функций-членов будет вызвана – статическая или нестатическая – зависит от результатов разрешения перегрузки. Процесс разрешения в ситуации, когда устояли как статические, так и нестатические члены, мы подробно рассмотрим в следующем разделе.
 15.11.2. Функции-кандидаты
 Рассмотрим два вида вызовов функции-члена:
 mc.mf( arg );
 pmc->mf( arg );
 где mc – выражение типа myClass, а pmc – выражение типа “указатель на тип myClass”. Множество кандидатов для обоих вызовов составлено из функций, найденных в области видимости класса myClass при поиске объявления mf().
 Аналогично для вызова функции вида
 myClass::mf( arg );
 множество кандидатов также состоит из функций, найденных в области видимости класса myClass при поиске объявления mf(). Например:
 class myClass {
 public:
  void mf( double );
  void mf( char, char = '\n' );
  static void mf( int* );
  // ...
 };
 
 int main() {
  myClass mc;
  int iobj;
  mc.mf( iobj );
 }
 Кандидатами для вызова функции в main() являются все три функции-члена mf(), объявленные в myClass:
 void mf( double );
 void mf( char, char = '\n' );
 static void mf( int* );
 Если бы в myClass не было объявлено ни одной функции-члена с именем mf(), то множество кандидатов оказалось бы пустым. (На самом деле рассматривались бы также и функции из базовых классов. О том, как они попадают в это множество, мы поговорим в разделе 19.3.) Если для вызова функции не оказывается кандидатов, компилятор выдает сообщение об ошибке.
 15.11.3. Устоявшие функции
 Устоявшей называется функция из множества кандидатов, которая может быть вызвана с данными фактическими аргументами. Чтобы она устояла, должны существовать неявные преобразования между типами фактических аргументов и формальных параметров. Например:
 class myClass {
 public:
  void mf( double );
  void mf( char, char = '\n' );
  static void mf( int* );
  // ...
 };
 
 int main() {
  myClass mc;
  int iobj;
  mc.mf( iobj ); // какая именно функция-член mf()? Неоднозначно
 }
 В этом фрагменте для вызова mf() из main() есть две устоявшие функции:
 void mf( double );
 void mf( char, char = '\n' );
 mf(double) устояла потому, что у нее только один параметр и существует стандартное преобразование аргумента iobj типа int в параметр типа double;
 mf(char,char) устояла потому, что для второго параметра имеется значение по умолчанию и существует стандартное преобразование аргумента iobj типа int в тип char первого формального параметра.
 При выборе наилучшей из устоявших функции преобразования типов, применяемые к каждому фактическому аргументу, ранжируются. Лучшей считается та, для которое все использованные преобразования не хуже, чем для любой другой устоявшей функции, и хотя бы для одного аргумента такое преобразование лучше, чем для всех остальных функций.
 В предыдущем примере в каждой из двух устоявших функций для приведения типа фактического аргумента к типу формального параметра применено стандартное преобразование. Вызов считается неоднозначным, так как обе функции-члена разрешают его одинаково хорошо.
 Независимо от вида вызова функции, в множество устоявших могут быть включены как статические, так и нестатические члены:
 class myClass {
 public:
  static void mf( int );
  char mf( char );
 };
 
 int main() {
  char cobj;
  myClass::mf( cobj ); // какая именно функция-член?
 }
 Здесь функция-член mf() вызывается с указанием имени класса и оператора разрешения области видимости myClass::mf(). Однако не задан ни объект (с оператором “точка”), ни указатель на объект (с оператором “стрелка”). Несмотря на это, нестатическая функция-член mf(char) все же включается в множество устоявших наряду со статическим членом mf(int).
 Затем процесс разрешения перегрузки продолжается: на основе ранжирования преобразований типов, примененных к фактическим аргументам, чтобы выбрать наилучшую из устоявших функций. Аргумент cobj типа char точно соответствует формальному параметру mf(char) и может быть расширен до типа формального параметра mf(int). Поскольку ранг точного соответствия выше, то выбирается функция mf(char).
 Однако эта функция-член не является статической и, следовательно, вызывается только через объект или указатель на объект класса myClass с помощью одного из операторов доступа. В такой ситуации, если объект не указан и, значит, вызов функции невозможен (как раз наш случай), компилятор считает его ошибкой.
 Еще одна особенность функций-членов, которую надо принимать во внимание при формировании множества устоявших функций, – это наличие спецификаторов const или volatile у нестатических членов. (Они рассматривались в разделе 13.3.) Как они влияют на процесс разрешения перегрузки? Пусть в классе myClass есть следующие функции-члены:
 class myClass {
 public:
  static void mf( int* );
  void mf( double );
  void mf( int ) const;
  // ...
 };
 Тогда и статическая функция-член mf(int*), и константная функция mf(int), и неконстантная функция mf(double) включаются в множество кандидатов для показанного ниже вызова. Но какие из них войдут в множество устоявших?
 int main() {
  const myClass mc;
  double dobj;
  mc.mf( dobj ); // какая из функций-членов mf()?
 }
 Исследуя преобразования, которые надо применить к фактическим аргументам, мы обнаруживаем, что устояли функции mf(double) и mf(int). Тип double фактического аргумента dobj точно соответствует типу формального параметра mf(double) и может быть приведен к типу параметра mf(int) с помощью стандартного преобразования.
 Если при вызове функции-члена используются операторы доступа “точка” или “стрелка”, то при отборе функций в множество устоявших принимается во внимание тип объекта или указателя, для которого вызвана функция.
 mc – это константный объект, для которого можно вызывать только нестатические константные функции-члены. Следовательно, неконстантная функция-член mf(double) исключается из множества устоявших, и остается в нем единственная функция mf(int), которая и вызывается.
 А если константный объект использован для вызова статической функции-члена? Ведь для такой функции нельзя задавать спецификатор const или volatile, так можно ли ее вызывать через константный объект?
 class myClass {
 public:
  static void mf( int );
  char mf( char );
 };
 int main() {
  const myClass mc;
  int iobj;
  mc.mf( iobj ); // можно ли вызывать статическую функцию-член?
 }
 Статические функции-члены являются общими для всех объектов одного класса. Напрямую они могут обращаться только к статическим членам класса. Так, нестатические члены константного объекта mc недоступны статической mf(int). По этой причине разрешается вызывать статическую функцию-член для константного объекта с помощью операторов “точка” или “стрелка”.
 Таким образом, статические функции-члены не исключаются из множества устоявших и при наличии спецификаторов const или volatile у объекта, для которого они вызваны. Статические функции-члены рассматриваются как соответствующие любому объекту или указателю на объект своего класса.
 В примере выше mc – константный объект, поэтому функция-член mf(char) исключается из множества устоявших. Но функция-член mf(int) в нем остается, так как является статической. Поскольку это единственная устоявшая функция, она и оказывается наилучшей.
 
 15.12. Разрешение перегрузки и операторы A
 В классах могут быть объявлены перегруженные операторы и конвертеры. Предположим, при инициализации встретился оператор сложения:
 SomeClass sc;
 int iobj = sc + 3;
 Как компилятор решает, что следует сделать: вызвать перегруженный оператор для класса SomeClass или конвертировать операнд sc во встроенный тип, а затем уже воспользоваться встроенным оператором?
 Ответ зависит от множества перегруженных операторов и конвертеров, определенных в SomeClass. При выборе оператора для выполнения сложения применяется процесс разрешения перегрузки функции. В данном разделе мы расскажем, как этот процесс позволяет выбрать нужный оператор, когда операндами являются объекты типа класса.
 При разрешении перегрузки используется все та же процедура из трех шагов, представленная в разделе 9.2:
 Отбор функций-кандидатов.
 Отбор устоявших функций.
 Выбор наилучшей из устоявших функции.
 Рассмотрим эти шаги более детально.
 Разрешение перегрузки функции не применяется, если все операнды имеют встроенные типы. В таком случае гарантированно употребляется встроенный оператор. (Использование операторов с операндами встроенных типов описано в главе 4.) Например:
 class SmallInt {
 public:
  SmallInt( int );
 };
 
 SmallInt operator+ ( const SmallInt &, const SmallInt & );
 void func() {
  int i1, i2;
  int i3 = i1 + i2;
 }
 Поскольку операнды i1 и i2 имеют тип int, а не тип класса, то при сложении используется встроенный оператор +. Перегруженный operator+(const SmallInt &, const SmallInt &) игнорируется, хотя операнды можно привести к типу SmallInt с помощью определенного пользователем преобразования в виде конструктора SmallInt(int). Описанный ниже процесс разрешения перегрузки в таких ситуациях не применяется.
 Кроме того, разрешение перегрузки для операторов употребляется только в случае использования операторного синтаксиса:
 void func() {
  SmallInt si(98);
  int iobj = 65;
  int res = si + iobj; // использован операторный синтаксис
 }
 Если вместо этого использовать синтаксис вызова функции:
 int res = operator+( si, iobj ); // синтаксис вызова функции
 то применяется процедура разрешения перегрузки для функций в пространстве имен (см. раздел 15.10). Если же использован синтаксис вызова функции-члена:
 // синтаксис вызова функции-члена
 int res = si.operator+( iobj );
 то работает соответствующая процедура для функций-членов (см. раздел 15.11).
 15.12.1. Операторные функции-кандидаты
 Операторная функция является кандидатом, если она имеет то же имя, что и вызванная. При использовании следующего оператора сложения
 SmallInt si(98);
 int iobj = 65;
 int res = si + iobj;
 операторной функцией-кандидатом является operator+. Какие объявления operator+ принимаются во внимание?
 Потенциально в случае применения операторного синтаксиса с операндами, имеющими тип класса, строится пять множеств кандидатов. Первые три – те же, что и при вызове обычных функций с аргументами типа класса:
 множество операторов, видимых в точке вызова. Объявления функции operator+(), видимые в точке использования оператора, являются кандидатами. Например, operator+(), объявленный в глобальной области видимости, – кандидат в случае применения operator+() внутри main():
 SmallInt operator+ ( const SmallInt &, const SmallInt & );
 
 int main() {
  SmallInt si(98);

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

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