Effective C++ item 46: Define Non-member Functions Inside Templates When Type Conversions Are Desired

As explained in item24, we use non-member function to support implicit type conversion for all arguments. So that following code will compile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Rational {
public:
Rational(int numerator = 0,
int denominator = 1)
: m_numerator(numerator)
, m_denominator(denominator){}

int numerator() const{return m_numerator;}
int denominator() const{return m_denominator;}

private:
int m_numerator;
int m_denominator;
};

const Rational operator*(const Rational &lhs, const Rational &rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}

int main() {
Rational oneHalf(1, 2);
Rational oneEighth(1, 8);
Rational result = oneHalf * oneEighth;
result = result * oneEighth;

result = oneHalf * 2;
result = 2 * oneHalf;

return 0;
}

How about do this in template?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template <typename T>
class RationalTP {
public:
RationalTP(const T& numerator = 0,
const T& denominator = 1)
: m_numerator(numerator)
, m_denominator(denominator){}

const T numerator() const{return m_numerator;}
const T denominator() const{return m_denominator;}

friend const RationalTP operator*(const RationalTP &lhs, const RationalTP &rhs) {
return RationalTP(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
private:
T m_numerator;
T m_denominator;
};

template <typename T>
const RationalTP<T> operator*(const RationalTP<T> &lhs, const RationalTP<T> &rhs) {
return RationalTP<T>(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}

int main() {
RationalTP<int> oneHalfTP(1, 2);
RationalTP<int> oneEighthTP(1, 8);
RationalTP<int> resultTP = oneHalfTP * oneEighthTP;
resultTP = resultTP * oneEighthTP;

resultTP = oneHalfTP * 2;
resultTP = 2 * oneHalfTP;

return 0;
}

You will realize that resultTP = oneHalfTP * 2 and resultTP = 2 * oneHalfTP lines won’t comiple. This is because implicit type conversion functions are never considered during template argument deduction. Such conversions are used during function calls, yes, but before you can call a function, you have to know which functions exist. In order to know that, you have to deduce parameter types for the relevant function templates (so that you can instantiate the appropriate functions). But implicit type conversion via constructor calls is not considered during template argument deduction. Item 24 involves no templates, so template argument deduction is not an issue. Now that we’re in the template part of C++, it’s the primary issue.

The way to solve the problem is to make it clear to compiler which version of function template should be instantiated when we construct the template object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
class RationalTP {
public:
RationalTP(const T& numerator = 0,
const T& denominator = 1)
: m_numerator(numerator)
, m_denominator(denominator){}

const T numerator() const{return m_numerator;}
const T denominator() const{return m_denominator;}

friend const RationalTP operator*(const RationalTP &lhs, const RationalTP &rhs) {
return RationalTP(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
private:
T m_numerator;
T m_denominator;
};

Now our mixed-mode calls to operator* will compile, because when the object oneHalfTP is declared to be of type RationalTP<int>, the class RationalTP<int> is instantiated, and as part of that process, the friend function operator* that takes RationalTP<int> parameters is automatically declared. As a declared function (not a function template), compilers can use implicit conversion functions (such as RationalTP‘s non-explicit constructor) when calling it, and that’s how they make the mixed-mode call succeed.

I also want to point out one syntax tip in above example. Inside a class template, the name of the class can be used as shorthand for the class and its parameters, so inside RationalTP<T>, we can just write RationalTP instead of RationalTP<T>.

An interesting observation about this technique is that the use of friendship has nothing to do with a need to access non-public parts of the class. In order to make type conversions possible on all arguments, we need a non-member function (Item 24 still applies); and in order to have the proper function automatically instantiated, we need to declare the function inside the class. The only way to declare a non-member function inside a class is to make it a friend.

Reference:
“Effective C++” Third Edition by Scott Meyers.