<< Пред. стр. 71 (из 121) След. >>
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), то конвертер все равно будет вызван при условии, что существует последовательность стандартных преобразований, приводящая к целевому типу из типа конвертера. (Эти последовательности описаны в разделе 9.3.) При обращении к функции 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, поскольку между ними и целыми числами нет явного логического соответствия. В таких случаях лучше вообще не определять конвертер.
15.9.2. Конструктор как конвертер
Набор конструкторов класса, принимающих единственный параметр, например, 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 ) ); // правильно: приведение типа
}
15.10. Выбор преобразования A
Определенное пользователем преобразование реализуется в виде конвертера или конструктора. Как уже было сказано, после преобразования, выполненного конвертером, разрешается использовать стандартное преобразование для приведения возвращенного значения к целевому типу. Трансформации, выполненной конструктором, также может предшествовать стандартное преобразование для приведения типа аргумента к типу формального параметра конструктора.
Последовательность определенных пользователем преобразований – это комбинация определенного пользователем и стандартного преобразования, которая необходима для приведения значения к целевому типу. Такая последовательность имеет вид:
Последовательность стандартных преобразований ->
Определенное пользователем преобразование ->
Последовательность стандартных преобразований
где определенное пользователем преобразование реализуется конвертером либо конструктором.
Не исключено, что для трансформации исходного значения в целевой тип существует две разных последовательности пользовательских преобразований, и тогда компилятор должен выбрать из них лучшую. Рассмотрим, как это делается.
В классе разрешается определять много конвертеров. Например, в нашем классе 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()
В таких случаях выбор наилучшей последовательности определенных пользователем преобразований основан на анализе последовательности преобразований, которая применяется после конвертера. В предыдущем примере можно применить такие две последовательности:
operator float() -> точное соответствие
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. Таким образом, имеются две последовательности определенных пользователем преобразований:
точное соответствие -> SmallInt( double )
стандартное преобразование -> SmallInt( int )
Поскольку точное соответствие лучше стандартного преобразования, то выбирается конструктор SmallInt(double).
Не всегда удается решить, какая последовательность лучше. Может случиться, что все они одинаково хороши, и тогда мы говорим, что преобразование неоднозначно. В таком случае компилятор не применяет никаких неявных трансформаций. Например, если в классе Number есть два конвертера:
class Number {
public:
operator float();
operator int();
// ...
};
то невозможно неявно преобразовать объект типа Number в тип long. Следующая инструкция вызывает ошибку компиляции, так как выбор последовательности определенных пользователем преобразований неоднозначен:
// ошибка: можно применить как float(), так и int()
long lval = num;
Для трансформации num в значение типа long применимы две такие последовательности:
operator float() -> стандартное преобразование
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 ) ); // ошибка: по-прежнему неоднозначно
Как видите, наличие большого числа подобных конвертеров и конструкторов небезопасно, поэтому их. следует применять с осторожностью. Ограничить использование конструкторов при выполнении неявных преобразований (а значит, уменьшить вероятность неожиданных эффектов) можно путем объявления их явными.
15.10.1. Еще раз о разрешении перегрузки функций
В главе 9 подробно описывалось, как разрешается вызов перегруженной функции. Если фактические аргументы при вызове имеют тип класса, указателя на тип класса или указателя на члены класса, то на роль возможных кандидатов претендует большее число функций. Следовательно, наличие таких аргументов оказывает влияние на первый шаг процедуры разрешения перегрузки – отбор множества функций-кандидатов.
На третьем шаге этой процедуры выбирается наилучшее соответствие. При этом ранжируются преобразования типов фактических аргументов в типы формальных параметров функции. Если аргументы и параметры имеют тип класса, то в множество возможных преобразований следует включать и последовательности определенных пользователем преобразований, также подвергая их ранжированию.
В этом разделе мы детально рассмотрим, как фактические аргументы и формальные параметры типа класса влияют на отбор функций-кандидатов и как последовательности определенных пользователем преобразований сказываются на выборе наилучшей из устоявших функции.
15.10.2. Функции-кандидаты
Функцией-кандидатом называется функция с тем же именем, что и вызванная. Предположим, что имеется такой вызов:
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;
}
Здесь кандидатами являются:
глобальные функции:
const matrix& add( const matrix &, int )
double add( double, double )
функция из пространства имен:
NS::add( const String &, const String & )
функция-друг:
NS::add( SmallInt, int )
При разрешении перегрузки выбирается функция-друг класса SmallInt NS::add( SmallInt, int ) как наилучшая из устоявших: оба фактических аргумента точно соответствуют заданным формальным параметрам.
Разумеется, вызванная функция может быть несколько аргументов типа класса, указателя или ссылки на класс либо указателя на член класса. Допускаются разные типы классов для каждого из таких аргументов. Поиск функций-кандидатов для них ведется в пространстве имен, где определен класс, и среди функций-друзей класса. Поэтому результирующее множество кандидатов для вызова функции с такими аргументами содержит функции из разных пространств имен и функции-друзья, объявленные в разных классах.
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' );