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