Мы можем помочь компилятору в выводе аргументов шаблона, воспользовавшись объявлением дружественной функции в шаблонном классе. Это означает, что класс RationalT может объявить operator* для RationalT как функцию-друга. К шаблонам классов процедура вывода аргументов не имеет отношения (она применяется только к шаблонам функций), поэтому тип T всегда известен в момент конкретизации RationalT. Это упрощает объявление соответствующей функции operator* как друга класса RationalT:
template typename T
class Rational {
public:
...
friend // объявление функции
const Rational operator*(const Rational lhs, // operator*
const Rational rhs); // (подробности см. ниже)
};
template typename T // определение функции
const RationalT operator*(const RationalT lhs, // operator*
const RationalT rhs)
{...}
Теперь вызовы operator* с аргументами разных типов скомпилируются, потому что при объявлении объект oneHalf типа Rationalint конкретизируется класс Rationalint и вместе с ним функция-друг operator*, которая принимает параметры Rationalint. Поскольку объявляется
К сожалению, фраза «сумеет разобраться» в данном контексте имеет иронический оттенок, поскольку хотя код и компилируется, но не компонуется. Вскоре мы займемся этой проблемой, но сначала я хочу сделать одно замечание о синтаксисе, используемом для объявления функции operator* в классе Rational.
Внутри шаблона класса имя шаблона можно использовать как сокращенное обозначение шаблона вместе с параметрами, поэтому внутри RatonalT разрешается писать просто Rational вместо RatonalT. В данном примере это экономит лишь несколько символов, но когда есть несколько параметров с длинными именами, это помогает уменьшить размер исходного кода и одновременно сделать его яснее. Я вспомнил об этом, потому что operator* объявлен как принимающий и возвращающий Rational вместо RationalT. Также корректно было бы объявить operator* следующим образом:
template typename T
class Rational {
public:
...
friend
const RationalT operator*(const RationalT lhs,
const RationalT rhs);
...
};
Однако проще (и часто так и делается) использовать сокращенную форму.
Теперь вернемся к проблеме компоновки. Код, содержащий вызов с параметрами различных типов, компилируется, потому что компилятор знает, что мы хотим вызвать вполне определенную функцию (operator*, принимающую параметры типа Rationalint и Rationalint), но эта функция только
Простейший способ исправить ситуацию – объединить тело operator* с его объявлением:
template typename T
class Rational {
public:
...
friend Rational operator*(const Rational lhs, const Rational rhs)
{
return Rational(lhs.numerator * rhs.numerator, // та же
lhs.denominator * rhs.denominator); // реализация,
} // что и
// в правиле 24
};
Наконец-то все работает как нужно: вызовы operator* с параметрами смешанных типов компилируются, компонуются и запускаются. Ура!
Интересное наблюдение, касающееся этой техники: использование отношения дружественности никак не связано с желанием получить доступ к закрытой части класса. Чтобы сделать возможными преобразования типа для всех аргументов, нам нужна функция, не являющаяся членом (см. правило 24); а для того чтобы получить автоматическую конкретизацию правильной функции, нам нужно объявить ее внутри класса. Единственный способ объявить свободную функцию внутри класса – сделать ее другом (friend). Что мы и делаем. Необычно? Да. Эффективно? Вне всяких сомнений.