НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
-SD ,твой выбор?
Всего ответов: 430
Оbserver Ward

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

15. Перегруженные операторы и определенные пользователем преобразования (3)
15.10.3. Функции-кандидаты для вызова функции в области видимости класса

Когда вызов функции вида

calc(t)

встречается в области видимости класса (например, внутри функции-члена), то первая часть множества кандидатов, описанного в предыдущем подразделе (т.е. множество, включающее объявления функций, видимых в точке вызова), может содержать не только функции-члены класса. Для построения такого множества применяется разрешение имени. (Эта тема детально разбиралась в разделах 13.9 – 13.12.)

Рассмотрим пример:

namespace NS {
   struct myClass {
      void k( int );
      static void k( char* );
      void mf();
   };
   int k( double );
};

void h(char);

void NS::myClass::mf() {
   h('a');   // вызывается глобальная h( char )
   k(4);     // вызывается myClass::k( int )
}

Как отмечалось в разделе 13.11, квалификаторы NS::myClass:: просматриваются в обратном порядке: сначала поиск видимого объявления для имени, использованного в определении функции-члена mf(), ведется в классе myClass, а затем – в пространстве имен NS. Рассмотрим первый вызов:

h( 'a' );

При разрешении имени 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

Функции-члены также могут быть перегружены, и в этом случае тоже применяется процедура разрешения перегрузки для выбора наилучшей из устоявших. Такое разрешение очень похоже на аналогичную процедуру для обычных функций и состоит из тех же трех шагов:

   1. Отбор функций-кандидатов.
   2. Отбор устоявших функций.
   3. Выбор наилучшей из устоявших функции.

Однако есть небольшие различия в алгоритмах формирования множества кандидатов и отбора устоявших функций-членов. Эти различия мы и рассмотрим в настоящем разделе.
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+ принимаются во внимание?

      Потенциально в случае применения операторного синтаксиса с операндами, имеющими тип класса, строится пять множеств кандидатов. Первые три – те же, что и при вызове обычных функций с аргументами типа класса:
          o множество операторов, видимых в точке вызова. Объявления функции operator+(), видимые в точке использования оператора, являются кандидатами. Например, operator+(), объявленный в глобальной области видимости, – кандидат в случае применения operator+() внутри main():

            SmallInt operator+ ( const SmallInt &, const SmallInt & );

            int main() {
               SmallInt si(98);
               int iobj = 65;
               int res = si + iobj;  // ::operator+() - функция-кандидат
            }

          o множество операторов, объявленных в пространстве имен, в котором определен тип операнда. Если операнд имеет тип класса и этот тип объявлен в пользовательском пространстве имен, то операторные функции, объявленные в том же пространстве и имеющие то же имя, что и использованный оператор, считаются кандидатами:

            namespace NS {
               class SmallInt { /* ... */ };
               SmallInt operator+ ( const SmallInt&, double );
            }

            int main() {
               // si имеет тип SmallInt:
               // этот класс объявлен в пространстве имен NS
               NS::SmallInt si(15);

               // NS::operator+() - функция-кандидат
               int res = si + 566;
               return 0;
            }

            Операнд si имеет тип класса SmallInt, объявленного в пространстве имен NS. Поэтому перегруженный operator+(const SmallInt, double), объявленный в том же пространстве, добавляется к множеству кандидатов;
          o множество операторов, объявленных друзьями классов, к которым принадлежат операнды. Если операнд принадлежит к типу класса и в определении этого класса есть одноименные применяемому оператору функции-друзья, то они добавляются к множеству кандидатов:

            namespace NS {
               class SmallInt {
                  friend SmallInt operator+( const SmallInt&, int )
                                           { /* ... */ }
               };
            }
            int main() {
               NS::SmallInt si(15);

               // функция-друг operator+() - кандидат
               int res = si + 566;
               return 0;
            }

      Операнд si имеет тип SmallInt. Операторная функция operator+(const SmallInt&, int), являющаяся другом этого класса, – член пространства имен NS, хотя непосредственно в этом пространстве она не объявлена. При обычном поиске в NS эта операторная функция не будет найдена. Однако при использовании operator+() с аргументом типа SmallInt функции-друзья, объявленные в области видимости этого класса, включаются в рассмотрение и добавляются к множеству кандидатов. Эти три множества операторных функций-кандидатов формируются точно так же, как и для вызовов обычных функций с аргументами типа класса. Однако при использовании операторного синтаксиса строятся еще два множества:
          o множество операторов-членов, объявленных в классе левого операнда. Если такой операнд оператора operator+() имеет тип класса, то в множество функций-кандидатов включаются объявления operator+(), являющиеся членами этого класса:

            class myFloat {
               myFloat( double );
            };
            class SmallInt {
            public:
               SmallInt( int );
               SmallInt operator+ ( const myFloat & );
            };

            int main() {
               SmallInt si(15);

               int res = si + 5.66;  // оператор-член operator+() - кандидат
            }

            Оператор-член SmallInt::operator+(const myFloat &), определенный в SmallInt, включается в множество функций-кандидатов для разрешения вызова operator+() в main();
          o множество встроенных операторов. Учитывая типы, которые можно использовать со встроенным operator+(), кандидатами являются также:

            int operator+( int, int );
            double operator+( double, double );
            T* operator+( T*, I );
            T* operator+( I, T* );

      Первое объявление относится к встроенному оператору для сложения двух значений целых типов, второе – к оператору для сложения значений типов с плавающей точкой. Третье и четвертое соответствуют встроенному оператору сложения указательных типов, который используется для прибавления целого числа к указателю. Два последних объявления представлены в символическом виде и описывают целое семейство встроенных операторов, которые могут быть выбраны компилятором на роль кандидатов при обработке операций сложения.

      Любое из первых четырех множеств может оказаться пустым. Например, если среди членов класса SmallInt нет функции с именем operator+(), то четвертое множество будет пусто.

      Все множество операторных функций-кандидатов является объединением пяти подмножеств, описанных выше:

      namespace NS {
         class myFloat {
            myFloat( double );
         };
         class SmallInt {
            friend SmallInt operator+( const SmallInt &, int ) { /* ... */ }
         public:
            SmallInt( int );
            operator int();
            SmallInt operator+ ( const myFloat & );
            // ...
         };
         SmallInt operator+ ( const SmallInt &, double );
      }

      int main() {
         // тип si - class SmallInt:
         // Этот класс объявлен в пространстве имен NS
         NS::SmallInt si(15);

         int res = si + 5.66;  // какой operator+()?
         return 0;
      }

      В эти пять множеств входят семь операторных функций-кандидатов на роль operator+() в main():
          o первое множество пусто. В глобальной области видимости, а именно в ней употреблен operator+() в функции main(), нет объявлений перегруженного оператора operator+(); второе множество содержит операторы, объявленные в пространстве имен NS, где определен класс SmallInt. В этом пространстве имеется один оператор:

            NS::SmallInt NS::operator+( const SmallInt &, double );

          o третье множество содержит операторы, объявленные друзьями класса SmallInt. Сюда входит

            NS::SmallInt NS::operator+( const SmallInt &, int );

          o четвертое множество содержит операторы, объявленные членами SmallInt. Такой тоже есть:

            NS::SmallInt NS::SmallInt::operator+( const myFloat & );

          o пятое множество содержит встроенные бинарные операторы:
    o

      int operator+( int, int );
      double operator+( double, double );
      T* operator+( T*, I );
      T* operator+( I, T* );

      Да, формирование множества кандидатов для разрешения оператора, использованного с применением операторного синтаксиса, утомительно. Но после того как оно построено, устоявшие функции и наилучшая из них находятся, как и прежде, путем анализа преобразований, применимых к операндам отобранных кандидатов.
      15.12.2. Устоявшие функции

      Множество устоявших операторных функций формируется из множества кандидатов путем отбора лишь тех операторов, которые могут быть вызваны с заданными операндами. Например, какие из семи найденных выше кандидатов устоят? Оператор использован в следующем контексте:

      NS::SmallInt si(15);
      si + 5.66;

      Левый операнд имеет тип SmallInt, а правый – double.

      Первый кандидат является устоявшей функцией для данного использования operator+():

      NS::SmallInt NS::operator+( const SmallInt &, double );

      Левый операнд типа SmallInt в качестве инициализатора точно соответствует формальному параметру-ссылке этого перегруженного оператора. Правый, имеющий тип double, также точно соответствует второму формальному параметру.

      Следующая функция-кандидат также устоит:

      NS::SmallInt NS::operator+( const SmallInt &, int );

      Левый операнд si типа SmallInt в качестве инициализатора точно соответствует формальному параметру-ссылке перегруженного оператора. Правый имеет тип int и может быть приведен к типу второго формального параметра с помощью стандартного преобразования.

      Устоит и третья функция-кандидат:

      NS::SmallInt NS::SmallInt::operator+( const myFloat & );

      Левый операнд si имеет тип SmallInt, т.е. тип того класса, членом которого является перегруженный оператор. Правый имеет тип int и приводится к типу класса myFloat с помощью определенного пользователем преобразования в виде конструктора myFloat(double).

      Четвертой и пятой устоявшими функциями являются встроенные операторы:

      int operator+( int, int );
      double operator+( double, double );

      Класс SmallInt содержит конвертер, который может привести значение типа SmallInt к типу int. Этот конвертер используется вместе с первым встроенным оператором для преобразования левого операнда в тип int. Второй операнд типа double трансформируется в тип int с помощью стандартного преобразования. Что касается второго встроенного оператора, то конвертер приводит левый операнд от типа SmallInt к типу int, после чего результат стандартно преобразуется в double. Второй же операнд типа double точно соответствует второму параметру.

      Лучшей из этих пяти устоявших функций является первая, operator+(), объявленная в пространстве имен NS:

      NS::SmallInt NS::operator+( const SmallInt &, double );

      Оба ее операнда точно соответствуют параметрам.
      15.12.3. Неоднозначность

      Наличие в одном и том же классе конвертеров, выполняющих неявные преобразования во встроенные типы, и перегруженных операторов может приводить к неоднозначности при выборе между ними. Например, есть следующее определение класса String с функцией сравнения:

      class String {
         // ...
      public:
         String( const char * = 0 );
         bool operator== ( const String & ) const;
         // нет оператора operator== ( const char * )
      };

      и такое использование оператора operator==:

      String flower( "tulip" );
      void foo( const char *pf ) {
         // вызывается перегруженный оператор String::operator==()
         if ( flower == pf )
            cout << pf <<" is a flower!\en";
            // ...
      }

      Тогда при сравнении

      flower == pf

      вызывается оператор равенства класса String:

      String::operator==( const String & ) const;

      Для трансформации правого операнда pf из типа const char* в тип String параметра operator==() применяется определенное пользователем преобразование, которое вызывает конструктор:

      String( const char * )

      Если добавить в определение класса String конвертер в тип const char*:

      class String {
         // ...
      public:
         String( const char * = 0 );
         bool operator== ( const String & ) const;
         operator const char*();  // новый конвертер
      };

      то показанное использование operator==() становится неоднозначным:

      // проверка на равенство больше не компилируется!
      if (flower == pf)

      Из-за добавления конвертера operator const char*() встроенный оператор сравнения

      bool operator==( const char *, const char * )

      тоже считается устоявшей функцией. С его помощью левый операнд flower типа String может быть преобразован в тип const char *.

      Теперь для использования operator==() в foo() есть две устоявших операторных функции. Первая из них

      String::operator==( const String & ) const;

      требует применения определенного пользователем преобразования правого операнда pf из типа const char* в тип String. Вторая

      bool operator==( const char *, const char * )

      требует применения пользовательского преобразования левого операнда flower из типа String в тип const char*.

      Таким образом, первая устоявшая функция лучше для левого операнда, а вторая – для правого. Поскольку наилучшей функции не существует, то вызов помечается компилятором как неоднозначный.

      При проектировании интерфейса класса, включающего объявление перегруженных операторов, конструкторов и конвертеров, следует быть весьма аккуратным. Определенные пользователем преобразования применяются компилятором неявно. Это может привести к тому, что встроенные операторы окажутся устоявшими при разрешении перегрузки для операторов с операндами типа класса.

      Упражнение 15.17

      Назовите пять множеств функций-кандидатов, рассматриваемых при разрешении перегрузки оператора с операндами типа класса.

      Упражнение 15.18

      Какой из операторов operator+() будет выбран в качестве наилучшего из устоявших для оператора сложения в main()? Перечислите все функции-кандидаты, все устоявшие функции и преобразования типов, которые надо применить к аргументам для каждой устоявшей функции.

      namespace NS {
         class complex {
            complex( double );
            // ...
         };
         class LongDouble {
            friend LongDouble operator+( LongDouble &, int ) { /* ... */ }
         public:
            LongDouble( int );
            operator double();
            LongDouble operator+( const complex & );
            // ...
         };
         LongDouble operator
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 1014 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp