We faced three related problems (Figure 3) in the previous section, and solved them with three different overloaded operator functions. Maintaining three functions is annoying but not too troublesome while they are small. However, correcting or extending them becomes more challenging as they become larger and more complex. Fortunately, we realized that we could consolidate the three operators into a single operator+ function with two changes. We begin the consolidation process by replacing the three overloaded operators with one and end the process by exploring functions that automatically convert one data type to another.
class fraction { private: int numerator; int denominator; public: fraction(int n = 0, int d = 1) : numerator(n), denominator(d); friend fraction operator+(fraction f1, fraction f2); };
(a) | (b) | (c) |
friend
function, and both arguments must be fractions.
There are often cases where converting a value represented by one data type into a value represented by a different type is necessary. What the conversion means and, therefore, how we implement it is completely dependent on the two data types. For example, the two primitive data types int
and double
are different kinds of numbers. While 2 and 2.0 are mathematically the same, they are strikingly different inside computers. Their differences notwithstanding, converting between the two types is a meaningful operation, just as the typecasting operator does.
Integer division was one of the first places we saw the need to change a value from one type to another. The formula to convert a temperature from Fahrenheit to Celsius contained the operation 5/9, which, in this form, is carried out using integer division. Integer division cannot represent a fractional amount, so 5/9 evaluates to 0. One solution was to rewrite the operation as 5.0/9.0, which is evaluated using floating-point arithmetic and produces the expected result. But what happens when both operands are variables, and we can't add a ".0?" C++ solves this problem with the casting operator.
int sum = 0; int count; . . . double average = sum / count; |
double average = (double)sum / count; double average = double(sum) / count; |
(a) | (b) |
sum / count
is still carried out using integer division: the type conversion takes place after the division but before the assignmentIt is possible to perform similar type conversions when one or both values are an instance of a class. In that case, C++ provides two mechanisms for converting one data type to the other: a conversion constructor and a conversion operator.
Conversion constructors, were introduced in chapter 9. They can convert a fundamental data type or an instance of one class into an instance of another class. For our example, we will convert (actually, we already have converted) an integer into a fraction object.
fraction(int i); |
fraction(int n = 0, int d = 1); |
fraction f(5); |
(a) | (b) | (c) |
foo(bar b); // bar → foo
foo(int i); // int → foo
Conversion constructors are not the only conversion mechanism available to C++ programmers. C++ also allows programmers to overload the casting operator demonstrated in Figure 2(b). The fraction class doesn't offer a good reason for using a conversion operator, so the following examples are once again based on the Time class, which has three integer member variables: hours, minutes, and seconds. The first step in the Time add function is to convert two Time objects into integers that the represents the time stored in each object as seconds.
We can create an overloaded conversion operator and use it to convert an instance of Time into an integer. An overloaded conversion operator can replace the expressions appearing on the right-hand side of the assignment operators in the add function.
Based on the overloaded conversion operator, it's possible to rewrite the Time add function. We also take this opportunity to change the add function into the overloaded operator+
, which is demonstrated both as a member and as a friend
. Unfortunately, neither is the final version of operator+ (see "Conversion Conflicts" in the red highlighted box below).
Member | friend |
---|---|
Time Time::operator+(Time t2) { int i1 = (int)*this; int i2 = (int)t2; return Time(i1 + i2); } |
Time operator+(Time t1, Time t2) { int i1 = (int)t1; int i2 = (int)t2; return Time(i1 + i2); } |
(a) | |
int i1 = int(*this); int i2 = int(t2); |
int i1 = int(t1); int i2 = int(t2); |
(b) | |
int i1 = *this; int i2 = t2; |
int i1 = t1; int i2 = t2; |
(c) |
operator+
. The left column illustrates operator+ implemented as a member function, while the right column illustrates it as a friend function. The member versions provide examples where the this
pointer is required and must be dereferenced with the dereference or indirection operator, *
, to obtain the implicit object rather than its address.
operator foo(); // bar → foo
operator int(); // bar → int
Unfortunately, it's not always possible for a class to define both a conversion constructor and a conversion operator to convert back and forth between the same types. If the Time class defines both a conversion constructor (int → Time) and a conversion operator (Time → int), some expressions can result in a conversion conflict. For example, let t be an instance of Time, then the expression t + 5
has two possible interpretations:
operator+(Time, Time)
: convert 5 to Time with the conversion constructor and call the Time operator+operator+(int, int)
: convert t to int with the conversion operator and add two integers with the built in integer "+" operatorIncluding both conversions in a single class makes the simple addition expression ambiguous. The compiler will display an error message, and the program will not compile.