14.9.2. Избегайте неоднозначных преобразований

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

Есть два случая, когда возникает несколько путей осуществления преобразования. Первый — когда два класса обеспечивают взаимное преобразование. Например, взаимное преобразование осуществляется тогда, когда класс А определяет конструктор преобразования, получающий объект класса B, а класс в определяет оператор преобразования в тип А.

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

Обычно не следует определять классы со взаимными преобразованиями или определять преобразования в или из арифметических типов.

Распознавание аргумента и взаимные преобразования

В следующем примере определены два способа получения объекта класса А из В: либо при помощи оператора преобразования класса В, либо при помощи конструктора класса А, получающего объект класса В:

// обычно взаимное преобразование между двумя типами - плохая идея

struct B;

struct А {

 А() = default;

 A(const В&); // преобразует В в A

 // другие члены

};

struct В {

 operator A() const; // тоже преобразует В в A

 // другие члены

};

A f (const A&);

A a = f(b); // ошибка неоднозначности: f(B::operator A())

            // или f(A::A(const B&))

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

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

A a1 = f(b.operator А()); // ok: использовать оператор преобразования В

A а2 = f(A(b));           // ok: использовать конструктор класса А

Обратите внимание: нельзя решить неоднозначность при помощи приведения — у самого приведения будет та же двусмысленность.

Двусмысленность и множественность путей преобразования во встроенные типы

Двусмысленность возникает также в случае, когда класс определяет несколько преобразований в (или из) типы, которые сами связываются преобразованиями. Самый простой и наглядный пример (а также особенно проблематичный) — это когда класс определяет конструкторы преобразования в или из более, чем один арифметический тип.

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

struct A {

 A(int = 0);              // обычно плохая идея иметь два

 A(double);               // преобразования из арифметических типов

 operator int() const;    // обычно плохая идея иметь два

 operator double() const; // преобразования в арифметические типы

 // другие члены

};

void f2(long double);

A a;

f2(a); // ошибка неоднозначности: f(A::operator int())

       // или f (A::operator double ())

long lg;

A a2(lg); // ошибка неоднозначности: A::A(int) или A::A(double)

В вызове функции f2() ни одно из преобразований не соответствует точно типу long double. Но для его получения применимо любое преобразование, сопровождаемое стандартным преобразованием. Следовательно, никакое из преобразований не лучше другого, значит, вызов неоднозначен.

Возникает та же проблема, что и при попытке инициализации объекта a2 значением типа long. Ни один из конструкторов не соответствует точно типу long. Каждый требовал преобразования аргумента прежде, чем использовать конструктор.

• Стандартное преобразование long в double, затем A(double).

• Стандартное преобразование long в int, затем A(int).

Эти последовательности преобразований равнозначны, поэтому вызов неоднозначен.

Вызов функции f2() и инициализация объекта a2 неоднозначны, поскольку у необходимых стандартных преобразований одинаковый ранг (см. раздел 6.6.1). Когда используется пользовательское преобразование, ранг стандартного преобразования, если таковые вообще имеются, позволяет выбрать наилучшее соответствие:

short s = 42;

// преобразование short в int лучше, чем short в double

А a3(s); // используется A::A(int)

В данном случае преобразование short в int предпочтительней, чем short в double. Следовательно, объект a3 создается с использованием конструктора A::A(int), который запускается для преобразования значения s.

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

Перегруженные функции и конструкторы преобразования

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

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

Внимание! Преобразования и операторы

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

• Никогда не создавайте взаимных преобразований типов. Другими словами, если класс Foo имеет конструктор, получающий объект класса Bar, не создавайте в классе Bar оператор преобразования для типа Foo.

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

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

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

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

struct C {

 C(int);

 // другие члены

};

struct D {

 D(int);

 // другие члены

};

void manip(const С&);

void manip(const D&);

manip(10); // ошибка неоднозначности: manip(С(10)) или manip(D(10))

Здесь у структур С и D есть конструкторы, получающие значение типа int. Для версий функции manip() подходит любой конструктор. Следовательно, вызов неоднозначен: он может означать преобразование int в С и вызов первой версии manip() или может означать преобразование int в D и вызов второй версии.

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

manip(С(10)); // ok: вызов manip(const C&)

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

Перегруженные функции и пользовательские преобразования

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

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

struct E {

 E(double);

 // другие члены

};

void manip2(const C&);

void manip2(const E&);

// ошибка неоднозначности: применимы два разных пользовательских

// преобразования

manip2(10); // manip2(C(10) или manip2(E(double(10)))

В данном случае у класса С есть преобразование из типа int и у класса E есть преобразование из типа double. Для вызова manip2(10) подходят обе версии функции manip2():

• Версия manip2(const C&) подходит потому, что у класса С есть конструктор преобразования, получающий тип int. Этот конструктор точно соответствует аргументу.

• Версия manip2(const E&) подходит потому, что у класса E есть конструктор преобразования, получающий тип double и возможность использовать стандартное преобразование для преобразования аргумента типа int, чтобы использовать этот конструктор преобразования.

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

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

Упражнения раздела 14.9.2

Упражнение 14.50. Представьте возможные последовательности преобразований типов для инициализации объектов ex1 и ex2. Объясните, допустима ли их инициализация или нет.

struct LongDouble {

 LongDouble(double = 0.0);

 operator double();

 operator float();

};

LongDouble IdObj;

int ex1 = IdObj;

float ex2 = IdObj;

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

void calc(int);

void calc(LongDouble);

double dval;

calc(dval); // которая calc()?