Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
85
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

С++ для начинающих

747

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) преобразует значения типа

extern void calc( SmallInt ); int i;

//необходимо преобразовать i в значение типа SmallInt

//это достигается применением SmallInt(int)

int в значения типа SmallInt. calc( i );

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

С++ для начинающих

748

//Псевдокод на C++

//создается временный объект типа SmallInt

{

SmallInt temp = SmallInt( i ); calc( temp );

}

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

class Number { public:

//создание значения типа Number из значения типа SmallInt Number( const SmallInt & );

//...

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

};

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

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

int main()

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

// ...

значение типа Number:

}

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

extern void calc( SmallInt ); double dobj;

//вызывается ли SmallInt(int)? Да

//dobj преобразуется приводится от double к int

//стандартным преобразованием

SmallInt, для приведения dobj к типу SmallInt? calc( dobj );

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

С++ для начинающих

749

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

class Number { public:

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

//...

употребления конструктора, объявим его явным (explicit):

};

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

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

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

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

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

С++ для начинающих

750

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

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

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

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

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

В классе разрешается определять много конвертеров. Например, в нашем классе 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().

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

С++ для начинающих

751

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

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

class Number { public:

operator float(); operator int(); // ...

классе Number есть два конвертера:

};

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

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

определенных пользователем преобразований неоднозначен: long lval = num;

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

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

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

С++ для начинающих

752

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

// правильно: явное приведение типа

Спомощью явного приведения типов программист способен задать нужное изменение: 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 ) ); // ошибка: по-прежнему неоднозначно

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