Конструктор як конвертер
Набір конструкторів класу, що приймають єдиний параметр, наприклад, SmallInt(int) класу SmallInt, визначає безліч неявних перетворень значення типу SmallInt. Так, конструктор SmallInt(int) перетворює значення типу int значення типу SmallInt.
При виклику calc(i) число i перетворюється на значення типу SmallInt з допомогою конструктора SmallInt(int), викликаного компілятором до створення тимчасового об'єкта потрібного типу. Потім копія цього об'єкта передається в calc(), ніби виклик функції був записаний у формі:
Фігурні дужки в цьому прикладі позначають час життя цього об'єкта: він знищується при виході з функції.
Типом параметра конструктора може бути тип деякого класу:
У такому випадку значення типу SmallInt можна використовувати усюди, де допустимо значення типу Number:
Якщо конструктор використовується для виконання неявного перетворення, чи повинен тип його параметра точно відповідати типу значення, що підлягає перетворенню? Наприклад, чи буде в наступному коді викликано SmallInt(int), визначений у класі SmallInt, для приведення dobj до типу SmallInt?
Якщо необхідно, до фактичного аргументу застосовується послідовність стандартних перетворень перед тим, як викликати конструктор, виконує певне користувачем перетворення. При зверненні до функції calc() використовується стандартне перетворення dobj з типу double на тип int. Потім для приведення результату до типу SmallInt викликається SmallInt(int).
Компілятор неявно використовує конструктор з єдиним параметром перетворення його типу на тип класу, якого належить конструктор. Однак іноді зручніше, щоб конструктор Number(const SmallInt&) можна було викликати лише для ініціалізації об'єкта типуNumber значенням типу SmallInt, але в жодному разі для виконання неявних перетворень. Щоб уникнути такого вживання конструктора, оголосимо його явним (explicit):
Компілятор ніколи не застосовує явні конструктори для виконання неявних перетворень типів:
Однак такий конструктор все ж таки можна використовувати для перетворення типів, якщо воно запрошено явно у формі оператора приведення типу:
Вибір перетворення A
Визначене користувачем перетворення реалізується як конвертера чи конструктора. Як було зазначено, після перетворення, виконаного конвертером, дозволяється використовувати стандартне перетворення для приведення повернутого значення цільового типу. Трансформації, виконаної конструктором, може передувати стандартне перетворення для приведення типу аргументу до типу формального параметра конструктора.
Послідовність певних користувачем перетворень– це комбінація певного користувачем та стандартного перетворення, яка необхідна для приведення значення до цільового типу. Така послідовність має вигляд:
Послідовність стандартних перетворень ->
Визначене користувачем перетворення ->
Послідовність стандартних перетворень
де певне користувачем перетворення реалізується конвертером чи конструктором.
Не виключено, що для трансформації вихідного значення в цільовий тип існує дві різні послідовності перетворень користувача, і тоді компілятор повинен вибрати з них кращу. Розглянемо як це робиться.
У класі дозволяється визначати багато конвертерів. Наприклад, у нашому класі Number їх два: operator int() і operator float(), причому обидва здатніперетворити об'єкт типу Number на значення типу float. Звичайно, можна скористатися конвертером Token::operator float() для прямої трансформації. Але і Token::operator int() теж підходить, тому що результат його застосування має тип int і, отже, може бути перетворений на тип float за допомогою стандартного перетворення. Чи є трансформація неоднозначною, якщо є кілька таких послідовностей? Чи якійсь із них можна віддати перевагу іншим?
float ff = num; // який конвертер? operator float()
У таких випадках вибір найкращої послідовності певних користувачем перетворень базується на аналізі послідовності перетворень, яка застосовується після конвертера. У попередньому прикладі можна застосувати такі дві послідовності:
1. operator float() -> точна відповідність
2. operator int() -> стандартне перетворення
Як було сказано в розділі 9.3, точна відповідність краща за стандартне перетворення. Тому перша послідовність краща за другу, а значить, вибирається конвертер Token::operator float().
Може статися так, що для перетворення значення в цільовий тип застосовні два різні конструктори. У цьому випадку аналізується послідовність стандартних перетворень, що передує виклику конструктора:
Тут у класі SmallInt визначено два конструктори – SmallInt(int) та SmallInt(double), які можна використовувати для зміни значення типу double в об'єкт типу SmallInt: SmallInt(double) трансформує double у SmallInt безпосередньо, а SmallInt(int) працює з результатом стандартного перетворення double в int. Таким чином, є дві послідовності визначених користувачем перетворень:
1. точну відповідність -> SmallInt(double)
2. стандартне перетворення -> SmallInt(int)
Оскільки точне відповідність краще стандартного перетворення, то вибирається конструктор SmallInt(double).
Не завжди вдається вирішити, яка послідовність краща. Може статися, що вони однаково хороші, і ми говоримо, що перетвореннянеоднозначно. У такому разі компілятор не застосовує жодних неявних трансформацій. Наприклад, якщо в класі Number є два конвертери:
то неможливо неявно перетворити об'єкт типу Number на тип long. Наступна інструкція викликає помилку компіляції, оскільки вибір послідовності певних користувачем перетворень неоднозначний:
Для трансформації num значення типу long застосовні дві такі послідовності:
1. operator float() -> стандартне перетворення
2. operator int() -> стандартне перетворення
Оскільки в обох випадках за використанням конвертера слід застосування стандартного перетворення, обидві послідовності однаково хороші і компілятор не може вибрати жодну з них.
За допомогою явного наведення типів програміст здатний задати потрібну зміну:
long lval = static_cast (num);
Внаслідок такої вказівки вибирається конвертер Token::operator int(), за яким слідує стандартне перетворення в long.
Неоднозначність при виборі послідовності трансформацій може виникнути і тоді, коли два класи визначають перетворення один одного. Наприклад:
compute (num); // помилка: можливо два перетворення
Аргумент num перетворюється на тип SmallInt двома різними способами: з допомогою конструктора SmallInt::SmallInt(const Number&) чи з допомогою конвертера Number::operator SmallInt(). Оскільки обидві зміни однаковіхороші, виклик вважається помилкою.
Для вирішення неоднозначності програміст може викликати конвертер класу Number:
compute(num.operator SmallInt());
Однак для вирішення неоднозначності не слід використовувати явне приведення типів, оскільки при відборі перетворень, що підходять для приведення типів, розглядаються як конвертер, так і конструктор:
compute (SmallInt (num)); // помилка: як і раніше, неоднозначно