Chapter 1. Определенные пользователем преобразования

Content

§ 1.1. Определенные пользователем преобразования

§ 1.2. Конвертеры

§ 1.3. Конструктор как конвертер

§ 1.4. Выбор преобразования А

§ 1.5. Еще раз о разрешении перегрузки функций

§ 1.6. Функции-кандидаты

§ 1.7. Функции-кандидаты для вызова функции в области видимости класса

§ 1.8. Ранжирование последовательностей определенных пользователем преобразований

§ 1.9. Разрешение перегрузки и функции-члены A

§ 1.10. Объявления перегруженных функций-членов

§ 1.11. Функции-кандидаты

§ 1.12. Устоявшие функции

§ 1.13. Разрешение перегрузки и операторы A

§ 1.14. Операторные функции-кандидаты

§ 1.15. Устоявшие функции

§ 1.16. Неоднозначность

§ 1.1. Определенные пользователем преобразования

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

char ch; short sh;, int ival;
/* в каждой операции один операнд требует преобразования типа */

ch + ival;          ival + ch;
ch + sh;            ch + ch;
ival + sh;          sh + ival;

Операнды ch и sh расширяются до типа int. При выполнении операции складываются два значения типа int. Расширение типа неявно выполняется компилятором и для пользователя прозрачно.

В этом разделе мы рассмотрим, как разработчик может определить собственные преобразования для объектов типа класса. Такие определенные пользователем преобразования также автоматически вызываются компилятором по мере необходимости. Чтобы показать, зачем они нужны, обратимся снова к классу SmallInt, введенному в разделе 10.9.

Напомним, что SmallInt позволяет определять объекты, способные хранить значения из того же диапазона, что unsigned char, т.е. от 0 до 255, и перехватывает ошибки выхода за его границы. Во всех остальных отношениях этот класс ведет себя точно так же, как unsigned char.

Чтобы иметь возможность складывать объекты SmallInt с другими объектами того же класса или со значениями встроенных типов, а также вычитать их, реализуем шесть операторных функций:

class SmallInt {
   friend operator+(const SmallInt&, int );
   friend operator-(const SmallInt&, int );
   friend operator-(int, const SmallInt&);
   friend operator+(int, const SmallInt&);
public:
   SmallInt(int ival) : value(ival) { }
   operator+(const SmallInt&);
   operator-(const SmallInt&);
   // ...
private:
   int value;
};

Операторы-члены дают возможность складывать и вычитать два объекта SmallInt. Глобальные же операторы-друзья позволяют производить эти операции над объектами данного класса и объектами встроенных арифметических типов. Необходимо только шесть операторов, поскольку любой встроенный арифметический тип может быть приведен к типу int. Например, выражение

SmallInt si( 3 );
si + 3.14159

разрешается в два шага:

  1. Константа 3.14159 типа double преобразуется в целое число 3.
  2. Вызывается operator+(const SmallInt &,int), который возвращает значение 6.

Если мы хотим поддержать битовые и логические операции, а также операции сравнения и составные операторы присваивания, то сколько же необходимо перегрузить операторов? Сразу и не сосчитаешь. Значительно удобнее автоматически преобразовать объект класса SmallInt в объект типа int.

В языке C++ имеется механизм, позволяющий в любом классе задать набор преобразований, применимых к его объектам. Для SmallInt мы определим приведение объекта к типу int. Вот его реализация:

class SmallInt {
public:
   SmallInt( int ival ) : value( ival ) { }

   // конвертер SmallInt ==> int
   operator int() { return value; }

   // перегруженные операторы не нужны
private:
   int value;
};

Оператор int() – это конвертер, реализующий определенное пользователем преобразование, в данном случае приведение типа класса к заданному типу int. Определение конвертера описывает, что означает преобразование и какие действия компилятор должен выполнить для его применения. Для объекта SmallInt смысл преобразования в int заключается в том, чтобы вернуть число типа int, хранящееся в члене value.

Теперь объект класса SmallInt можно использовать всюду, где допустимо использование int. Если предположить, что перегруженных операторов больше нет и в SmallInt определен конвертер в int, операция сложения

SmallInt si( 3 );
si + 3.14159

разрешается двумя шагами:

  1. Вызывается конвертер класса SmallInt, который возвращает целое число 3.
  2. Целое число 3 расширяется до 3.0 и складывается с константой двойной точности 3.14159, что дает 6.14159.

Такое поведение больше соответствует поведению операндов встроенных типов по сравнению с определенными ранее перегруженными операторами. Когда значение типа int складывается со значением типа double, то выполняется сложение двух чисел типа double (поскольку тип int расширяется до double) и результатом будет число того же типа.

В этой программе иллюстрируется применение класса SmallInt:

#include <iostream>
#include "SmallInt.h"
int main() {
   cout<< "Введите SmallInt, пожалуйста: ";
   while ( cin >> si1 ) {
      cout << "Прочитано значение "
           << si1 << "\nОно ";
      // SmallInt::operator int() вызывается дважды
      cout << ( ( si1 > 127 )
              ? "больше, чем "
              : ( ( si1 < 127 )
                ? "меньше, чем "
                : "равно ") ) <<"127\n";
      cout << "Введите SmallInt, пожалуйста \
              (ctrl-d для выхода): ";
   }
   cout <<"До встречи\n";
}

Откомпилированная программа выдает следующие результаты:

Введите SmallInt, пожалуйста: 127

Прочитано значение 127

Оно равно 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 126

Оно меньше, чем 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 128

Оно больше, чем 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 256

*** Ошибка диапазона SmallInt: 256 ***

В реализацию класса SmallInt добавили поддержку новой функциональности:

#include <iostream>

class SmallInt {
   friend istream& operator>>(istream& is, SmallInt& s);
   friend ostream& operator<<(ostream& is, const SmallInt& s)
      { return os << s.value; }
public:
   SmallInt( int i=0 ) : value( rangeCheck( i ) ){}
   int operator=( int i )
      { return( value = rangeCheck( i ) ); }
   operator int() { return value; }
private:
   int rangeCheck( int );
   int value;
};

Ниже приведены определения функций-членов, находящиеся вне тела класса:

istream& operator>>(istream& is, SmallInt& si) {
   int ix;
   is >> ix;
   si = ix;   // SmallInt::operator=(int)
   return is;
}

int SmallInt::rangeCheck(int i) {
/* если установлен хотя бы один бит, кроме первых восьми,
 * то значение слишком велико; сообщить и сразу выйти */

   if ( i & ~0377 ) {
      cerr <<"\n*** Ошибка диапазона SmallInt: "
           << i << " ***" << endl;
      exit(1);
   }
   return i;
}

§ 1.2. Конвертеры

Конвертер – это особый случай функции-члена класса, реализующий определенное пользователем преобразование объекта в некоторый другой тип. Конвертер объявляется в теле класса путем указания ключевого слова operator, за которым следует целевой тип преобразования.

Имя, находящееся за ключевым словом, не обязательно должно быть именем одного из встроенных типов. В показанном ниже классе Token определено несколько конвертеров. В одном из них для задания имени типа используется typedef tName, а в другом – тип класса SmallInt.

#include "SmallInt.h"
typedef char *tName;
class Token {
public:
   Token( char *, int );
   operator SmallInt() { return val; }
   operator tName()    { return name; }
   operator int()      { return val; }
   // другие открытые члены
private:
   SmallInt val;
   char *name;
};

Обратите внимание, что определения конвертеров в типы SmallInt и int одинаковы. Конвертер Token::operator int() возвращает значение члена val. Поскольку val имеет тип SmallInt, то неявно применяется SmallInt::operator int() для преобразования val в тип int. Сам Token::operator int() неявно употребляется компилятором для преобразования объекта типа Token в значение типа int. Например, этот конвертер используется для неявного приведения фактических аргументов t1 и t2 типа Token к типу int формального параметра функции print():

#include "Token.h"

void print(int i) {
   cout << "print( int ) : " << i << endl;
}

Token t1( "integer constant", 127 );
Token t2( "friend", 255 );

int main() {
   print( t1 );    // t1.operator int()
   print( t2 );    // t2.operator int()
   return 0;
}

После компиляции и запуска программа выведет такие строки:

print( int ) : 127
print( int ) : 255

Общий вид конвертера следующий:

operator type();

где type может быть встроенным типом, типом класса или именем typedef. Конвертеры, в которых type – тип массива или функции, не допускаются. Конвертер должен быть функцией-членом. В его объявлении не должны задаваться ни тип возвращаемого значения, ни список параметров:

operator int( SmallInt&);  // ошибка: не член
class SmallInt {
public:
   int operator int();      // ошибка: задан тип возвращаемого значения
   operator int(int = 0);  // ошибка: задан список параметров
   // ...
};

Конвертер вызывается в результате явного преобразования типов. Если преобразуемое значение имеет тип класса, у которого есть конвертер, и в операции приведения указан тип этого конвертера, то он и вызывается:

#include "Token.h"
Token tok( "function", 78 );

// функциональная нотация: вызывается Token::operator SmallInt()
SmallInt tokVal = SmallInt( tok );
// static_cast: вызывается Token::operator tName()
char *tokName = static_cast<char*>( tok );

У конвертера Token::operator tName() может быть нежелательный побочный эффект. Попытка прямого обращения к закрытому члену Token::name помечается компилятором как ошибка:

char *tokName = tok.name;  // ошибка: Token::name - закрытый член

Однако наш конвертер, разрешая пользователям непосредственно изменять Token::name, делает как раз то, от чего мы хотели защититься. Скорее всего, это не годится. Вот, например, как могла бы произойти такая модификация:

#include "Token.h"
Token tok( "function", 78 );
char *tokName = tok;   // правильно: неявное преобразование
*tokname = 'P';  // но теперь в члене name находится Punction!

Мы намереваемся разрешить доступ к преобразованному объекту класса Token только для чтения. Следовательно, конвертер должен возвращать тип const char*:

typedef const char *cchar;
class Token {
public:
   operator cchar() { return name; }
   // ...
};
// ошибка: преобразование char* в const char* не допускается
char *pn = tok;
const char *pn2 = tok;   // правильно

Другое решение – заменить в определении Token тип char* на тип string из стандартной библиотеки C++:

class Token {
public:
   Token( string, int );
   operator SmallInt() { return val; }
   operator string()   { return name; }
   operator int()      { return val; }
   // другие открытые члены
private:
   SmallInt val;
   string name;
};

Семантика конвертера Token::operator string() состоит в возврате копии значения (а не указателя на значение) строки, представляющей имя лексемы. Это предотвращает случайную модификацию закрытого члена name класса Token.

Должен ли целевой тип точно соответствовать типу конвертера? Например, будет ли в следующем коде вызван конвертер int(), определенный в классе Token?

extern void calc( double );
Token tok( "constant", 44 );
// Вызывается ли оператор int()? Да
// применяется стандартное преобразование int --> double
calc( tok );

Если целевой тип (в данном случае double) не точно соответствует типу конвертера (в нашем случае int), то конвертер все равно будет вызван при условии, что существует последовательность стандартных преобразований, приводящая к целевому типу из типа конвертера. При обращении к функции calc() вызывается Token::operator int() для преобразования tok из типа Token в тип int. Затем для приведения результата от типа int к типу double применяется стандартное преобразование.

Вслед за определенным пользователем преобразованием допускаются только стандартные. Если для достижения целевого типа необходимо еще одно пользовательское преобразование, то компилятор не применяет никаких преобразований. Предположим, что в классе Token не определен operator int(), тогда следующий вызов будет ошибочным:

extern void calc( int );
Token tok( "pointer", 37 );
// если Token::operator int() не определен,
// то этот вызов приводит к ошибке компиляции
calc( tok );

Если конвертер Token::operator int() не определен, то приведение tok к типу int потребовало бы вызова двух определенных пользователем конвертеров. Сначала фактический аргумент tok надо было бы преобразовать из типа Token в тип SmallInt с помощью конвертера

Token::operator SmallInt()

а затем результат привести к типу int – тоже с помощью пользовательского конвертера

Token::operator int()

Вызов calc(tok) помечается компилятором как ошибка, так как не существует неявного преобразования из типа Token в тип int.

Если логического соответствия между типом конвертера и типом класса нет, назначение конвертера может оказаться непонятным читателю программы:

class Date {
public:
   // попробуйте догадаться, какой именно член возвращается!
   operator int();
private:
   int month, day, year;
};

Какое значение должен вернуть конвертер int() класса Date? Сколь бы основательными ни были причины для того или иного решения, читатель останется в недоумении относительно того, как пользоваться объектами класса Date, поскольку между ними и целыми числами нет явного логического соответствия. В таких случаях лучше вообще не определять конвертер.

§ 1.3. Конструктор как конвертер

Набор конструкторов класса, принимающих единственный параметр, например, SmallInt(int) класса SmallInt, определяет множество неявных преобразований в значения типа SmallInt. Так, конструктор SmallInt(int) преобразует значения типа int в значения типа SmallInt.

extern void calc( SmallInt );
int i;
// необходимо преобразовать i в значение типа SmallInt
// это достигается применением SmallInt(int)
calc( i );

При вызове calc(i) число i преобразуется в значение типа SmallInt с помощью конструктора SmallInt(int), вызванного компилятором для создания временного объекта нужного типа. Затем копия этого объекта передается в calc(), как если бы вызов функции был записан в форме:

// Псевдокод на C++
// создается временный объект типа SmallInt
{
   SmallInt temp = SmallInt( i );
   calc( temp );
}

Фигурные скобки в этом примере обозначают время жизни данного объекта: он уничтожается при выходе из функции.

Типом параметра конструктора может быть тип некоторого класса:

class Number {
public:
   // создание значения типа Number из значения типа SmallInt
   Number(const SmallInt&);
   // ...
};

В таком случае значение типа SmallInt можно использовать всюду, где допустимо значение типа Number:

extern void func( Number );
SmallInt si(87);

int main() {  // вызывается Number(const SmallInt&)
   func( si );
   // ...
}

Если конструктор используется для выполнения неявного преобразования, то должен ли тип его параметра точно соответствовать типу подлежащего преобразованию значения? Например, будет ли в следующем коде вызван SmallInt(int), определенный в классе SmallInt, для приведения dobj к типу SmallInt?

extern void calc( SmallInt );
double dobj;

// вызывается ли SmallInt(int)?
// Да, dobj преобразуется приводится от double к int стандартным преобразованием
calc(dobj);

Если необходимо, к фактическому аргументу применяется последовательность стандартных преобразований до того, как вызвать конструктор, выполняющий определенное пользователем преобразование. При обращении к функции calc()употребляется стандартное преобразование dobj из типа double в тип int. Затем уже для приведения результата к типу SmallInt вызывается SmallInt(int).

Компилятор неявно использует конструктор с единственным параметром для преобразования его типа в тип класса, к которому принадлежит конструктор. Однако иногда удобнее, чтобы конструктор Number(const SmallInt&) можно было вызывать только для инициализации объекта типа Number значением типа SmallInt, но ни в коем случае не для выполнения неявных преобразований. Чтобы избежать такого употребления конструктора, объявим его явным (explicit):

class Number {
public:
   // никогда не использовать для неявных преобразований
   explicit Number(const SmallInt&);
   // ...
};

Компилятор никогда не применяет явные конструкторы для выполнения неявных преобразований типов:

extern void func( Number );
SmallInt si(87);

int main() {
   // ошибка: не существует неявного преобразования из SmallInt в Number
   func( si );
   // ...
}

Однако такой конструктор все же можно использовать для преобразования типов, если оно запрошено явно в форме оператора приведения типа:

SmallInt si(87);

int main() {  // ошибка: не существует неявного преобразования из SmallInt в Number
   func(si);
   func(Number(si));  // правильно: приведение типа
   func(static_cast<Number>(si));  // правильно: приведение типа
}

§ 1.4. Выбор преобразования А

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

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

Последовательность стандартных преобразований ->

Определенное пользователем преобразование ->

Последовательность стандартных преобразований

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

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

В классе разрешается определять много конвертеров. Например, в нашем классе Number их два: operator int() и operator float(), причем оба способны преобразовать объект типа Number в значение типа float. Естественно, можно воспользоваться конвертером Token::operator float() для прямой трансформации. Но и Token::operator int() тоже подходит, так как результат его применения имеет тип int и, следовательно, может быть преобразован в тип float с помощью стандартного преобразования. Является ли трансформация неоднозначной, если имеется несколько таких последовательностей? Или какую-то из них можно предпочесть остальным?

class Number {
public:
   operator float();
   operator int();
   // ...
};
Number num;
float ff = num;   // какой конвертер? operator float()

В таких случаях выбор наилучшей последовательности определенных пользователем преобразований основан на анализе последовательности преобразований, которая применяется после конвертера. В предыдущем примере можно применить такие две последовательности:

  1. operator float() -> точное соответствие
  2. operator int() -> стандартное преобразование

Как было сказано в разделе 9.3, точное соответствие лучше стандартного преобразования. Поэтому первая последовательность лучше второй, а значит, выбирается конвертер Token::operator float().

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

class SmallInt {
public:
   SmallInt(int ival) : value(ival) { }
   SmallInt(double dval)
      : value(static_cast<int>(dval));
   { }
};

extern void manip( const SmallInt&);

int main() {
   double dobj;
   manip( dobj );   // правильно: SmallInt( double )
}

Здесь в классе SmallInt определено два конструктора – SmallInt(int) и SmallInt(double), которые можно использовать для изменения значения типа double в объект типа SmallInt: SmallInt(double) трансформирует double в SmallInt напрямую, а SmallInt(int) работает с результатом стандартного преобразования double в int. Таким образом, имеются две последовательности определенных пользователем преобразований:

  1. точное соответствие -> SmallInt( double )
  2. стандартное преобразование -> SmallInt( int )

Поскольку точное соответствие лучше стандартного преобразования, то выбирается конструктор SmallInt(double).

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

class Number {
public:
   operator float();
   operator int();
   // ...
};

то невозможно неявно преобразовать объект типа Number в тип long. Следующая инструкция вызывает ошибку компиляции, так как выбор последовательности определенных пользователем преобразований неоднозначен:

// ошибка: можно применить как float(), так и int()
long lval = num;

Для трансформации num в значение типа long применимы две такие последовательности:

  1. operator float() -> стандартное преобразование
  2. operator int() -> стандартное преобразование

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

С помощью явного приведения типов программист способен задать нужное изменение:

// правильно: явное приведение типа
long lval = static_cast<int>( num );

Вследствие такого указания выбирается конвертер Token::operator int(), за которым следует стандартное преобразование в long.

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

class SmallInt {
public:
   SmallInt( const Number&);
   // ...
};

class Number {
public:
   operator SmallInt();
   // ...
};

extern void compute( SmallInt );
extern Number num;

compute(num);  // ошибка: возможно два преобразования

Аргумент num преобразуется в тип SmallInt двумя разными способами: с помощью конструктора SmallInt::SmallInt(const Number&) либо с помощью конвертера Number::operator SmallInt(). Поскольку оба изменения одинаково хороши, вызов считается ошибкой.

Для разрешения неоднозначности программист может явно вызвать конвертер класса Number:

// правильно: явный вызов устраняет неоднозначность
compute( num.operator SmallInt() );

Однако для разрешения неоднозначности не следует использовать явное приведение типов, поскольку при отборе преобразований, подходящих для приведения типов, рассматриваются как конвертер, так и конструктор:

compute( SmallInt( num ) );  // ошибка: по-прежнему неоднозначно

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

§ 1.5. Еще раз о разрешении перегрузки функций

В главе 9 подробно описывалось, как разрешается вызов перегруженной функции. Если фактические аргументы при вызове имеют тип класса, указателя на тип класса или указателя на члены класса, то на роль возможных кандидатов претендует большее число функций. Следовательно, наличие таких аргументов оказывает влияние на первый шаг процедуры разрешения перегрузки – отбор множества функций-кандидатов.

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

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

§ 1.6. Функции-кандидаты

Функцией-кандидатом называется функция с тем же именем, что и вызванная. Предположим, что имеется такой вызов:

SmallInt si(15);
add( si, 566 );

Функция-кандидат должна иметь имя add. Какие из объявлений add() принимаются во внимание? Те, которые видимы в точке вызова.

Например, обе функции add(), объявленные в глобальной области видимости, будут кандидатами для следующего вызова:

const matrix& add(const matrix&, int);
double add(double, double);

int main() {
   SmallInt si(15);
   add(si, 566);
   // ...
}

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

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

namespace NS {
   class SmallInt { /* ... */ };
   class String { /* ... */ };
   String add(const String&, const String&);
}

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

   add(si, 566);  // NS::add() - функция-кандидат
   return 0;
}

Аргумент si имеет тип SmallInt, т.е. тип класса, объявленного в пространстве имен NS. Поэтому к множеству функций-кандидатов добавляется add(const String&, const String&), объявленная в этом пространстве имен;

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

namespace NS {
   class SmallInt {
      friend SmallInt add( SmallInt, int ) { /* ... */ }
   };
}

int main() {
   NS::SmallInt si(15);

   add( si, 566 );  // функция-друг add() - кандидат
   return 0;
}

Аргумент функции si имеет тип SmallInt. Функция-друг класса SmallInt add(SmallInt, int) – член пространства имен NS, хотя непосредственно в этом пространстве она не объявлена. При обычном поиске в NS функция-друг не будет найдена. Однако при вызове add() с аргументом типа класса SmallInt принимаются во внимание и добавляются к множеству кандидатов также друзья этого класса, объявленные в списке его членов.

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

Рассмотрим следующий пример:

namespace NS {
   class SmallInt {
      friend SmallInt add( SmallInt, int ) { /* ... */ }
   };
   class String { /* ... */ };
   String add(const String&, const String&);
}

const matrix& add(const matrix&, int);
double add(double, double);

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

   add( si, 566 );  // вызывается функция-друг
   return 0;
}

Здесь кандидатами являются:

При разрешении перегрузки выбирается функция-друг класса SmallInt NS::add( SmallInt, int ) как наилучшая из устоявших: оба фактических аргумента точно соответствуют заданным формальным параметрам.

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

§ 1.7. Функции-кандидаты для вызова функции в области видимости класса

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

calc(t)

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

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

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 )
}

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

h( 'a' );

При разрешении имени h() в определении функции-члена mf() сначала просматриваются функции-члены myClass. Поскольку функции-члена с таким именем в области видимости этого класса нет, то далее поиск идет в пространстве имен NS. Функции h()нет и там, поэтому мы переходим в глобальную область видимости. Результат – глобальная функция h(char), единственная функция-кандидат, видимая в точке вызова.

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

k( 4 );

Сначала поиск ведется в области видимости класса myClass. При этом найдены две функции-члена k(int) и k(char*). Поскольку множество кандидатов содержит лишь функции, объявленные в той области, где разрешение успешно завершилось, то пространство имен NS не просматривается и функция k(double) в данное множество не включается.

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

§ 1.8. Ранжирование последовательностей определенных пользователем преобразований

Фактический аргумент функции может быть неявно приведен к типу формального параметра с помощью последовательности определенных пользователем преобразований. Как это влияет на разрешение перегрузки? Например, если имеется следующий вызов 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 );   // какая функция?
}

§ 1.9. Разрешение перегрузки и функции-члены A

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

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

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

§ 1.10. Объявления перегруженных функций-членов

Функции-члены класса можно перегружать:

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 )
    // ...
};

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

§ 1.11. Функции-кандидаты

Рассмотрим два вида вызовов функции-члена:

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(), то множество кандидатов оказалось бы пустым. (еще рассматривались бы также и функции из базовых классов) Если для вызова функции не оказывается кандидатов, компилятор выдает сообщение об ошибке.

§ 1.12. Устоявшие функции

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

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' );

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

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

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

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 у нестатических членов. Как они влияют на процесс разрешения перегрузки? Пусть в классе 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) в нем остается, так как является статической. Поскольку это единственная устоявшая функция, она и оказывается наилучшей.

§ 1.13. Разрешение перегрузки и операторы 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).

§ 1.14. Операторные функции-кандидаты

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

SmallInt si(98);
int iobj = 65;
int res = si + iobj;

операторной функцией-кандидатом является operator+. Какие объявления operator+ принимаются во внимание?

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

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

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

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

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

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), объявленный в том же пространстве, добавляется к множеству кандидатов;

3) множество операторов, объявленных друзьями классов, к которым принадлежат операнды. Если операнд принадлежит к типу класса и в определении этого класса есть одноименные применяемому оператору функции-друзья, то они добавляются к множеству кандидатов:

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

4) множество операторов-членов, объявленных в классе левого операнда. Если такой операнд оператора 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();

5) множество встроенных операторов. Учитывая типы, которые можно использовать со встроенным 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():

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

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

§ 1.15. Устоявшие функции

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

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);

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

§ 1.16. Неоднозначность

Наличие в одном и том же классе конвертеров, выполняющих неявные преобразования во встроенные типы, и перегруженных операторов может приводить к неоднозначности при выборе между ними. Например, есть следующее определение класса 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