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


Выбор преобразования *


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

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

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

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

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

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

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

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

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


Содержание раздела